1、 volatile
是什么
是java虚拟机提供的轻量级的同步机制
1.1 保证可见性
1.2 不保证原子性
1.3 禁止指令重排
2、谈谈JMM
是什么
JVM(java 内存模型)本身是一种抽象的概念并不真实存在,他描述的是一组规则或规范,通过这组规范定义了程序中各
个变量的访问方式
JMM 关于同步的规定
1、线程解锁前,必须把共享变量的值刷新回主内存
2、线程加锁前,必须读取主内存的最新值到自己的工作内存
3、加锁解锁是同一把锁
由于JVM运行程序的尸体是线程,而每个线程创建时JVM都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而java
内存模型中规定的所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作必须在工作内存中进行,
首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,
各个线程中工作内存中存储着主内存中的变量副本拷贝,因此不同线程间无法访问对方的工作内存,线程间的通信必须通过主内存来完成
1.可见性
1.1 可见性问题演示
public class TestVisual {
public static void main(String[] args) {
MYdata mYdata = new MYdata();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3); //如果没有这个 下面将会直接执行
} catch (InterruptedException e) {
e.printStackTrace();
}
mYdata.set();
},"AAA").start();
while (mYdata.number == 0 ){ //此时 main线程读取到的number的值为0 (将 number的值 拷贝到自己的工作内存中)
// 三秒后,AAA 线程将 number的值 写到主内存 ,但是 并没有通知 main线程 数据已经改变
// main 线程的值则一直是 0
}
System.out.println("main end ");
}
}
class MYdata{
public int number;
public void set(){
number = 60;
System.out.println(Thread.currentThread().getName() + "设置number的值:" + number);
}
}
1.2 可见性问题解决
class MYdata{
public volatile int number;
public void set(){
number = 60;
System.out.println(Thread.currentThread().getName() + "设置number的值:" + number);
}
}
2 原子性
是什么
不可分割,完整性,也即是某个线程正在做某个具体业务时,中间不可以被打断,需要完整性
要么同时成功,要么同时失败
1.1 原子性问题演示
public class TestAtomic {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 10_00; j++) {
myData.plus();
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(myData.number); //8780
}
}
1.2 原子问题产生的原因
因为number++ 在最终执行的时候并不是一个原子操作
我的理解:
A 线程拿到的数值为0 进行++ 操作之后为 1 ,正在准备写回主内存的时候,被线程 B 打断,
B 线程从主内存中拷贝值,因为A被没有写入,所以值为 0 ,B 拿到的值就是 0 再进行++,为1
此时,B将 1 写入 主内存,A 被唤醒,将 A 工作内存中的 1 写去主内存
1.3 解决方法
使用同步锁 效率低 不推荐,可以使用JUC下面的原子类 ,底层使用的CAS 比较并交换
public class TestAtomic {
public static void main(String[] args) {
MyData1 myData = new MyData1();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 10_00; j++) {
myData.plus();
myData.atomicplus();
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("finally myData.number :" + myData.number); //8780
System.out.println("finally myData.number :" +myData.atomicInteger.get()); //8780
}
}
class MyData1 {
volatile int number = 0;
AtomicInteger atomicInteger = new AtomicInteger();
public void plus() {
number++;
}
public void atomicplus() {
atomicInteger.getAndIncrement();
}
}
3 有序性
是什么
计算机执行程序时,为了提高性能,编译器和处理器常常对指令做重排,一般分为以下三种
源代码 =》 编译器优化的重排 =》 指令并行的重排 =》 内存系统的重排 =》最终执行的指令
单线程环境里面确保最终执行结果和代码顺序执行的结果一致
处理器在进行重排序是必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果是无法预测
3 禁止指令重排小结
volatile 实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
内存屏障(Memory Barrier): 又称内存栅栏,是一个CPU指令,它的作用有两个
一、保证特定操作的执行顺序
二、保证某些变量的内存可见性,
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉CPU
不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行
重新排序优化。内存屏障另外一个作用就是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能
读取到这些数据的祖新版本
对 volatile 变量 进行写操作时
会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新到主内存
对 volatile 变量 进行读操作时
会在读操作后加入一条load屏障指令,将工作内存中读取的共享变量
4 单例模式 volatile 分析
public class Singleton1 {
private Singleton1() {
}
private static Singleton1 instance;
public static Singleton1 getInstance() throws InterruptedException {
if (instance == null) { //如果有就直接返回
Thread.sleep(100);
synchronized (Singleton1.class) {
Thread.sleep(100);
if (instance == null) {
Thread.sleep(100);
instance = new Singleton1();
Thread.sleep(100);
}
}
}
return instance;
}
}
测试是没有发现问题,但是经过百度发现new 并不是原子操作,
可以参看一下博客
https://blog.csdn.net/qq_15038565/article/details/106981553
分析:
原因在于某一个线程执行到第一次检测,读取到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/初始化对象