解决可见性和有序性,不能保持原子性
这是与volatile的使命有关的,创造它的背景就是在某些情况下可以代替synchronized
实现可见性的目的,规避synchronized
带来的线程挂起、调度的开销。
防止重排序
线程单例模式的实现——双重检查加锁(DCL)
1 package com.paddx.test.concurrent;
2
3 public class Singleton {
4 public static volatile Singleton singleton;
5
6 /**
7 * 构造函数私有,禁止外部实例化
8 */
9 private Singleton() {};
10
11 public static Singleton getInstance() {
12 if (singleton == null) {
13 synchronized (singleton) {
14 if (singleton == null) {
15 singleton = new Singleton();
16 }
17 }
18 }
19 return singleton;
20 }
21 }
分析为什么在对象singleton
前面加上volatile
关键字
对象创建的3个过程(1)分配内存空间(2)初始化对象(3)将内存空间的地址赋值给对应的引用
因为操作系统可以对指令进行重排序,上面的过程顺序可能为:
(1)分配内存空间(2)将内存空间赋值给对应的引用(3)初始化对象
重排序容易将未初始化的对象暴露出来,所以需要设置成volatile
类型的变量
有序性的实现
happen-before规则
- 程序次序规则:在一个线程里,按照代码顺序,书写在前面的操作优先发生于书写在后面的操作。准确的是控制顺序而不是代码顺序,因为要考虑分支、循环结构
- 监视器锁规则:一个
unlock
操作先行发生于后面对同一个对象锁的lock操作。这里强调的是同一个锁,而“后面”指的是时间上的先后顺序,如发生在其他线程中的lock
操作。
-volatile
变量规则:对一个volatile
变量的写操作发生于后面对这个变量的读操作,这里的“后面”也指的是时间上的先后顺序。 - 线程的
start()
方法 happen-before 该线程所有的后续操作。 - 如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。
重排序规则表
解释:
- 当第二个操作是volatile 写时,不管第一个操作是什么,都不能重排序。
- 当第一个操作是volatile 读时,不管第二个操作是什么,都不能重排序。
- 当第一个操作是volatile 写,第二个操作是volatile 读时,不能重排序
实现可见性
可见性问题:主要指一个线程修改了共享变量值,而另一个线程却看不到,volatile
可以解决此问题
每个线程拥有自己的告诉缓存区——线程工作内存
1 package com.paddx.test.concurrent;
2
3 public class VolatileTest {
4 int a = 1;
5 int b = 2;
6
7 public void change(){
8 a = 3;
9 b = a;
10 }
11
12 public void print(){
13 System.out.println("b="+b+";a="+a);
14 }
15
16 public static void main(String[] args) {
17 while (true){
18 final VolatileTest test = new VolatileTest();
19 new Thread(new Runnable() {
20 @Override
21 public void run() {
22 try {
23 Thread.sleep(10);
24 } catch (InterruptedException e) {
25 e.printStackTrace();
26 }
27 test.change();
28 }
29 }).start();
30
31 new Thread(new Runnable() {
32 @Override
33 public void run() {
34 try {
35 Thread.sleep(10);
36 } catch (InterruptedException e) {
37 e.printStackTrace();
38 }
39 test.print();
40 }
41 }).start();
42
43 }
44 }
45 }
直观上说,这段代码的结果只可能有两种:b=3;a=3 或 b=2;a=1。不过运行上面的代码(可能时间上要长一点),你会发现除了上两种结果之外,还出现了第三种结果:
b=3,a=1
原因:
原因就是第一个线程将值a=3修改后,但是对第二个线程是不可见的,所以才出现这一结果。
如果将a和b都改成volatile类型的变量再执行,则再也不会出现b=3;a=1的结果了。
可见性实现原理(重要)
-
修改
volatile
变量时会强制将修改后的值刷到主存中 -
修
改volatile
变量后会导致其他线程工作内存中对应的变量值失效,因此,再读取该变量值的时候需要重新从主存中读取。
不能保证原子性
1 package com.paddx.test.concurrent;
2
3 public class VolatileTest01 {
4 volatile int i;
5
6 public void addI(){
7 i++;
8 }
9
10 public static void main(String[] args) throws InterruptedException {
11 final VolatileTest01 test01 = new VolatileTest01();
12 for (int n = 0; n < 1000; n++) {
13 new Thread(new Runnable() {
14 @Override
15 public void run() {
16 try {
17 Thread.sleep(10);
18 } catch (InterruptedException e) {
19 e.printStackTrace();
20 }
21 test01.addI();
22 }
23 }).start();
24 }
25
26 Thread.sleep(10000);//等待10秒,保证上面程序执行完成
27
28 System.out.println(test01.i);
29 }
30 }
可能每个人运行的结果不相同。不过应该能看出,volatile
是无法保证原子性的(否则结果应该是1000)。原因也很简单,i++其实是一个复合操作,包括三步骤:读取i的值。对i加1。将i的值写回内存。
volatile与synchronized的区别
当多个线程进行操作共享数据时,可以保证内存中的数据可见
相对于synchronized是一种较为轻量级的同步锁
注意:
volatile不具备互斥性
volatile不能保证变量的原子性
参考博客
https://blog.csdn.net/qq_39054532/article/details
https://www.cnblogs.com/paddix/p/5428507.html