转自: 多线程编程从来都是一件比较困难的事情,调试多线程程序也相当困难,这种困难来自于线程对共享资源操作的复杂性 ( 包括对于资源操作的线程间的先后顺序 ) 。对于 Java 来说,它封装了底层硬件和操作系统之间很多的细节,对于线程之间的调度底层细节我们大多数时候不用关心,然而真正编写 java 多线程程序时有一些东西我们却是不得不知道的。 在 java 中, 1、 多个线程之间数据交换是依靠内存来实现的。 注意以上提到的几点,理解他们在 java 多线程编程中至关重要,再继续之前,我来举两个实际的例子来说明一下上面的 2 和 3 点。 对于第 2 点,先来 看这一段代码:
在单线程中执行这一段代码,处理器将 y 读入缓存,并且在执行 y+3 后,将计算到的结果再次存入缓存中,在某个时候 ( 可能立即,也可能在之后的某个合适的时间 ) 再将这个 b 值刷新到内存中。 换成多线程,线程 A 执行 (1)(2) ,而从另外一个线程 B 来读取 b 的值会有什么样的结果呢?答案是不确定。为什么呢 ? 即使是 A 线程先执行了 (1)(2) ,线程 B 再读取 b 的值,也有可能读取不到,因为有可能 b 的真正的值可能还在缓存中,而线程 B 只能从自己的缓存或者从内存中去读取所要的值,这就会造成 B 读到的可能是一个过期的内存值。 对于第 3 点,排序是什么意思呢?同样来看一段代码
我们直观会觉得,处理器会依次执行上面的代码,但是答案也是不确定的,因为不同的编译器或处理器为了获得{zg}性能,很可能会调整最终代码的执行顺序,只要最终不影响程序语义即可,例如,这里可以先执行 (3)(4)(6), 再执行 (5)(6)(7) ,这都是不影响最终结果和语义的,怎么调整都可以。更甚至对于这样的顺序操作 A. 从内存读取数值到缓存 B. 执行得到结果并放入缓存 C. 将缓存数据刷新到内存,这么几步操作都有可能被编译器给颠倒执行,本来正常应该 ABC 的顺序,{zh1}真正执行的可能是 ACB 的顺序。 对于只在线程内执行的操作和访问的变量来讲,上面的几点都不会有问题,而对于会在多线程中来访问和操作的变量来说上面的优化可能会变成了灾难。
为了解决资源争用的问题, java 引入了 synchornized 关键字,同时它还有另外一层语义,那就是解决了值可见性问题。 Synchornized 关键字保证了: 1、 在进入同步块时,失效缓存,强制从内存读取{zx1}值。 2、 在退出同步块时,将缓存值强制刷到内存中。 以上两点保证了同一个监视器保护下的多个线程都可以看到{zx1}值,而不会读取到过期值,如果线程 A 进入同步块,执行后得到的所有共享变量值,在它退出后,对紧接着进入同一个同步块的线程 B 都是可见的,即线程 B 可以保证读取到线程 A 在同步块中计算到的共享变量的{zx1}值。 在 java 中还定义另一个关键字,也一样可以保证变量的在跨线程中的可见性,那就是 volatile ,它保证读写直接在主存而不是寄存器或者本地处理器缓存中进行。即使用 volatile 修改的变量可以保证在其他线程中读取该变量时可以读取到,例如,如果上面 (2) 中 b 加上 volatile 关键字,那么在线程 B 中就可以立马看到该变量修改的{zx1}值。 如果既没有使用 synchornized ,也没有使用 volatile 的共享资源,那么在 java 中是不保证线程之间对{zx1}值是可见的。
上面还谈到了编译器对内存操作重排序的问题,这有什么影响呢?看如下代码,
原始想法是线程A 如果完成了对config 的初始化,设置initialized 为true表示初始化完成,B 线程如果检测到初始化完成,则执行use config 。然而这段代码可能并不会像我们想的那样运行,前面说过了,有可能线程B 永远都看不到initialized=true 的那{yt},因为这里没有任何保证线程B能够看到initialized读取到{zx1}值, 如果initialized 加上volatile 关键字会怎么样呢?将(8) 修改成 volatile boolean initialized = false; 就可以保证线程B 可以看到initialized 的{zx1}值。在JDK1.5之前,解决了这个可见性问题,但是又有一个问题出现了,因为JDK1.5之前编译器对 volatile 变量的读和写不能与对其他 volatile 变量的读和写一起重新排序,但是它们仍然可以与对不是 volatile 变量的读写一起重新排序,意思是说,这里 (9) 和 (10) 的执行顺序有可能被编译器给颠倒了,此时如果线程 B 检测到 initialized 为true ,准备执行config 时,却因为config 没有被初始化导致代码出现严重错误,杯具。可见,这里编译器调换了执行顺序对于多线程来说有时候是多么可怕。但是在JDK1.5 中, volatile 又增加了一个语义,那就是申明了 volatile 的变量告诉编译器不能和其他非 volatile 变量一起排序,同时volatile 变量自身的所有内存操作也必须按照顺序执行,不能颠倒。因此 volatile 的变量其实是关闭了编译器对其的优化。
前面也讲了,在线程内编译器对操作进行排序优化,只要其中不要涉及到公共资源的操作,并不会引起什么问题,但是一旦进行了排序,而我们在大多数时候又无法预料线程与线程之间的操作执行顺序,就可能会引起程序 crash. 在 java 中,新的 java 内存模型定义了一部分线程与线程之间操作的执行顺序,叫做 happen-before ,它保证只要满足 happen-before 关系,那么后面的操作可以看到前面操作的结果。
线程内的每个操作happen-before稍后按程序顺序传入的该线程中的每个操作。
明白了以上几点,就可以解释一个经典 DCL 问题,例如:
这段代码有问题么?标准的double check.instance = new Singleton()根据以上几点分析, mem = allocate(); //Allocate memory for Singleton object.
|