原子性问题到底如何解决呢
原子性的问题是因为线程切换,如果能够禁用线程那不就可以解决问题了吗,而操作系统做线程切换是依赖CPU中断,所以禁止CPU发生中断就可以禁止线程切换
在早期单核CPU时代,这个方案是可行的,但是在多核CPU场景就是不适应的,比如在32CPU上执行long变量的写操作说明这个问题,long类型变量是64位,在32位CPU上会被拆成两次写操作如下图
在单核CPU,同一时刻只有一个线程执行,当我们禁止CPU中断,就可以避免线程切换,这是CPU使用权的线程就可以不断的执行,做一两次写操作就一定要么执行成功,要么执行失败,保证了原子性
在多核CPU上,并不能保证同一时刻只有一个线程,比如有两个线程分别在不同的CPU上执行,禁止CPU中断,只能保证CPU上的线程连续执行,但是如何此时两个线程同时操作高32的值,就会出现bug.
同一时刻只有一个线程,称之为互斥,只要保证了对共享变量的互斥,不管在单核还是在多核CPU上都能保证原子性
简单锁模型
一般我理解的锁的样子如下图
我们把互斥执行的代码成为临时区,线程在进入临时去之前,首先尝试加锁,如果成功,则进入临时去,此时我们就有对这个线程持有锁,否则等待,知道持有锁的线程释放锁,持有锁的线程执行完临界区的代码后,执行解锁unlock看起很完美,但是我们忽略了两点
改进后的锁模型
在现实生活中,你用你家的锁,锁住你家的门,我用我家的锁,锁住我家的门,在并发编程世界里,也是一样的,这个关系正如上图一样
首先,我们要把受保护资源R标注出来,如图上的受保护资源R,其次我们要保护资源R就得为它创建一把锁LR,最后针对这个锁LR,我们还需在进出临界区添加锁,和解锁操作,同时在锁LR和受保护的资源R之间有一条关联,正如上面的那条线,如果我们用自家的锁,去锁别家的资源,就可能导致bug出现。
锁技术:synchronized
synchronized关键字就是一种锁的实现,他可以修饰方法,也可以修饰代码块,如下图
class X {
// 修饰非静态方法
synchronized void foo() {
// 临界区
}
// 修饰静态方法
synchronized static void bar() {
// 临界区
}
// 修饰代码块
Object obj = new Object();
void baz() {
synchronized(obj) {
// 临界区
}
}
}
我们发现好像没有加锁和解锁的操作,其实synchronized内部实现了加解锁的操作,这样锁是为了避免我们遗忘加锁操作,否则会出现致命的bug
同时上面加锁的对象有两条默认规则如下
上面代码可以改成下面
class X {
// 修饰静态方法
synchronized(X.class) static void bar() {
// 临界区
}
}
class X {
// 修饰非静态方法
synchronized(this) void foo() {
// 临界区
}
}
使用synchronized解决问题
如下面代码我们有类safecalc,一个属性value,两个方法get和addOne
class SafeCalc {
long value = 0L;
long get() {
return value;
}
synchronized void addOne() {
value += 1;
}
}
其中addOne是被syncronized修饰,而get没有修饰,都是对value变量的有操作,那么有没有问题呢
先拿addOne方法解释,首先使用了syncronized修饰后,就可以保证无论在单核还是多核,都可以保证原子操作,且保证了线程的可见性
管程中锁的规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁
按照上面规则如果多个线程使用addOne方法,可见性可以保证,也就说有1000个线程执行addOne方法,最终的结果就是1000,
看上去还是很完美,但是我们忘记了get方法,因为管程中锁的规则是只能保证后续操作对这个锁的加锁的可见性,而get并没有加锁操作,因此并不能保证可见性,这个问题也很简单,只要加上synchronized,就可以解决
class SafeCalc {
long value = 0L;
synchronized long get() {
return value;
}
synchronized void addOne() {
value += 1;
}
}
上面代码get和addOne方法都需要访问value这个受保护的资源,这个资源使用this对象这把锁保护,线程进入临界区get和addone,必须获取this这把锁,因此get和addone互斥,这样就可以避免并发问题
这里就像球场的门票管理一样,一个座位只能有一个人使用,这个座位就是受保护的资源,而入场就是Java类中的方法,而门票就是保护资源的锁,java检票就由synchronized执行
锁和受保护资源的关系
受保护资源和锁的关系是N:1的关系一把锁可以锁多个资源,对应的现实中就是球赛的作为让你包场了
我们把上面的例子修改一下,看看有没有并发问题
class SafeCalc {
static long value = 0L;
synchronized long get() {
return value;
}
synchronized static void addOne() {
value += 1;
}
}
我们发现get和addone分别用两把不同的锁,get使用的this,而addOne使用的safecalc.Class,此时由于是不同的两把锁,而临界区没有互斥关系,因此两个方法对临界区的value就没有办法保证可见性,就会引发并发问题,如下图