原子性问题
原子性问题的源头是线程切换,如果能够禁用线程切换就可以解决原子性的问题,而操作系统做线程切换是依赖CPU中断的,所以禁用CPU发送中断就可以禁止线程切换。
在单核时代很容易实现:但是在多核场景下,同一时刻有可能有两个线程同时在执行,一个线程执行在CPU1上,一个线程执行在CPU2上,此时禁止CPU中断,只能保证COU上的线程连续执行,并不能保证同一时刻只有一个线程执行。
同一时刻只有一个线程执行,我们称之为互斥,如果我们能够保证对共享变量的修改是互斥的,那么就可以保证原子性。
Java提供的锁技术:synchronized
class X {
// 修饰非静态方法
synchronized void foo() {
// 临界区
}
// 修饰静态方法
synchronized static void bar() {
// 临界区
}
// 修饰代码块
Object obj = new Object();
void baz() {
synchronized(obj) {
// 临界区
}
}
}
Java一条隐士规则:
1.当修饰静态方法的时候,锁定的是当前类的Class对象
2.当修饰非静态方法时,锁定的时当前实例对象this
下面分析一下下面的场景:
class SafeCalc {
long value = 0L;
long get() {
return value;
}
synchronized void addOne() {
value += 1;
}
}
synchronized修饰的的临界区时互斥的,同一个时刻只有一个线程能够执行临界区里的代码,而对一个对象的解锁操作 Happens-Before 于后续对这个对象的加锁操作,综合Happens-Before的传递性原则 可以得出 一个线程在临界区修改共享变量(在解锁之前)对于后续进入临界区的线程(该操作在解锁之后)是可见的,所以使用synchronized关键字可以解决两个线程修改共享变量的值和预期的不一样的问题。但只能get是没有synchronized修饰的,根据管程中锁的原则,解锁的操作总保证对于后续的解锁是可见的,而Get方法并没有加锁操作,所以没法保证可见性。
式列2:
class SafeCalc {
static long value = 0L;
synchronized long get() {
return value;
}
synchronized static void addOne() {
value += 1;
}
}
这里使用两把锁保护同一资源,一个对象是this,一个对象是Class,由于临界区是两把锁保护的,因而这两个临界区没有互斥性,这样就会导致并发问题了。
式列3
class SafeCalc {
long value = 0L;
long get() {
synchronized (new Object()) {
return value;
}
}
void addOne() {
synchronized (new Object()) {
value += 1;
}
}
}
加锁本质就是在锁对象的对象头中写入当前线程id,但是new object每次在内存中都是新对象,所以加锁无效。这样写会被Jvm逃逸分析的优化后synchronized代码会被优化掉,等于没加锁(而且这种new出来只在一个地方使用的对象,其它线程不能对它解锁,这个锁会被编译器优化掉)