线程的概念(三)---Java的内存模型JMM

1、JMM的关键技术点都是围绕着多线程的原子性、可见性有序性来建立的。

1.1、原子性(Atomicity)

  原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

  比如,对于一个静态全局变量int i,两个线程同时对它赋值,线程A给它赋值1,线程B给它赋值为-1。那么不管这两个线程以何种方式、何种步调工作,i的值要么是1,要么是-1。线程A和线程B之间是没有干扰的。这就是原子性的一个特点,不可被中断。

  但如果我们不使用int型数据而使用long型数据,可能就没有那么幸运了。对于32位系统来说,long型数据的读写不是原子性的(因为long型数据有64位)。也就是说,如果两个线程同时对long型数据进行写入(或者读取),则对线程之间的结果是有干扰的。

1.2、可见性(Visibility)

  可见性是指当一个线程修改了某一个共享变量的值时,其他线程是否能够立即知道这个修改。显然,对于串行程序来说,可见性问题是不存在的。因为你在任何一个操作步骤中修改了某个变量,在后续的步骤中读取这个变量的值时,读取的一定是修改后的新值。

1.3、有序性(Ordering)

  有序性问题可能是三个问题中最难理解的了。对于一个线程的执行代码而言,我们总是习惯性地认为代码是从前往后依次执行的。这么理解也不能说完全错误,因为就一个线程内而言,确实会表现成这样。但是,在并发时,程序的执行可能就会出现乱序。给人的直观感觉就是:写在前面的代码,会在后面执行。听起来有些不可思议,是吗?有序性问题的原因是程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。下面来看一个简单的例子:

  

 

 

   假设线程A首先执行writer()方法,接着线程B执行reader()方法,如果发生指令重排,那么线程B在代码第10行时,不一定能看到a已经被赋值为1了。图1.15展示了两个线程的调用关系。

  

 

  这确实是一个看起来很奇怪的问题,但是它确实可能存在。

  注意:这里说的是可能存在。因为如果指令没有重排,这个问题就不存在了,但是指令是否发生重排、如何重排,恐怕是我们无法预测的。因此,对于这类问题,我认为比较严谨的描述是:线程A的指令执行顺序在线程B看来是没有保证的。如果运气好的话,线程B也许真的可以看到和线程A一样的执行顺序。

  不过这里还需要强调一点,对于一个线程来说,它看到的指令执行顺序一定是一致的(否则应用根本无法正常工作)。也就是说指令重排是有一个基本前提的,就是保证串行语义的一致性。指令重排不会使串行的语义逻辑发生问题。因此,在串行代码中,大可不必担心。

   注意:指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。  

  那么,好奇的你可能马上就会在脑海里闪出一个疑问,为什么要指令重排呢?一步一步执行多好呀!也不会有那么多奇葩的问题。
之所以那么做,完全是出于性能考虑。我们知道,一条指令的执行是可以分为很多步的。

2、哪些指令不能重排:Happen-Before规则

  在前文已经介绍了指令重排,虽然Java虚拟机和执行系统会对指令进行一定的重排,但是指令重排是有原则的,并非所有的指令都可以随便改变执行位置,以下罗列了一些基本原则,这些原则是指令重排不可违背的。

● 程序顺序原则:一个线程内保证语义的串行性。
● volatile规则:volatile变量的写先于读发生,这保证了volatile变量的可见性。
● 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前。
● 传递性:A先于B,B先于C,那么A必然先于C。
● 线程的start()方法先于它的每一个动作。
● 线程的所有操作先于线程的终结(Thread.join())。
● 线程的中断(interrupt())先于被中断线程的代码。
● 对象的构造函数的执行、结束先于finalize()方法。

 

posted @ 2019-11-28 23:08  jackcto  阅读(141)  评论(0)    收藏  举报