JMM
JMM(Java内存模型,Java Memory Model)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过规范定制了程序中的各个变量的访问方式。
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存。
- 线程加锁前,必须读取主内存的最新值到自己的工作内存。
- 加锁解锁必须是同一个锁。
特点:
- 原子性:即一个操作或多个操作在执行的过程中,要成功都成功,要失败都失败。
- 可见性:多个线程访问同一个变量时,当一个线程修改该变量时,其他线程可见。
- 有序性:保证程序运行的顺序是代码的顺序。 在java 内存模型中,为了效率,是允许编译器和处理器对指令进行重排序的,对单线程运行不会影响,但是会影响多线程运行结果。
happens-before
了解有序性后需要了解一下happens-before原则:
定义:
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果对第二个操作执行结果可见,而且第一个操作顺序在第二个之前
- 如果两个操作存在happens-before关系,并不意味着一定按照happens-before顺序执行。如果重排序后的运行的值和happens-before运行结果一样,那么这种重排序并不违法
规则(来源 深入理解 Java 虚拟机)
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作,happens-before 于书写在后面的操作。
- 锁定规则:一个 unLock 操作,happens-before 于后面对同一个锁的 lock 操作。
- volatile 变量规则:对一个变量的写操作,happens-before 于后面对这个变量的读操作。
- 传递规则:如果操作 A happens-before 操作 B,而操作 B happens-before 操作C,则可以得出,操作 A happens-before 操作C
- 线程启动规则:Thread 对象的 start 方法,happens-before 此线程的每个一个动作。
- 线程中断规则:对线程 interrupt 方法的调用,happens-before 被中断线程的代码检测到中断事件的发生。
- 线程终结规则:线程中所有的操作,都 happens-before 线程的终止检测,我们可以通过Thread.join() 方法结束、Thread.isAlive() 的返回值手段,检测到线程已经终止执行。
- 对象终结规则:一个对象的初始化完成,happens-before 它的 finalize() 方法的开始
JMM内存模型:
数据一致性的问题(可见性问题)
从上图可以看到,计算机在执行的过程中,由于每条指令都是在CPU中运行,这样就会涉及到去主存中读取数据,但是主存的运行速度没有CPU运行速度快,所以有了CPU高速缓存,CPU高速缓存属于某个CPU独有,只与运行在该CPU的线程有关。这种情形解决了效率的问题,但是也带来了新的问题,即数据一致性,当CPU第一次从主存中获取后,会将信息放入到CPU高速缓存中,这是有其他线程修改了主存的信息,这是就会引起CPU缓存的信息和主存的信息不一致。
volatile
volatile 可以理解为轻量级的synchronized。在多线程的开发过程中,保证了内存可见性及禁止重排序。
特点
- 保证可见性
- 不保证原子性
- 禁止指令重排序
保证可见性
class Demo {
// private volatile int number = 0; //1
private int number = 0; //2
public void add() {
this.number = 100;
}
public int getNumber() {
return number;
}
public Demo setNumber(int number) {
this.number = number;
return this;
}
}
/**
* 两种运行结果
* 当执行1时候,线程1修改完后,立刻输出 内存可见
* 当执行2时候,线程1修改完后,程序死循环
*/
public class test {
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(() -> {
System.err.println(Thread.currentThread().getName() + "开始 执行");
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
demo.add();
System.err.println(Thread.currentThread().getName() + "已经执行完,当前number = " + demo.getNumber());
}, "线程1").start();
while (demo.getNumber() == 0) {
}
System.err.println("内存可见");
}
}
不保证原子性
volatile 不能保证复合操作的原子性。如下:
//创建初始值a = 0
private static volatile Integer a = 0;
//a ++;
public static void add() {
a++;
}
public static void main(String[] args) throws InterruptedException {
// 模拟一共一百个线程同时对a进行a++操作,如果是原子的操作,则最终结果为100;
CountDownLatch countDownLatch = new CountDownLatch(100);
//创建一个线程池(快速创建,但是在开发过程中不要这么写,可能会内存泄露,后面会单独讲解)
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
executor.execute(() -> {
add();
countDownLatch.countDown();
});
}
countDownLatch.await();
//尝试执行了5次
System.err.println(a); //结果:100、99、92、93、100
// 关闭线程池
executor.shutdown();
}
为什么volatile无法保证复合操作的原子性?
public class Demo2 {
private volatile int a = 0;
public void add() {
a++;
}
}
public class com.hhb.concurrency.atguigu.thread.Demo2 {
public com.hhb.concurrency.atguigu.thread.Demo2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field b:I
9: aload_0
10: iconst_0
11: putfield #3 // Field a:I
14: return
public void add(); //对应上面add()方法
Code:
0: aload_0 //从局部变量0中装载引用类型值
1: dup //复制栈顶部一个字长内容
2: getfield #3 //1、先获取a的值
5: iconst_1
6: iadd // 2、真正的对a++
7: putfield #3 // 3、 最后在赋值给a
10: return
}
通过上面的代码可以看到,a++的操作在java中看起来是一个操作,但是在执行的时候,被分为了下面三个操作;那么问题就出现在下面那三个步骤中,当A线程获取到a值,并对a进行++操作完后,正要执行(7)的时候,B线程开始获取a的值,此时A线程中a的值还没有刷新回主内存,所以B在获取到的a的值还是0,然后继续执行a+1的操作,并刷新a在主内存中的值,a=1,然后A线程在执行,刷新主内存,a=1,此时两个线程分别都进行的a++,目标应该是2,但是实际结果是1.
2: getfield #3 //1、先获取a的值
5: iconst_1
6: iadd // 2、真正的对a++
7: putfield #3 // 3、 最后在赋值给a
禁止重排序
- 编译器重排序:编译器在不影响单线程运行结果的前提之前,可以重新安排语句的执行顺序
- 处理器重排序:不存在数据依赖性,处理器可以改变语句对应的机器码的执行顺序。
volatile如何做到的禁止重排序?
内存屏障 ( Memory Barrier)
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用就是强制刷新出各种CPU的缓存数据,因此在CPU上的线程都能读取到这些数据的最新版本。
策略:
- 在每个volatile写之前,插入一个StoreStore屏障
- 在每个volatile写之后,插入一个StoreLoad屏障
- 在每个volatile读之前,插入一个LoadLoad屏障
- 在每个volatile读之后,插入一个LoadStore屏障
原因:
- StoreStore屏障:保证在volatile写之前,其前面的所有的普通写操作,都已经刷新到主存中
- StoreLoad屏障:避免volatile写和后面发发生的volatile 读/写操作重排序
- LoadLoad屏障:禁止处理器把上面的volatile读与下面的普通读重排序
- LoadStore屏障:禁止处理器把上面的volatile读与下面普通写重排序
禁止重排序最著名的例子:双重检查的单例模式
/**
* 创建对象的过程:
* 1、 memory = allocate() 分配对象内存空间
* 2、ctorInstance() 初始化对象
* 3、instance = memory 设置instance指向刚才的分配的内存
* <p>
* 不安全的原因:
* cpu和jvm优化,发生了指令重排序
* <p>
* 上面的过程变成了
* 1、 memory = allocate() 分配对象内存空间
* 2、instance = memory 设置instance指向刚才的分配的内存
* 3、ctorInstance() 初始化对象
* <p>
* <p>
* 假设现在有两个线程 A、B
* B线程执行到了4,但是执行到上面创建对象的第二步,还没有初始化时
* A线程指向到了1步骤,就会直接返回
*/
//private volatile static SingletonExample5 instance;
private static SingletonExample5 instance;
private SingletonExample5() {
}
public static SingletonExample5 getInstance() {
if (instance == null) { // 1
synchronized (SingletonExample5.class) { // 2
if (instance == null) { // 3
instance = new SingletonExample5(); // 4
}
}
}
return instance; //5
}