JAVA内存模型(JMM)

JMM 即 Java Memory Model,简单地说,JMM定义了一套在多线程读写共享数据(成员变量、数组)时,对数据的可见性、有序性、和原子性的规则和保障。它定义了主存(共享内存)、工作内存(线程私有)抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。

一、JMM——原子性-(synchronized)

语法

synchronized(对象){
要作为原子操作代码
}

用synchronized解决并发问题

public class Demo {

   static int i=0;
   static Object obj=new Object();
   
    public static void main(String[] args) throws InterruptedException {

      Thread t1=new Thread(()->{
          for (int i= 0; i<50000 ; i++) {
              synchronized (obj) {
                  i++;
              }
          }
      });
      Thread t2=new Thread(()->{
          for (int j = 0; j <50000 ; j++) {
              synchronized (obj) {
                  i--;
              }
          }
      });
      t1.start();
      t2.start();
      // join: 让主线程等待,一直等到其他线程不再活动为止 
      t1.join();
      t2.join();
        System.out.println(i);
    }
}

建议用synchronized对对象加锁的力度稍微大些(上述写法需对对象锁进行50000操作,较耗时),可将代码做如下调整:
在这里插入图片描述
如何理解呢:可以将obj想象成一个房间, 线程t1, t2想象成两个人。

当线程t1执行到synchronized(obj)时就好比t1进入了这个房间,并反手锁住了门,在门内执行count++代码。

这时候如果t2也运行到了synchronized(obj)时,它发现i ]被锁住了,只能在i门外等待。

当tl执行完synchronized{}块内的代码,这时候才会解开门上的锁,从obj房间出来。t2 线程这时才可以进入obj房间,反锁住门,执行它的count–代码。

上例中tl和t2线程必须用synchronized锁住同-个obj对象,如果1锁住的是ml对象,t2 锁住的是m2对象,就好比两个人分别进入了两个不同的房间,没法起到同步的效果

实际开发中,具体业务具体分析,如果业务很短多应使锁的粒度应该尽可能的小,这样便可缩短其他线程等待时间,以敬可能的提高并发效率

JMM——原子性-问题

问题提出:两个线程对初始值为0的静态变量,一个做自增,一个做自减,各做5000次,结果是0吗?
在这里插入图片描述

以上的结果可能是正数、负数、零。【Java中对静态变量的自增,自减并不是原子操作;在多线程环境下这些操作可能会被CPU进行交错执行】
例如相对于i++而言(i为静态变量)====>静态变量的i++需要将静态变量和常数都放在操作数栈用iadd来完成自增,并非在局部变量表中直接执行 ,实际产生如下的JVM指令:

getsatic     i      // 获取静态常量i的值
iconst        i      // 准备常量1
iadd                 // 加法【局部变量自增调用iinc】
putstatic    i     // 将修改后的值存入静态变量i

对应i–也是类似:

getsatic     i      // 获取静态常量i的值
iconst        i      // 准备常量1
isub                 // 加法
putstatic    i     // 将修改后的值存入静态变量i

而Java的内存模型分为如下两部分(主内存工作内存),完成静态变量的自增,自减需要在主存和线程内存中进行数据交换:
在这里插入图片描述
静态变量(共享的变量)放在主存中;线程则是在工作内存中

若为单线程以上8行代码是顺序执行(不会交错)没有问题:
在这里插入图片描述
但多线程下这8行代码可能交错运行
操作系统的线程模型都是一种抢先多任务模型,线程会轮流拿到CPU的使用权;CPU会以时间片为单位,在时间片1把使用权交给线程1运行,在时间片2再把线程分给线程2运行===>多个线程轮流使用CPU

出现负数的情况:【第一个线程执行getstatic获取到静态变量的初始值(i=0),恰巧在此时刻时间排片被用完,CPU将其剔出,于是CPU开始执行线程2的代码…】
在这里插入图片描述

出现正数的情况:
在这里插入图片描述

二、JMM——可见性-问题

2.1 退不出的循环

观察现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止(程序始终不能停止下来)
1在这里插入图片描述
分析原因如下:
Java内存模型将整个Java的内存分为主存工作内存

主存:所有共享信息存储的位置
工作内存:每个线程私有信息存储的位置

初始状态,t线程刚开始从主内存读取了run的值到工作内存
在这里插入图片描述

因为t线程要频繁从内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中(做进一步优化),减少对主存中run的访问,提高效率
在这里插入图片描述

1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
在这里插入图片描述

2.2 可见性——解决

volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中的高速缓存中查找变量的值,必须到主存中获取它的值,线程操做volatile变量都是直接操作主存===>volatile修饰的变量每次都到主存中读取

2.3 可见性

上述例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile 变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程, 多个读线程的情况:

上例从字节码理解是这样的:
在这里插入图片描述

加上volatile关键字后,程序运行1s后便可结束
在这里插入图片描述

比较之前线程安全时所举的例子:两个线程一个i++一个i–(不可保证原子性)
在这里插入图片描述

注意:synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized是属于重量级操作,性能相对更低

如果在前面示例的死循环中加入System. out.printla()会发现即使不加volatile修饰符,线程t也能正确看到对run变量的修改了,原因是什么?
在这里插入图片描述

在这里插入图片描述
查看println()方法源码后发现具有synchronized关键字,要对当前打印输出流做同步

三、JMM——有序性-问题

