目录
*简单谈谈volatile是什么意思
volatile是Java虚拟机提供的一种轻量级的同步机制
它能够保证可见性、有序性、但是不保证原子性。
*JMM
Java内存模型,本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.
*可见性
*详细解释
假设有多个线程从主内存中拷贝值到各自线程的工作内存,若有一个线程改了当前拷贝的值,并将修改好的值返回给了主内存,那就会导致其它线程操作的数据与主内存的数据产生偏差。
这个时候就必须要有一种机制,就是一旦有一个线程修改完工作内存的值,并返回给主内存之后,要及时通知其它线程,这样及时通知的这种情况,就是JMM内存模型里面的一个重要特性,可见性。
官方解释
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方被称为栈空间),
工作内存是每个线程的私有数据区域
而Java内存模型中规定所有变量都存储在主内存,主内存处于一片共享的内存区域,所有线程都可以访问,但线程不能直接操作主内存中的变量,对变量的操作(读取赋值等)必须在工作内存中进行,所以首先要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写回主内存
各个线程中的工作内存存储着主内存的变量拷贝副本,因此不同的线程无法访问对方的工作内存,线程间的通信(传值)必须要通过主内存来完成
*写一个可见性demo
public class VolatileDemo{
public static void main(String[] args){
MyData m = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName());
try{
TimeUnit.SECONDS.sleep(3);
}catch(InterruptedException e){
e.printStackTrace();
}
m.addNum();
},"AAA").start();
while(m.num == 0){
}
System.out.println("Mission Finish num " + m.num);
}
}
class MyData{
// 加不加volatile是两个结果
volatile int num = 0;
void addNum(){
num = 60;
}
}
原子性
*写一个不保证原子性的demo
public class VolatileDemo {
public static void main(String[] args){
MyData m = new MyData();
for(int i = 1; i <= 20; i++){
new Thread(() -> {
for(int j = 1; j <= 1000; j++){
m.addPlusPlus();
}
}, String.valueOf(i)).start();
}
while(Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "\t" + m.num);
}
}
class MyData{
int num = 0;
void addPlusPlus(){
num++;
}
}
*为什么会出现数值少于20000?volatile为什么不能保证原子性
假设主物理内存中有一个num的变量,初始值为0,现在有3个线程要来读取并操作这个主物理内存的num,
A、B、C三个线程通过副本拷贝将num分别拷贝到了自己所在线程的工作空间,这个时候A、B线程同时调用了一个使num++的方法,
A线程的执行完之后,要将结果1返回到主物理内存进行更新时,因为多线程之间的调度关系,A线程突然被挂起了,
此时B线程也进行了同样的操作,然后B线程成功将1写入到主内存了,这时会通知其它线程,
此时挂起的A线程被唤醒,它继续将之前工作空间中计算出的1写入到主内存,结果就是A线程的1将B线程的1覆盖掉,
导致最终出现了数据丢失。
*结论:
在底层的源码编译中,putfield这步写回去的时候,有很多值因为线程的调度可能被挂起了,刚好也没有收到最新值的通知,
有这么一个纳秒级别的时间差,一写就出现了写覆盖,所以最后就把人家的值覆盖掉了
*如何解决不保证原子性的问题
使用AtomicInteger
public class VolatileDemo {
public static void main(String[] args){
MyData m = new MyData();
for(int i = 1; i <= 20; i++){
new Thread(() -> {
for(int j = 1; j <= 1000; j++){
m.addPlusPlus();
m.addMyAtomic();
}
}, String.valueOf(i)).start();
}
while(Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "\tint\t\t" + m.num);
System.out.println(Thread.currentThread().getName() + "\tAtomicInteger\t" + m.ai);
}
}
class MyData{
int num = 0;
void addPlusPlus(){
num++;
}
AtomicInteger ai = new AtomicInteger();
void addMyAtomic(){
ai.getAndIncrement();
}
}
*为什么加了AtomicInteger就能解决不能保证原子性问题?
因为它里面有一个方法,getAndIncrement,它的意思就是带原子性的使一个值加1
这样其它线程就必须等待操作它的线程执行完之后,才可以对它进行操作,这样就能解决不保证原子性的问题
*AtomicInteger底层是什么
CAS
*有序性
计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一般分为以下三种
处理器在进行重新排序时必须要考虑指令之间的数据依赖性
*写一个有序性demo
public class ResortSeqDemo{
int a = 0;
boolean flag = false;
void method1() {
a = 1; //语句1
flag = true; //语句2
}
void method2() {
if (flag) {
a = a + 5; //语句3
System.out.println(a);
}
}
}
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用过的变量能否保持一致性是无法确定的,结果无法预测
以上图的代码为例,如果开启多个线程操作这个资源类,语句1和语句2的执行顺序就无法得到保障,从而导致的结果就会完全不同。
总结
*你在哪些地方用过volatile
单例模式DCL+volatile
public class SingletonDemo{
private static volatile SingletonDemo instance = null;//关键
private SingletonDemo(){
System.out.println(Thread.currentThread().getName() + "\tSingletonDemo()");
}
public static SingletonDemo getInstance(){
if(instance == null){
//Double Check Lock,双端检锁:在加锁前后都进行判断
synchronized(SingletonDemo.class){
if(instance == null){
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args){
for(int i = 0; i < 10; i++){
new Thread(() -> {
SingletonDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
*分析
DCL(双端检锁)不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排
原因在于某一个线程在执行第一次检测,读取到instance不为null时,instance的引用对象可能没有完成初始化
instance = new SingletonDemo();
可以分为以下步骤
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实例未必完成初始化,也会造成线程安全问题。