Java线程内存模型-JMM 与 高并发程序优化

多核并发缓存架构图

多核并发缓存架构图

  • CPU运行程序:先将数据从硬盘读取到内存,再将主内存中的数据读取到CPU的高速缓存,CPU读写数据和CPU高速缓存打交道,之后CPU高速缓存再把数据同步给主内存。

JMM内存模型图(Java线程内存模型图)

JMM内存模型图

  • Java线程内存模型跟cpu缓存模型类似,是基于cpu缓存模型来建立的,Java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
  • 图中:线程A、B、C操作主内存中同一个共享变量时,不是直接和主内存打交道,而是把共享变量的值复制一份放在自己工作内存中操作。

JMM内存模型 代码举例:

public class VolatileVisibilityTest{

    private static boolean initFlag = false;

    public static void main(String[] args) throws InterruptedException {
    	//线程1
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("waiting data...");
                while (!initFlag){

                }
                System.out.println("======success");
            }
        }).start();
		//确保线程1已经开始运行了,再运行线程2。
        Thread.sleep(2000);
		//线程2
        new Thread(new Runnable() {
            @Override
            public void run() {
                prepareData();
            }
        }).start();
    }

    private static void prepareData() {
        System.out.println("prepareing data...");
        initFlag = true;
        System.out.println("prepare data end...");
    }
}
  • 上面代码问题:
    上面代码会进入死循环 “======success” 不输出。线程1和线程2的工作内存中都有共享变量initFlag的值,线程2更改值为"true"后,线程1感知不到,并依旧使用自己工作内存initFlag共享变量副本的值,因此无法结束while循环。
  • 解决方案:
    volatile:保证共享变量再多线程之间的可见性。当其中一个线程更改了共享变量,其它线程可以马上感知并更新自己共享变量副本共享变量的值。
    静态变量添加volatile修饰即可:private static volatile boolean initFlag = false;

JMM数据原子操作

原子操作都是硬件级别的实现。

  • read (读取):从主内存读取数据。
  • load (载入):将主内存读取到的数据写入工作内存。
  • user (使用):从工作读取数据来计算。
  • assign (赋值):将计算好的值重新赋值到工作内存中。
  • store (存储):将工作内存数据写入主内存。
  • write (写入):将store过去的变量值赋值给主内存中的变量。
  • lock (锁定):将主内存变量加锁,标识为线程独占状态。
  • unlock (解锁):将主内存变量解锁,解锁后其它线程可锁定该变量。

JMM内存模型 代码案例执行流程

  • 静态变量未加volatile修饰符:
  1. 线程1执行read操作,从主内存读取initFlag = false
  2. 线程1执行load操作,将initFlag = false写入线程1的工作内存中。
  3. 线程1执行use操作从工作内存中读取数据来计算(读取initFlag = false并取反),执行引擎执行while循环的字节码指令(死循环)。
  4. 线程sleep2秒。
  5. 线程2执行read操作,从主内存读取initFlag = false
  6. 线程2执行load操作,将initFlag = false写入线程2的工作内存中。
  7. 线程2执行user操作从工作内存中读取数据来计算(读取initFlag = false要改成true)。
  8. 线程2执行assign将计算好的值true重新赋值到工作内存中(把initFlag = false改成initFlag = true)。
  9. 线程2执行store操作,将新值initFlag = true放写入主内存,但还没有写入共享变量initFlag中。
  10. 线程2执行write操作,将放到主内存的新值initFlag = true,写入主内存里对应的共享变量中。
  11. 线程2以执行完毕,但程序一直再运行中。因为线程1还是用着自己工作内存中的initFlag = false,继续死循环。

JVM内存模型 代码案例执行流程 模型图

  • 静态变量未加volatile修饰符:
    JVM内存模型 代码案例执行流程 模型图

JMM缓存不一致问题

  • 总线加锁(性能太低)
    总线:cpu和内存传递数据用到的线,机箱内一排一排的线。
    cpu从主内存读取数据到告诉缓存,会在总线对这个数据加锁,这样其它cpu没法去读或写这个数据,直到这个cpu使用完数据释放锁后,其它cpu才能读取该数据。
    早期cpu有过这样的设计,但是效率低,多核的并行变成了串行,现已不用。
  • MESI缓存一致性协议
    多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其它cpu可以通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效。
    总线嗅探机制:cpu同步开启一个对总线的监听,一旦总线里面有一个共享变量数据到达了总线,其它cpu就会检测到该共享变量数据的变动,并把自己工作内存中副本变为失效状态,然后重新read值。类似消息中间件,消费者监听着消息队列,当消息队列有消息后,消费者马上监听到消息,然后做处理。
    volatile底层的实现方式。

Volatile可见性底层实现原理

  • Volatile缓存可见性实现原理
    底层实现主要通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并回写到主内存。
    IA-架构软件开发者手册对lock指令的解释:
    1. 会将当前处理器缓存行的数据立即写回到系统内存。
      lock前缀指令会触发cpu在store之前做一步lock原子操作,把主内存里的缓存行加一把锁。当storewrite执行完后,意味着主内存里共享变量的值已经更新完,才会释放锁。防止新值写入主内存但还未写入共享变量时,其它线程就更新副本却还是拿的旧值。
    2. 写回内存的操作会引起在其它CPU里缓存了该内存地址的数据无效(MESI协议)
  • Java程序汇编代码查看
    需要hsdis-amd64工具,放到jdk→jre→bin目录里面。
    Run/Debug Configurations里的Java虚拟机参数配置(VM options):
    -ser -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*VolatileVisibilityTest.prepareData
    并把JRE选为存放了hsdis-amd64工具的JRE目录。

Volatile 可见性、原子性、有序性。

  • 并发编程三大特性:可见性、原子性、有序性。
  • volatile保证可见性与有序性,但能不保证原子性,保证原子性需借助synchronized这样的锁机制。

volatile不能保证原子性案例代码

public class VolatileAtomicTest {

    public static volatile int num = 0;

    public static void increase() {
        num++; //num = num+1
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        for (Thread t : threads) {
            t.join();
        }

        System.out.println(num); //1000*10=10000

    }

}
  • 上面代码预测应该每次都=10000,执行完毕结果却是<=10000。

volatile不能保证原子性案例代码 模型图

volatile不能保证原子性案例代码 模型图

volatile不能保证原子性案例代码 执行流程

  1. 线程1和线程2执行read操作,从主内存读取num = 0
  2. 线程1和线程2执行load操作,将num = 0写入线程1的工作内存中。
  3. 线程1和线程2执行use操作从工作内存中读取出局来计算,执行引擎执行num++字节码指令。
  4. 但线程1线执行完assign操作并写入主内存的共享变量中。此时线程2的cpu总线嗅探器监听到共享变量更改,将自己工作内存中运算完num++的值变为失效状态,线程2就浪费了一次num++
  5. 这时线程1和线程2都执行了一次num++num的值为1,因线程2的作废。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值