3.1 诡异的结果

  int num=0;
  boolean ready=false;

    // 线程1执行此方法
    public void actor1(I_Result r){
        if(ready){
            r.r1=num+num;
        }else {
            r.r1=1;
        }
}

    // 线程2执行此方法
    public void actor2(I_Result r){
        num=2;
        ready=true;
    }

I Result是一个对象,有一个属性r1用来保存结果,问,可能的结果有几种?

情况1: 线程1先执行,这时ready= false,所以进入else 分支结果为1
情况2: 线程2先执行num=2,但没来得及执行ready= true,线程1执行,还是进入else分支,结果为1
情况3: 线程2执行到ready=true,线程1执行,这回进入if分支,结果为4 (因为num已经执行过了)

除以上情况,结果还有可能为0: 线程2执行ready=true(num=2的赋值有可能还没执行),切换到线程1,进入if分支,相加为0,再切回线程2执行num=2

这种现象在Java内存模型中称为指令重排,是JIT编译器在运行时的一些优化,这个现象需要通过大量测试才能发现:可借助java并发压测工具jcstresshttps://wiki.openjdk.java.net/display/CodeTools/jcstress

3.2 解决方法

volatile修饰的变量,可以禁用指令重排

3.3 有序性理解

同一个线程内,JVM会在不影响正确性的前提下,可以调整语句的执行顺序,思考以下代码

static   int  i
static   int  j

// 在某个线程内执行如下赋值操作
i = ...;    // 较为耗时的操作(假如i的赋值可能需要做一些计算)
j = ...;

假如i的赋值可能需要做一些计算,j的可能马上会运算完毕;这种情况下JVM会对指令进行调整。因此以上操作无论是先执行i还是先执行j,对最终的结果不会产生影响。所以,上面代码真正执行时,即可以是

i = ...;    // 较为耗时的操作
j = ...;

也可以是

j = ...;
i = ...;    // 较为耗时的操作

这种特性称之为【指令重排】,多线程下指令重排会影响正确性,例如著名的double-checked locking模式实现单例

实现要点:单例类且为懒惰初始化===>单例是否创建,若没有创建则new单例对象;若已创建,则直接拿到上次已创建好的单例对象(为实现懒惰初始化也应考虑线程安全问题)

public final class Singleton {

    private Singleton() {
    }

    private static Singleton INSTANCE = null;

    public static Singleton getINSTANCE() {
        // 实例没创建,才会进入内部的synchronized代码块
        if (INSTANCE == null){
            synchronized (Singleton.class){
                //也许有其他线程已经创建实例,所以再判断一次
                if (INSTANCE == null){
                    INSTANCE=new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

以上的实现特点:
● 懒惰实例化
● 首次使用getINSTANCE()才使用synchronized加锁,后续使用无需加锁

但在多线程下,上面的代码存在问题(发生指令重排), INSTANCE=new Singleton();对应的字节码为:
在这里插入图片描述
其中4、7两个步骤时不固定的,也许jvm会优化为:先将引用地址赋值给INSTANCE变量后,再执行构造方法,如果两个线程t1、t2按如下时间顺序执行:
在这里插入图片描述
这时t1还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么t2拿到的将是一个未初始化完毕的单例

对INSTANCE使用volatile修饰即可,可以禁用指令重排,但要注意JDK5以上版本的volatile才会真正有效

3.4 happens-before

happens-before规定了哪些写操作对其他线程的读操作可见,他是可见性与有序性的一套规则总结:

线程解锁m之前对变量的锁,对于接下来对m加锁的其他线程对该变量的读可见

static int i=0;
static Object obj=new Object();

new Thread(()->{
              synchronized (m) {
                 x = 10;
              }
},"t1").start();

new Thread(()->{
              synchronized (m) {
                System.out.println(x);
              }
},"t2").start();

线程对volatile变量的写,对接下来其他线程对该变量的读可见

volatile  static int  x;

new Thread(()->{
      x = 10; 
},"t1").start();

new Thread(()->{
      System.out.println(x);
},"t2").start();

线程start前对变量的写,对该线程开始后对该变量的读可见

static int  x;
x = 10;

new Thread(()->{
      System.out.println(x);
},"t2").start();

线程结束前对变量的写,对其他线程得知它结束后的读可见(比如其他线程调用t1.isAlive()或t1.join()等待它结束)

static int  x;

 Thread t1=new Thread(()->{
        x = 10;
 },"t1");
 t1.start();

t1.join()  // 主线程调用t1.join()等待t1结束
System.out.println(x);

线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被打断后对变量的读可见(通过t2.interrupt或t2.isInterrupted)

public final class JMM {

    static int x;

    public static void main(String[] args) throws InterruptedException {

        Thread t2 = new Thread(() -> {
            while (true) {
                // 与主线程类似,在被打断后继续循环,并不会影响线程的继续运行(设置一个线程的打断标记)
                // 被打断后下次再进入循环条件成立
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(x);
                    break;
                }
            }
        }, "t2");
        t2.start();

        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // x的写操作是在打断前写的,因此得知它打断后再读取x的值必定可以拿到x最新的结果
            x = 10;
            // 打断t2线程(1s后)
            t2.interrupt();
        }, "t1").start();

        // 主线程不断循环,观察t2是否被打断
        while (!t2.isInterrupted()) {
            // 若未打断则一直循环       
            Thread.yield();
        }
        System.out.println(x);
    }
}

● 对变量默认值(0,false,null)的写,对其他线程对该变量的读可见

● 具有传递性,如果x hb->y并且y hb->z那么有x hb->z
变量都是指成员变量或静态成员变量

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

new一个对象_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值