JUC的本质

在介绍可见性、原子性、有序性的时候,特意提到缓存导致的可见性问题,线程切换带来的原子性问题,编
译优化带来的有序性问题,其实缓存、线程、编译优化的目的和我们写并发程序的目的是相同的,都是提高
程序性能。但是技术在解决一个问题的同时,必然会带来另外一个问题,所以在采用一项技术的同时,一定
要清楚它带来的问题是什么,以及如何规避。

 

那synchronized里的加锁lock()和解锁unlock()锁定的对象在哪里呢?上面的代码我们看到只有修饰代码块的时候,锁定了一个obj对象,那修饰方法的时候锁定的是什么呢?这个也是Java的一条隐式规则:

当修饰静态方法的时候,锁定的是当前类的Class对象,在上面的例子中就是Class X;
当修饰非静态方法的时候,锁定的是当前实例对象this。

编译优化带来的有序性问题
那并发编程里还有没有其他有违直觉容易导致诡异Bug的技术呢?有的,就是有序性。顾名思义,有序性指
的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序
中:“a=6;b=7;”编译器优化后可能变成“b=7;a=6;”,在这个例子中,编译器调整了语句的顺序,
但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的Bug。
在Java领域一个经典的案例就是利用双重检查创建单例对象,例如下面的代码:在获取实例getInstance()的
方法中,我们首先判断instance是否为空,如果为空,则锁定Singleton.class并再次检查instance是否为
空,如果还为空则创建Singleton的一个实例。

public class Singleton {
 static Singleton instance;
 static Singleton getInstance(){
 if (instance == null) {
  synchronized(Singleton.class) {
       if (instance == null)
             instance = new Singleton();
}
  }
   return instance;
 }
}

假设有两个线程A、B同时调用getInstance()方法,他们会同时发现 instance == null ,于是同时对
Singleton.class加锁,此时JVM保证只有一个线程能够加锁成功(假设是线程A),另外一个线程则会处于等
待状态(假设是线程B);线程A会创建一个Singleton实例,之后释放锁,锁释放后,线程B被唤醒,线程B
再次尝试加锁,此时是可以加锁成功的,加锁成功后,线程B检查 instance == null 时会发现,已经创
建过Singleton实例了,所以线程B不会再创建一个Singleton实例。
这看上去一切都很完美,无懈可击,但实际上这个getInstance()方法并不完美。问题出在哪里呢?出在new
操作上,我们以为的new操作应该是:
1. 分配一块内存M;
2. 在内存M上初始化Singleton对象;
3. 然后M的地址赋值给instance变量。
但是实际上优化后的执行路径却是这样的:

1. 分配一块内存M;
2. 将M的地址赋值给instance变量;
3. 最后在内存M上初始化Singleton对象。

优化后会导致什么问题呢?我们假设线程A先执行getInstance()方法,当执行完指令2时恰好发生了线程切
换,切换到了线程B上;如果此时线程B也执行getInstance()方法,那么线程B在执行第一个判断时会发现
instance != null ,所以直接返回instance,而此时的instance是没有初始化过的,如果我们这个时候
访问 instance 的成员变量就可能触发空指针异常。

 

 

Java方法里面的局部变量是否存在并发问题?

一点问题都没有。因为每个线程都有自己的调用栈,局部变量保存在线程各自的调用栈里面,不会共
享,所以自然也就没有并发问题。再次重申一遍:没有共享,就没有伤害。

 

posted @ 2020-11-18 10:10  不死码农  阅读(147)  评论(0编辑  收藏  举报