①. 什么是volatile
1>.
什么是volatile?
-
①. volatile是Java虚拟机提供的 轻量级的同步机制(乞丐版的synchronized)
-
②.
特征:
- 保证可见性
- 不保证原子性
- 禁止指令重排
②. JMM的理解
2>.
JMM的理解(java内存模型)
-
①. JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念
并不真实存在
,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式. -
②. JMM关于同步规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
- ③. 原理的理解:
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存
,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:
③. volatile保证可见性详解
3>.
volatile保证可见性详解
-
①. 原理的理解:
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存
,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:
-
②. 代码演示
/*
验证volatile的可见性:
1.加入int number=0; number变量之前没有添加volatile关键字修饰,没有可见性
2.添加了volatile,可以解决可见性问题
* */
class Resource{
//volatile int number=0;
volatile int number=0;
public void addNumber(){
this.number=60;
}
}
public class Volatile_demo1 {
public static void main(String[] args) {
Resource resource=new Resource();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t coming ");
try {TimeUnit.SECONDS.sleep(4);}catch (InterruptedException e){e.printStackTrace();}
resource.addNumber();
System.out.println(Thread.currentThread().getName()+"\t update "+resource.number);
},"线程A").start();
//如果主线程访问resource.number==0,那么就一直进行循环
while(resource.number==0){
}
//如果执行到了这里,证明main现在通过resource.number的值为60
System.out.println(Thread.currentThread().getName()+"\t"+resource.number);
}
}
- ③. 对以上代码的详解
④. Volatile不保证原子性
4>.
Volatile不保证原子性
- ①. 代码演示
public class Volatile_demo3 {
public static void main(String[] args) {
/* System.out.println(Thread.activeCount());*/
AutoResource autoResource=new AutoResource();
//20个线程每个线程循环100次
for (int i = 1; i <=20; i++) {
new Thread(()->{
for (int j = 1; j <=100; j++) {
autoResource.numberPlusPlus();
autoResource.addAtomicInteger();
}
},String.valueOf(i)).start();
}
//需要等待上面20个线程都全部计算完后,再用main线程取得的最终的结果值是多少
//默认有两个线程,一个main线程,二是后台gc线程
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t int type"+autoResource.number);
System.out.println(Thread.currentThread().getName()+"\t AutoInteger type"+autoResource.atomicInteger.get());
}
}
class AutoResource{
volatile int number=0;
public void numberPlusPlus(){
number++;
}
//使用AutoInteger保证原子性
AtomicInteger atomicInteger=new AtomicInteger();
public void addAtomicInteger(){
atomicInteger.getAndIncrement();
}
}
- ②. 解释不保证证原子性
- ③. 关于n++源码解析
- ④. 解决方案:
1.使用synchronized
2.使用AtomicInteger(推荐)
原理部分在CAS部分有解释
AtomicInteger atomicInteger=new AtomicInteger();
public void addAtomicInteger(){
atomicInteger.getAndIncrement();
}
⑤. 禁止指令重排(有序性)
5>.
禁止指令重排(有序性)
- ①. 计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一把分为以下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
-
④. 加上volatile关键字的时候会禁止指令重排,编程会按照指定的顺序执行
⑥. 你在哪些地方用到过volatile?
6>.
你在哪些地方用到过volatile?
- ①. 单例模式DCL代码
public class SingletonDemo {
private static volatile SingletonDemo instance=null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"\t 构造方法");
}
/**
* 双重检测机制
* @return
*/
public static SingletonDemo getInstance(){
if(instance==null){
synchronized (SingletonDemo.class){
if(instance==null){
instance=new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 1; i <=10; i++) {
new Thread(() ->{
SingletonDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
- ②. 出现线程不安全原因的分析如下:
原因:
DCL(双端检锁) 机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化
.
instance=new SingletonDem(); 可以分为以下步骤(伪代码)
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.初始化对象
但是指令重排只会保证串行语义的执行一致性(单线程) 并不会关心多线程间的语义一致性
所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也就造成了线程安全问题