说到多线程的原子性、可见性和有序性。
这是多线程确保线程安全的三个标准。
首先。咱说说。原子性。原子性其实很好理解。原子就是最小的单元,他就是可执行的最小的单元。在程序执行的时候,最小的一个可执行单元就是一个原子。
一段原子性的代码执行的时候。不会被打断。这一段代码的执行,要么不执行,要么全部执行完毕。
这段代码也许只有一行代码,也许是多行代码。
一行代码很多也不是原子性的,因为这个原子性并非是我们Java程序代码的原子性,而是CPU执行 阶段的原子性。并不是说代码少他就是原子性。也不是说代码多的就不是原子性。一行代码很多都不是原子性的,多行代码加上锁,也可以是原子性的。
比如。定义一个整型int i = 10。这一行代码它就是原子性的,这里是给整形变量i赋值,这个就是原子性的,它不可能被打断。
如果定义的一个长整型的 long j=10,这就不是原子性的,为什么呢?因为这个long类型啊,他是64位的,64位的长整型。在CPU中执行赋值操作的时候,它是分两步,分别64位的高位和低位进行赋值,这是分两步来完成的。类似的double也是这样子,double也是64位的,会分两步分别给高32位和低32位赋值,就是两步操作,不是原子性的。
那么咱们来看另一个语句。int i=10; i++ ; 这个i=10我们刚刚说过了,是原子性的,那这个 i++是不是原子型的呢?直接说答案:i++不是原子型的。
别看这么简单的一个计算,写程序就一行,但到了CPU级别就需要3条指令,1 获取i的值, 2 执行i+1 的操作,3 把i+1的结果赋值给i 。所以这个i++不是原子性的。
这么简单的语句都不是原子性的,那么是不是对于多行代码就肯定不是原子性了呢?其实也不是,Java可以利用锁来保证多行语句是原子性的。
从CPU的角度来看,就是插入一个Lock指令。当一段代码被Lock指令锁住后,一个线程执行这段代码的时候,其他线程就无法执行,只能等着这个线程执行完毕才能获得执行权。这就是保证这段代码的原子性。
具体在Java代码里面写的时候就用synchronized 和lock对象,来实现一段代码,一个方法,一个对象的锁。这是对原子性的解释。
那么可见性是什么呢?在Java的内存模型中,每一个子线程会拥有一个单独的工作内存,主线程有一个主内存。主内存中存放的是共享变量,虽说是共享变量,看起来是被主线程和子线程共享的,那么子线程就可以读写操作该共享变量了吧?其实不行的,子线程是不能直接读写操作主内存中的共享变量的。
子线程会在工作内存中创建该共享变量的副本,然后读写操作该副本。这里再强调一下,子线程不能直接读写主内存的共享变量,只能读写工作内存中的共享变量的副本,读写完毕后再将副本的值回写到主内存中。
这时候,我们就发现一个问题了,假设主线程中有共享变量 x = 10 , 有两个子线程 A和B,要操作共享变量x,就会分别在各自的工作内存中加载一份x的副本。这时候咱们看到了,原来只有一个x,现在变成了3份:主内存中的x和两个副本x。
如果子线程A对副本x做了+1 操作,线程A中的x就变成 11 了,但线程B看到的x还是10,这就出现了可见性的问题。
通过上面的描述,总结一下什么是线程的可见性问题:就是多线程情况下,一个线程修改了变量的值,另一个线程看不到这个变化。
那怎么解决这个问题呢?思路是这样的,对于线程A来说,只要修改了x副本的值,马上把这个值回写到主内存中;对于线程B来说,只要想读x的值,就再次从主线程加载x,这样副本的值永远都与主内存中的值是一样的。这就可以解决可见性问题了。
在具体实现上Java内存模型是通过内存屏障来实现的。
那这里又引出一个内存屏障的概念,啥是内存屏障呢?内存屏障其实就是一个指令,将这个指令插入到其他指令之间,会执行某些特殊的操作。具体到解决可见性是两个内存屏障指令:load屏障和store屏障。 load和store这俩词儿之前讲内存模型的8个指令的时候见过,load指令是工作内存从主内存加载共享变量生成副本的指令,store指令是将工作内存的共享变量副本回写到主内存的指令。那么load屏障就呼之欲出了,这是在指令A前插入load屏障,那么指令A用到的变量副本将失效,必须从主内存加载对应的共享变量的值并替换当前副本;对应的store屏障插入到指令B之后,那么指令B修改的副本的值,马上会被回写到主内存。
这个效果应该可以想象了吧?通过load屏障和store屏障,可以实现修改了副本马上回写到主内存,读副本之前先从主内存加载一下,这样就确保了,只要副本值修改了马上就更新到主内存里面,然后另一个线程读副本的时候直接装载主内存中的新值,这样就解决了多线程下变量的可见性。
接下来讲解一下有序性。
什么是有序性呢?这里的序是指什么的顺序呢?
这里说的序是指CPU中指令执行的顺序,就是一条条的汇编指令的先后顺序。我们Java程序员写的是Java代码,写出来一行行的Java代码最后编译转为汇编在CPU中执行,这时候就是一条条的汇编指令了。一般程序员的理解是这样的,写了两行代码,行A在行B的前面,应该是先执行A再执行B,但其实CPU在指令执行的时候做了优化,如果指令A和指令B调换顺序后不影响执行结果,那么CPU会进行指令重排,重排后就是先执行指令B再执行指令A。这个原则叫做as if serial ,就是好像是连续的一样,也就是重排序优化后就好像没有重排序,而是正常连续执行的那样。
比如:
int i=10;
int j=20;
这样的两条指令,先执行哪条其实是无所谓的,这时候CPU就会执行指令重排。
对于单线程程序来说,CPU指令重排绝对不会有问题,但对于多线程指令重排就有可能产生数据安全问题。
打个比喻:
刘能和赵四去买冰糕,这个指令顺序是,推门进冷饮店,掏钱,拿冰糕。
如果就刘能自己去买冰糕的话,先掏钱再拿冰糕还是先拿冰糕再掏钱,没什么关系,冷饮店老板照顾的过来。
现在刘能和赵四俩人一起去,结果赵四掏钱了,还没拿冰糕,刘能先执行了拿冰糕,这下赵四不干了,我掏的钱,我掏的钱。老板说,没办法,人太多忙不过来,先后顺序调整一下,这样快,你别叫唤了。
那赵四能干嘛?赵四大叫:不行啊,出现多线程的有序性问题了。
那咋解决这个问题呢?还是下内存屏障。
有序内存屏障是在两条指令之间插入的屏障,插入后,前后两条指令就不能重排序了。
处理有序性的屏障有4个。
LoadLoad屏障:下在load和load之间,这个屏障指令之前的load指令和之后的load相关的指令不能重排序;
LoadStore屏障:下在load和store之间,这个屏障指令之前的load指令和之后的store指令不能重排序;
StoreLoad屏障:下在store和load之间,这个屏障指令之前的store指令和之后的load指令不能重排序;
StoreStore屏障:下载store和store之间,这个屏障指令之前的store指令和之后的store指令不能重排序。
说白了,这个有序性的内存屏障,就是禁止CPU对前后的指令进行重排序的,有了这个屏障,多线程下CPU也不能做重排序优化了,就解决了有序性的问题了。
关于内存屏障的细节,我们今儿就不聊了,以后我们再细说。
这个多线程的原子性、可见性和有序性是啥意思,你明白了吧?