目录
1.谈谈volatile的理解
1.1 基本概念
1.2 保证可见性
1.3 不保证原子性(JMM的理解)
1.4 禁止指令重排
2.在哪些地方用过volatile
2.1单例模式DCL代码
2.2 单例模式volatile代码
3.总结
1.谈谈volatile的理解
1.1 基本概念
volatile是java虚拟机提供的轻量级的同步机制,它拥有三大特性:①保证可见性 ②不保证原子性 ③禁止指令重排
接下来对这三个特性进行一一讲述
1.2 保证可见性
先引入JMM的概念
JMM 本身是一种抽象的概念并不是真实存在,它描述的是一组规定或则规范,通过这组规范定义了程序中的访问方式。
JMM 同步规定
线程解锁前,必须把共享变量的值刷新回主内存 线程加锁前
线程加锁前,必须读取主内存的最新值到自己的工作内存
加锁解锁是同一把锁
由于 JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而 Java 内存模型中规定所有变量的储存在主内存,主内存是共享内存区域,所有的线程都可以访问,但线程对变量的操作(读取赋值等)必须都工作内存进行看。
首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。具体看下面的图
程序运行过程保证可见性分为以下步骤:
1)创建线程1,2,3,分别分配工作内存
2)若线程1先将age的值改变(赋值操作)则线程2,3将"看见"age的值发生了改变 此时将不会进行下一步操作
这就是可见性的解释。用图表示如下
代码具体实现:
import java.util.concurrent.TimeUnit;
public class VolatileDemo01 {
public static void main(String[] args) {
//new一个线程
MyData myData=new MyData();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"t come in");
try {TimeUnit.SECONDS.sleep(5);}catch (InterruptedException e){e.printStackTrace();}
myData.addNumber();
System.out.println(Thread.currentThread().getName()+"t update"+myData.number);
},"线程A").start();
//第二个线程main 若number==0 则一直循环 不往下执行
while (myData.number==0){
}
//如果这行代码可以显示则证明了volatile的可见性 此时number=60,若不能执行则number=0,会在上面while一直循环
System.out.println(Thread.currentThread().getName()+"t update"+myData.number);
}
}
class MyData{
volatile int number=0;
public void addNumber(){
this.number=60;
}
}
验证如下:
1.3 不保证原子性
原子性是什么?
简单来说,不可分割,完整性,即某个线程正在做某个具体业务时,中间不可加塞或者分割。要么两个同时成功,要么两个同时失败。
验证代码如下:
public class VolatileDemo02 {
public static void main(String[] args) {
Test test=new Test();
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <=1000 ; j++) {
test.add();
}
},String.valueOf(i)).start();
}
//需要等待20个线程计算完
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " "+test.number);
}
}
class Test{
volatile int number=0;
public void add(){
number++;
}
}
验证结果:
上述代码的解释:正常情况下没有volatile时创建了20个线程,++操作执行了1000次,那么最后的值是20000,但是加了volatile之后却始终小于等于20000,说明volatile不能保证原子性
那么要怎么保证原子性呢?
1.使用synchronized关键字
2.使用AtomticInteger关键字
用synchronized可以解决,但是小题大做,所以接下来用AtomticInteger解决
代码如下:
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileDemo03 {
public static void main(String[] args) {
Test02 test=new Test02();
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <=1000 ; j++) {
test.add();
test.atomicAdd();
}
},String.valueOf(i)).start();
}
//需要等待20个线程计算完
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("加volatile "+test.number);
System.out.println("加AtomicInteger "+test.atomicInteger);
}
}
class Test02{
//使用volatile不保证原子性
volatile int number=0;
public void add(){
number++;
}
//使用AtomicInteger可保证原子性
AtomicInteger atomicInteger=new AtomicInteger();
public void atomicAdd(){
atomicInteger.getAndIncrement();
}
}
1.4 禁止指令重排
计算机在执行程序时,为了提高性能,编译器个处理器常常会对指令做重排,一般分为以下 3 种
- 单线程环境里面确保程序最终执行的结果和代码执行的结果一致
- 处理器在进行重排序时必须考虑指令之间的数据依赖性
- 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证用的变量能否一致性是无法确定的,结果无法预测
重排指令事例1:
public void sort(){
int x = 1; //语句1
int y = 2; //语句2
x=x+1; //语句3
y=x*x; //语句4
}
在多线程的情况下,由于编译器优化的指令重排,程序的执行顺序有多种情况:1->2->3->4,2->1->3->4,1->3->2->4等
重排指令事例2:
public class VolatileDemo04 {
int a = 0;
boolean flag = false;
public void method01() {
a = 1; // 语句1
// ----线程切换----
flag = true; // 语句2
}
public void method02() {
if (flag) { //语句3
a = a + 3;
System.out.println("a = " + a);
}
}
}
当正常执行的情况下,输出a=4,但是当多线程的情况下,由于编译器优化的指令重排,可能使得执行顺序发生改变比如先执行语句2,然后执行语句3,此时flag=true,而语句1未被执行,所以a=0,a=a+3的最终值为3
显然指令重排不是我们想要的结果,而volatile则可以禁止指令重排,加了volatile之后代码如下:
public class VolatileDemo04 {
volatile int a = 0;//加了volatile
volatile boolean flag = false;//加了volatile
public void method01() {
a = 1; // 语句1
// ----线程切换----
flag = true; // 语句2
}
public void method02() {
if (flag) { //语句3
a = a + 3;
System.out.println("a = " + a);
}
}
}
volatile 实现禁止指令重排序的优化,从而避免了多线程环境下程序出现乱序的现象
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个 CPU 指令,它有两个作用:
一是保证特定操作的执行顺序
二是保证某些变量的内存可见性(利用该特性实现 volatile 的内存可见性) 由于编译器和处理器都能执行指令重排序优化,如果在指令间插入一条 Memory Barrier 则会告诉编译器和 CPU,不管什么指令都不能个这条 Memory Barrier 指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后执行重排序优化。内存屏障另一个作用是强制刷出各种 CPU 缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。
2.在哪些地方用过volatile
2.1单例模式DCL代码
首先看看不使用任何机制前代码的运行情况:
public class SingletonDemo {
//单例模式
private static SingletonDemo instance =null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"t 我是构造方法SingletonDemo()");
}
public static SingletonDemo getInstance(){
if(instance==null){
instance=new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
//单线程
// System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
//多线程
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
结果如下:
本来使用单例模式我们想要的结果是只输出一次结果,但是根据上面可以看到结果不止一次
于是我们采用加synchronized的方法解决上述问题,可以加两个地方一是:
//第一种是直接把synchronized加到方法里面 此时把方法里的成员都锁住
public static synchronized SingletonDemo getInstance(){
if(instance==null){
instance=new SingletonDemo();
}
return instance;
}
第二种是:
public class SingletonDemo02 {
private static SingletonDemo02 instance =null;
private SingletonDemo02(){
System.out.println(Thread.currentThread().getName()+"t 我是构造方法SingletonDemo()");
}
public static SingletonDemo02 getInstance(){
//DCL Double Check Lock双端检锁机制
if(instance==null){
synchronized(SingletonDemo.class){
if(instance==null){
instance=new SingletonDemo02();
}
}
}
return instance;
}
public static void main(String[] args) {
//单线程
// System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
//多线程
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
SingletonDemo02.getInstance();
}, String.valueOf(i)).start();
}
}
}
public static SingletonDemo02 getInstance(){
//DCL Double Check Lock双端检锁机制
if(instance==null){
synchronized(SingletonDemo.class){
if(instance==null){
instance=new SingletonDemo02();
}
}
}
return instance;
}
这种称为DCL双端检锁机制,这种方法比第一种更加保险,安全,虽然这种方式安全,但是正确的概率为有可能是99%,并不能完全保证。因此继续改善加volatile!
2.2 单例模式volatile代码
代码如下:
public class SingletonDemo03 {
private static volatile SingletonDemo03 instance =null;//注意加了volatile!
private SingletonDemo03(){
System.out.println(Thread.currentThread().getName()+"t 我是构造方法SingletonDemo()");
}
public static SingletonDemo03 getInstance(){
//DCL Double Check Lock双端检锁机制
if(instance==null){
synchronized(SingletonDemo.class){
if(instance==null){
instance=new SingletonDemo03();
}
}
}
return instance;
}
public static void main(String[] args) {
//单线程
// System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
//多线程
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
SingletonDemo03.getInstance();
}, String.valueOf(i)).start();
}
}
}
小结:DCL(双端检锁)机制不一定线程安全,原因是指令重排序的存在,某一个线程执行到第一次检测,读取到的 instance 不为 null 时,instance 的引用对象可能还没有完成初始化。加入 volatile 可以禁止指令重排。instance = new Singleton() 可以分为以下三步完成。
memory = allocate(); // 1.分配对象空间
instance(memory); // 2.初始化对象
instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance != null
步骤 2 和步骤 3 不存在依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种优化是允许的。
若在多线程情况下执行顺序可能编程1->3->2,也就是在没有初始化对象时就指向了分配的内存地址。这样会出错!
3.总结volatile三大特性:①保证可见性 ②不保证原子性 ③禁止指令重排
volatile可用于:单例模式,使用synchronized的DCL双重检锁机制不一定能保证安全,因为多线程下有指令重排,使用volatile后可解决这个问题。



