volatile
volatile是Java虚拟机提供的轻量级的同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排
JMM
概念
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.
JMM关于同步规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成.
其简要访问过程如下图:
可见性
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中的。这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时 ,另外一个线程BBB又对内存中的一个共享变量X进行操作,但此时A线程工作内存中的共享比那里X对线程B来说并不不可见.这种工作内存与主内存同步延迟现象就造成了可见性问题.
保证可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值
不加volatile
package com.liang.volatiledemo;
import java.util.concurrent.TimeUnit;
public class TestVolatile {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
try {
//暂停3秒钟 等待主线程
TimeUnit.SECONDS.sleep(3);
//调用add方法
data.add();
System.out.println(Thread.currentThread().getName()+"\t 修改numer为"+data.number);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"myThread").start();
while (data.number!=99){
//如果data.number的值一直不为99 那么main线程将一直在这里循环
}
System.out.println("结束");
}
}
class Data{
int number = 0;
public void add (){
this.number = 99;
}
}
运行
线程进入死循环。
因为线程myThread修改了变量number的值,但是对主线程来说并不可见,就造成了主线程进入死循环
加volatile
package com.liang.volatiledemo;
import java.util.concurrent.TimeUnit;
public class TestVolatile {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
try {
//暂停3秒钟 等待主线程
TimeUnit.SECONDS.sleep(3);
//调用add方法
data.add();
System.out.println(Thread.currentThread().getName()+"\t 修改numer为"+data.number);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"myThread").start();
while (data.number!=99){
//如果data.number的值一直不为99 那么main线程将一直在这里循环
}
System.out.println("结束");
}
}
class Data{
// int number = 0;
volatile int number = 0;
public void add (){
this.number = 99;
}
}
运行
程序没有死循环,结束执行。
保证了MyThtead和主线程的可见性
不保证原子性
原子性:不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败
验证示例(变量添加volatile关键字,方法不添加synchronized)
package com.liang.volatiledemo;
import java.util.concurrent.TimeUnit;
public class TestVolatile {
public static void main(String[] args) {
// volatiledemo1(); //验证可见性
volatiledemo2(); //不保证原子性
}
//可见性demo
public static void volatiledemo1(){
Data data = new Data();
new Thread(()->{
try {
//暂停3秒钟 等待主线程
TimeUnit.SECONDS.sleep(3);
//调用add方法
data.add();
System.out.println(Thread.currentThread().getName()+"\t 修改numer为"+data.number);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"myThread").start();
while (data.number!=99){
//如果data.number的值一直不为99 那么main线程将一直在这里循环
}
System.out.println("结束");
}
//不保证原子性
public static void volatiledemo2(){
Data data = new Data();
// 启动20个线程
for (int i = 0; i < 20; i++) {
new Thread(()->{
//每个线程修改1000次
for (int j = 0; j < 1000; j++) {
data.addSelf();
}
},i+"").start();
}
//需要等待上面20个线程全部计算完成后,再用主线程获取最终结果值
// 主线程暂停5秒钟
// try {
// TimeUnit.SECONDS.sleep(5);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// 因为不知道上面的线程执行完需要多长时间, 可能小于5秒 所以使用
while (Thread.activeCount()>2){ //后台默认两个线程 1是main线程 2是GC垃圾回收线程
Thread.yield(); //等待
}
System.out.println(Thread.currentThread().getName()+"\t 最终结果为"+data.number);
}
}
class Data{
// int number = 0;
volatile int number = 0;
public void add (){
this.number = 99;
}
public void addSelf(){
number++;
}
}
运行
可以发现最终结果并不是20000.
验证示例(变量添加volatile关键字,方法添加synchronized)
最终结果为20000
number++在多线程下是非线程安全的,如何不加synchronized解决?
使用AtomicInteger
package com.liang.volatiledemo;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class TestVolatile {
public static void main(String[] args) {
// volatiledemo1(); //验证可见性
volatiledemo2(); //不保证原子性
}
//可见性demo
public static void volatiledemo1(){
Data data = new Data();
new Thread(()->{
try {
//暂停3秒钟 等待主线程
TimeUnit.SECONDS.sleep(3);
//调用add方法
data.add();
System.out.println(Thread.currentThread().getName()+"\t 修改numer为"+data.number);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"myThread").start();
while (data.number!=99){
//如果data.number的值一直不为99 那么main线程将一直在这里循环
}
System.out.println("结束");
}
//不保证原子性
public static void volatiledemo2(){
Data data = new Data();
// 启动20个线程
for (int i = 0; i < 20; i++) {
new Thread(()->{
//每个线程修改1000次
for (int j = 0; j < 1000; j++) {
data.addSelf();
data.atomicAdd();
}
},i+"").start();
}
//需要等待上面20个线程全部计算完成后,再用主线程获取最终结果值
// 主线程暂停5秒钟
// try {
// TimeUnit.SECONDS.sleep(5);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// 因为不知道上面的线程执行完需要多长时间, 可能小于5秒 所以使用
while (Thread.activeCount()>2){ //后台默认两个线程 1是main线程 2是GC垃圾回收线程
Thread.yield(); //等待
}
System.out.println(Thread.currentThread().getName()+"\t number++最终结果为"+data.number);
System.out.println(Thread.currentThread().getName()+"\t atomicInteger最终结果为"+data.atomicInteger);
}
}
class Data{
// int number = 0;
volatile int number = 0;
public void add (){
this.number = 99;
}
public void addSelf(){
number++;
}
AtomicInteger atomicInteger = new AtomicInteger(); //不写参数默认为0
public void atomicAdd(){
atomicInteger.getAndIncrement(); //i++
}
}
线程安全性获得保证
有序性
计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一把分为以下3中
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致
处理器在进行重新排序是必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测
重排1
public void mySort(){
int x=11;//语句1
int y=12;//语句2
x=x+5;//语句3
y=x*x;//语句4
}
//可能的顺序:
//1234
//2134
//1324
//问题:
//请问语句4 可以重排后变成第一条码?
//存在数据的依赖性 没办法排到第一个
重排2
int a ,b ,x,y=0;
线程1 | 线程2 |
---|---|
x=a | y=b |
b=1 | a=2 |
… | |
x=0 y=0 |
如果编译器对这段代码进行执行重排优化后,可能出现下列情况:
线程1 | 线程2 |
---|---|
b=1 | a=2 |
x=a | y=b |
… | |
x=2 y=1 |
这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程使用的变量能否保持一致是无法确定的.
案例:
package com.liang.volatiledemo;
public class ReSortSeqDemo {
int a = 0;
boolean flag = false;
public void method01() {
a = 1; // flag = true;
// ----线程切换----
flag = true; // a = 1;
}
public void method02() {
if (flag) {
a = a + 3;
System.out.println("a = " + a);
}
}
}
如果两个线程同时执行,method01 和 method02 如果线程 1 执行 method01 重排序了,然后切换的线程 2 执行 method02 就会出现不一样的结果。
禁止指令重排
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
先了解一个概念,内存屏障(Menory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化,如果在指令间插入一条Menory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Menory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。没存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
线程安全性保证
工作内存与主内存同步延迟现象导致可见性问题
- 可以使用 synchronzied 或 volatile 关键字解决,它们可以使用一个线程修改后的变量立即对其他线程可见
对于指令重排导致可见性问题和有序性问题
- 可以利用 volatile 关键字解决,因为 volatile 的另一个作用就是禁止指令重排序优化
单例模式DCL代码
单例模式多线程环境下可能存在的安全问题
package com.liang.volatiledemo;
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + " 构造方法...");
}
public static SingletonDemo getInstance() {
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
getInstance();
},i+"").start();
}
}
}
执行:
发现构造器里的内容会多次输出
双重锁单例
package com.liang.volatiledemo;
public class SingletonDemo1 {
private static volatile SingletonDemo1 instance=null;
private SingletonDemo1(){
System.out.println(Thread.currentThread().getName()+"\t 构造方法");
}
/**
* 双重检测机制
* @return
*/
public static SingletonDemo1 getInstance(){
if(instance==null){
synchronized (SingletonDemo1.class){
if(instance==null){
instance=new SingletonDemo1();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 1; i <=10; i++) {
new Thread(() ->{
SingletonDemo1.getInstance();
},String.valueOf(i)).start();
}
}
}
-
如果没有加 volatile 就不一定是线程安全的,原因是指令重排序的存在,加入 volatile 可以禁止指令重排。
-
原因是在于某一个线程执行到第一次检测,读取到的 instance 不为 null 时,instance 的引用对象可能还没有完成初始化。
instance = new Singleton()
可以分为以下三步完成
memory = allocate(); // 1.分配对象空间
instance(memory); // 2.初始化对象
instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance != null
步骤 2 和步骤 3 不存在依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种优化是允许的。
发生重排:
memory = allocate(); // 1.分配对象空间
instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance != null,但对象还没有初始化完成
instance(memory); // 2.初始化对象
所以不加 volatile 返回的实例不为空,但可能是未初始化的实例