在看java并发的书的时候,看到了关于java的单例模式使用了volatile关键字,但是对volatile关键字的真正含义一直很模糊,本文就尝试浅析总结一下volatile的作用和具体用法。
单例模式中的volatile关键字
1 | public class TestInstance { |
在并发情况下,如果没有volatile关键字,在第5行会出现问题
instance = new TestInstance();//5 可以分解以下三部
1,申请内存
2,通过构造方法初始化对象
3,将对象指向申请的内存
但当A线程进行第五部初始化的时候,可能是因为构造函数里面的操作太多,所以A线程还没有初始化完毕,但已经被赋值了,也就是以上的顺序由1-2-3变成了1-3-2。此时B线程进来判断TestInstance不为null,错以为已经实例化完毕返回了一个未初始化的对象(因为构造函数未完全,初始设置值未完全的对象),就出问题了。如果声明称volatile,2和3步骤不会被排序
volatile的作用与特点
通过上诉单例模式中volatile的使用,那么volatile关键字具体的作用到底是什么呢?
首先看一张图
由上图,需要知道的几点
java内存模型规定了所有的变量都存储在主内存中
Java内存模型分为主内存,和工作内存
主内存是所有的线程所共享的,工作内存是每个线程自己有一个,不是共享的。
线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝
线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,而不能直接读写主内存中的变量
不同线程之间也无法直接访问对方工作内存中的变量
基于上诉的主内存和工作内存的描述,那么
volatile的作用:
volatile赋予了变量可见——禁止编译器对成员变量进行优化,它修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。
从网上看见了一篇博客可以很好的说明,我将相关部分截了个图如下:
多线程的三个特性
- 原子性(Atomicity)
1 | 原子性是指一个原子操作在 中不可以暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。原子操作保证了原子性问题。 |
需要注意的是:i++不是原子操作:
x++(包含三个原子操作)a.将变量x 值取出放在寄存器中 b.将将寄存器中的值+1 c.将寄存器中的值赋值给x
1 | synchronized块之间的操作也具备原子性 |
- 可见性(Visibility)
1 | 可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每使用前立即从内存刷新。因为我们可以说volatile保证了线程操作时变量的可见性,而普通变量则不能保证这一点。 |
- 有序性(Ordering)
1 | Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。(比如上述单例模式中对象初始化的顺序对另外一个线程来讲可能会错序) |
volatile和synchronized的区别和联系
用法上:
- synchronized往往作用于方法或代码块
- volatile往往修饰变量。
实现机制:
- synchronized是利用锁实现互斥访问,多线程访问会阻塞
- Volatile没有利用锁,而是利用内存共享,强制线程每次从主内存中讲到变量,而不是从线程的私有内存中读取变量,多线程访问不回阻塞
区别:
- syncchronized不仅保证了可见性,还保证了原子性,因为只有获得锁对象的线程才能进入同步代码块,从而保证同步代码块的所有语句全部都能执行。多个线程争抢syncchronized锁对象的时候,会出现阻塞
- volatile只保证了可见性,不能保证原子性,不可保证同步,多个线程同时访问不会发生阻塞
-
volatile关键字的正确使用(一句话概括volatile)
一个线程修改某个变量后,其他线程需要知道这个变量修改后的最新的值,此时就需要使用volatile,保证多线程访问的是主内存的值,而不是访问各自线程的工作内存的副本值(其实仍然访问的是副本值,只不过副本值有修改后立即刷新到主内存中)也就是说保证了线程的可见性,但是不能像syncchronized保证原子性,volatile没有利用锁的机制,多线程访问时不会发生阻塞,也就是说volatile修饰的变量可以保证在一个线程中修改的时候,其他线程可以继续访问,一旦该变量修改完毕,其他线程再获取的一定是修改后的最新的值
synchronized同步的死锁是怎么产生的
多线程死锁案例:
什么是死锁:
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放.由于线程被无限期地阻塞,因此程序不能正常运行