volatile关键字
内存模型的相关概念
如上图所示,在java中,每个线程都拥有自己的本地空间,在需要对共享变量操作时,会将共享变量复制一份到自己的本地空间,操作完成后,在某个时间刷回主内存。线程与线程所拥有的空间是相互独立的,一般情况下是不可见的,这个时候,就会出现一个问题,内存可见性问题。
内存可见性问题
先举个例子
1 | public class RunThread extends Thread { |
本来在thread.setRunning(false);
后,线程就应该停止的了。但是在我们运行后,线程一直都在循环当中,无法结束。
这里的话,64位的jdk无须做任何修改(默认就是server方式启动),32位的需要加上-server参数。
仔细的分析一下,有两个线程,一个main线程,一个RunThread线程。他们都会试图操纵isRunning变量。而按照上面所说的,main线程会将isRunning变量读取到本地线程内存空间,修改后,刷会主内存。而RunThread线程则从主内存中获取isRunning变量到本地线程内存空间,通过读取isRunning的值,作为while循环的判断条件。本来到这里是没什么问题的。但是,当JVM是以-server模式与性的时候,线程会一直在私有堆栈中读取变量的值,也就是说,RunThread线程读取的一直都是没有改变过的那个isRunning的值,始终无法读取到main线程改变的isRunning的值。所以这里就出现了死循环了。
如果在while循环中加上一句sout,会发现执行若干次后,程序结束了循环。
这种情况,称为“活性失败”
这里只需要将isRunnig用volatile关键字修饰,使得isRunning在各个线程之间是可见的,会强制线程从主内存中取被volatile修饰的isRunning变量。
原子性问题
所谓原子性,就是某系列的操作要么全部执行,要么全不执行。
比如说i = i + 1;
,粗略的可分成三个步骤:
- 从内存中取出i的值
- 将i的值加1
- 将相加后的值放到i中,写回内存
上面用volatile关键字解决了可见性问题,但是volatile是非原子性的。
1 | public class MyThread extends Thread { |
正常的来说,最后count的值应该是10000,但实际上,往往会比这个值要小一些。
比如,假设 i 自增到 5,线程A从主内存中读取i,值为5,将它存储到自己的线程空间中,执行加1操作,值为6。此时,CPU切换到线程B执行,从主从内存中读取变量i的值。由于线程A还没有来得及将加1后的结果写回到主内存,线程B就已经从主内存中读取了i,因此,线程B读到的变量
i 值还是5。有点类似于数据库的脏读了。
这种情况称为“安全性失败”
有序性问题
一般来说,JVM为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
1 | int i = 0; |
在上面,第三句与第四句谁先执行都对程序结果是没有影响的,因为在他们中间也没有其他的语句了,你先我先,都一样的。
JVM在对指令进行重排序的时候,是根据指令之间的数据依赖性来判断是否要重排序的,从而来保证结果没有任何影响。
但是,这仅仅是在单个线程内,如果是在多个线程呢?
1 | //一些声明 |
上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
那么如果我们在声明inited变量的时候,用volatile关键字修饰,那么就不会存在这个问题,JVM保证volatile修饰的变量不会发生指令重排序,也就是说,volatile修饰的变量会禁止指令重排序。
Java中的有序性:
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
下面就来具体介绍下happens-before原则(先行发生原则):
①程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
②锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
③volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
④传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
⑤线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
⑥线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
⑦线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
⑧对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。
下面我们来解释一下前4条规则:
对于程序次序规则来说,就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,但是虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。
第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果处于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。
第三条规则是一条比较重要的规则。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。
第四条规则实际上就是体现happens-before原则具备传递性。
总结
volatile主要用于线程对共享变量的使用的安全问题,使线程每次在获得共享变量的值时,都是从主内存中去获取,而不是从线程的私有内存中读取,从而来保证数据的可见性。
volatile只能修饰变量,不能修饰代码块,方法等(可以考虑使用synchronized关键字)
仅仅使用volatile的话,是无法保证线程的安全性的,而synchronized可以)
synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。
对于多线程中的并发安全问题,volatile关键字是这样子的:
- 保证内存可见性
- 不保证原子性
- 禁止指令的重排序