在上次的文章中我们聊了Java 内存模型(JMM)非要把它讲清楚,Java内存模型(JMM),那是JVM层面的规范,来去解决并发编程中带来的缓存一致性的问题。但是我们想要写出安全的并发程序,具体要怎么做呢?我们应该如何应用这些规则,或者触发这些规则呢?希望大家在读之前要明确自己的目的,我们现在要写的是并发程序,我们要解决的是在并发情况下出现的问题,不能只从单线程的角度思考。在这个前提下我们思考如下问题:
什么才是安全的并发程序?
简单来说,并发程序中,有三大特性:原子性,可见性,有序性。相关解释的程序网上有很多,我先用通俗一点的话给大家解释一下:
- 原子性:一个线程要执行就把这块的代码都执行了,要不就不执行,不能执行到一半,被其他线程打断,或者其他线程也来执行。
- 可见性:当一个线程改了一个大家共享的变量,大家都知道他改了,例如:库存只有一本书,一个线程改了库存为0,其他线程就要马上知道,不然就大家就会还有库存,就会导致事故。
- 有序性:理论上来说我们的程序都是按我们所写的代码从上到下执行的,但我们的java执行引擎会对我们的指令集进行优化,改变他们的执行顺序,使得执行效率提高。他最直观的一个指令重排原则就是单线的结果不改变,但多线程的结果就可能会出问题。我们写代码智慧就在于有指令重排的前提下,如何使得多线程也不会出问题。
我们在知道了我们要写出什么样的并发程序之后就可以来认识我们今天的主角了 happens-before 原则 和 Volatile 关键字。
如何写出安全的并发程序?
上面balabala讲了一堆,有人不服了,因为真的不知道老夫现在写的这三两行代码,到底会不会在多线程高并发的环境下出问题啊!这就提现出了 happens-before 原则 的作用。他和JMM 类似是一种规则,只不过他是给我们程序员看的,我们通过这个规则来判断我们目前写的这份代码,满不满足有序性、原子性、可见性,放到多线程环境下会不会出问题。 以下就是 happens-before 原则的具体内容:
- 程序顺序规则: 一个线程中的每个操作,happens-before于该线程中的任意后续操作(注意这里指的是一个线程内的视角,即使有指令重排,但对程序员是透明的,所以这是程序看的规则是没问题的)
- 监视器锁规则: 对一个线程的解锁,happens-before于随后对这个线程的加锁
- volatile变量规则: 对一个volatile域的写,happens-before于后续对这个volatile域的读
- 传递性: 如果A happens-before B ,且 B happens-before C, 那么 A happens-before C
- start()规则: 如果线程A执行操作ThreadB_start()(启动线程B) , 那么A线程的ThreadB_start()happens-before 于B中的任意操作
- join()原则: 如果A执行ThreadB.join()并且成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
- interrupt()原则: 对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生
- finalize()原则: 一个对象的初始化完成先行发生于它的finalize()方法的开始
这里边涉及的规则比较多和上次我们讲的JMM的规则差不多,大家自己悟一悟,今天我们 把Volatile 的规则提一下,概括一下就是先写后读,写的肯定可以读到,可见性就有保证了。
那其他两个特性呢,volatile 禁止了指令的重排,有序性可以保证了。
原子性 volatile 保证不了得用synchronized,Lock或者并发包下的atomic。
估计写到这有人就有人骂了,“就这?你说行就行了,为啥啊?” 这一小节重点就是如何写并发程序,我们先会用就行。下面我们再聊底层的实现。
底层如何实现?
其实底层实现我们已经讲过了,就在上一篇文章里非要把它讲清楚,Java内存模型(JMM),那就已经是最底层了,不,是java的最底层了。我们在上一次的文章中定义了几种JMM操作和规则,volatile 就是应用这些操作和规则,建立了内存屏障从而保证了有序性和可见性。那我们再聊聊内存屏障,这条从程序员到jvm底层的路就基本通了,可是今天有点累了下次再说吧。
|