深入了解JMM(JAVA内存模型)

JMM研究什么

JMM研究的是在多线程下Java代码的执行顺序,以及共享变量的读写。在多线程下,对共享变量读写的场景下,代码的执行结果可能和自己的期望结果不一致。

为什么会产生这种现象呢?

首先我们要知道两个现象:

  1. 我们写的代码,未必是实际运行的代码;
  2. 代码的编写顺序,未必就是实际的执行顺序。

为什么会有上面的两个现象呢?

处于性能的考虑,软件层次,JVM会对我们编写的代码进行优化,优化为更高效的代码指令;硬件层次,CPU还有Processor 优化和缓存优化。从而,造成以下三个问题:

  • 可见性问题
  • 原子性问题
  • 有序性问题

JMM研究的重点其实就上面这三个问题。

什么是JMM

jmm(java memory model)规范,他规范了java虚拟机与计算机内存如何协调工作 ,他规定了一个线程如何及何时看到其他线程修改过的变量的值,以及在必须时,如何同步的访问共享变量。

JMM规范

竞态条件

多线程下,没有依赖关系的代码在执行共享变量的读写操作时,不能保证以代码的编写顺序执行代码,称之为发生了竞态条件。

例如:
Thread1

r.r1 = y;
r.r2 = x;

Thread2

x = 1;
y = 2;

结果:r1 == 1,r2 == 0。代码执行顺序可能如下:

y = 1;
r.r1 = y;
r.r2 = x;
x = 1;
Synchronization Order

同步动作:在多线程下,如果要想让每个线程中代码的执行顺序,按照代码编写顺序执行,必须要使用Synchronization Actions来保证。这些同步动作有:

  • lock,unlock-synchronized,ReentrantLock等,保证原子性;
  • volatile 方式读写变量,保证可见性,防止指令重排;
  • varhandler方式读写变量(JDK9以上支持)
happens before

若是变量的读写是发生线程切换,在这些边界的处理上如果有action1先于action2发生,那么代码的可以按照确定的顺序执行,称之为happens before。

具体规则如下:

  1. 线程启动和运行的边界

    T1(x = 1) —>T1(t2.start())
    —>T2(run())
    —> T2(x == 10)

  2. 线程结束和join边界

T1(x = 10) —> T1(terminated)
—> T2(t1.join())
—> T2(x == 10)

 static boolean stop = false; //停止标记
    
 public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true; 
        });
        System.out.println("start " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
        t.start();
        t.join(); // 线程t在结束前执行了写入操作,当线程t 在join到主线程时,发生了happens before
        foo();
    }

   private static void foo() {
        while (true) {
         
                boolean b = stop; // volatile 的读
                if (b) {
                    break;
                }    
        }
        System.out.println("end " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
    }
  1. 线程的打断和得知打断的边界

    T1(x = 10) —> T1(t2.interrupt())
    —> T2(this.isInterrupt())
    —> T2(x == 10)

  2. unlock与lock边界

public class TestInfinityLoop {
    static volatile boolean stop = false; //停止标记

    static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
          synchronized (lock) {
                stop = true; 
            } // 解锁
        });
        System.out.println("start " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
        t.start();
        foo();
    }

    private static void foo() {
        while (true) {
            synchronized (lock) { // 加锁
                boolean b = stop; // volatile 的读
                if (b) {
                    break;
                }
            }
        }
        System.out.println("end " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
    }
}
  1. volatile write与volatile read 边界

    注意:写要读的前面

public class TestInfinityLoop {
    static volatile boolean stop = false; //停止标记

   	public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true; // volatile 的写
        });
        System.out.println("start " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
        t.start();
        foo();
    }

    private static void foo() {
        while (true) {
              boolean b = stop; // volatile 的读
              if (b) {
                  break;
              }
        }
        System.out.println("end " + LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
    }
}
  1. 传递性
Causality(因果律)

如果代码有依赖关系,即使没有加同步动作,代码的执行顺序也是能够确定。

安全发布

当一个对象被创建出来,作为共享变量使用时,要考虑安全发布,并且避免出现this溢出(在构造方法中,将this赋值给一个共享变量)。需要用final或者volatile修饰他的属性。因为,构造方法可能不是原子的。
使用final修饰成员变量,可以不用考虑它的位置;使用volatile修饰,必须修饰最后写的变量才能保证安全发布。

内存屏障

  • LoadLoad
    发生在两次读取操作之间,防止后面的读取操作重排序到屏障前面去。

  • LoadStore
    发生读取和写入之间,防止后面的写入操作重排序到屏障之前

  • StoreStore
    发生在两次连续的写之间,防止的写入操作重排序到屏障后面

  • StoreLoad
    在线程切换时有效,让所有写入都同步到内存,同时读取的内容是内存中最新的数据。

LoadLoad + LoadStore = Acquire 即让同一线程内读操作之后的读写上不去,第一个 Load 能读到主存最新值
LoadStore + StoreStore = Release 即让同一线程内写操作之前的读写下不来,后一个 Store 能将改动都写入主

StoreLoad 最为特殊,还能用在线程切换时,对变量的写操作 + 读操作做同步,只要是对同一变量先写后读,那
么屏障就能生效

volatile

本质

在这里插入图片描述
事实上对 volatile 而言 Store-Load,与 LoadLoad 屏障最为有用,volatile的作用:

  • 单一变量的赋值原子性;
  • 保证线程内按屏障有序,线程切换时,按Happens before有序;
  • 线程切换时若发生了写 —》读,则变量可见,顺带影响普通变量的可见。

visibility(可见性)

@JCStressTest
    @Outcome(id = {"3, 3, 3", "0, 0, 0"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    @Outcome(id = {"0, 3, 3", "0, 0, 3"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING")
    @Outcome(id = "0, 3, 0", expect = Expect.FORBIDDEN, desc = "FORBIDDEN")
    @State
    public static class Case2 {
        static class Foo {
        // 加上volatile后,会阻止编译器对代码的优化,并加入StoreLoad屏障保证线程的中变量的写入,对于其他线程都是可见的。
            volatile int x = 0; 
        }

        Foo p = new Foo();
        Foo q = p;

        @Actor
        public void actor1(III_Result r) {
            r.r1 = p.x;
            r.r2 = q.x;
            r.r3 = p.x;
        }

        @Actor
        public void actor2() {
            p.x = 3;
        }
    }

partial ordering

volatile修饰了多个共享变量中的一个。

    @JCStressTest
    @Outcome(id = {"0, 0", "1, 1", "0, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    @Outcome(id = "1, 0", expect = Expect.FORBIDDEN, desc = "FORBIDDEN")
    @State
    public static class Case2 {
        int x;
        volatile int y;

        @Actor
        public void actor1() {
            x = 1;
            y = 1;
        }

        @Actor
        public void actor2(II_Result r) {
            r.r1 = y;
            r.r2 = x;
        }
    }

    @JCStressTest
    @Outcome(id = {"0, 0", "1, 1", "0, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    @Outcome(id = "1, 0", expect = Expect.FORBIDDEN, desc = "FORBIDDEN")
    @State
    public static class Case3 {
        volatile int x;
        int y;

        @Actor
        public void actor1() {
            x = 1;
            y = 1;
        }

        @Actor
        public void actor2(II_Result r) {
            r.r1 = y;
            r.r2 = x;
        }
    }

volatile修饰y和修饰x的效果有很大的差别,具体如下图所示:

在这里插入图片描述

total ordering

volatile修饰所有共享变量。

	/**
     * 案例1: 演示 total ordering 存在的问题
     */
    @JCStressTest
    @Outcome(id = {"1, 0", "0, 1", "1, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    @Outcome(id = "0, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING")
    @State
    public static class Case1 {
        int x;
        int y;

        @Actor
        public void a1(II_Result r) {
            y = 1;
            r.r2 = x;
        }

        @Actor
        public void a2(II_Result r) {
            x = 1;
            r.r1 = y;
        }
    }

使用volatile修饰其中一个共享变量,不能保证不会出现意外的情况发生。

	/**
     * 案例2: 演示单个 volatile 变量不能解决 total ordering 的情况
     */
    @JCStressTest
    @Outcome(id = {"1, 0", "0, 1", "1, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    @Outcome(id = "0, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING")
    @State
    public static class Case2 {
        int x;
        volatile int y;

        @Actor
        public void a1(II_Result r) {
            y = 1;
            r.r2 = x;
        }

        @Actor
        public void a2(II_Result r) {
            x = 1;
            r.r1 = y;
        }
    }

在所有共享变量上使用volatile修饰

    /**
     * 案例4: 演示两个 volatile 变量可以解决 total ordering 存在的问题
     */
    @JCStressTest
    @Outcome(id = {"1, 0", "0, 1", "1, 1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    @Outcome(id = "0, 0", expect = Expect.FORBIDDEN, desc = "FORBIDDEN")
    @State
    public static class Case4 {
        volatile int x;
        volatile int y;

        @Actor
        public void a1(II_Result r) {
            y = 1;
            r.r2 = x;
        }

        @Actor
        public void a2(II_Result r) {
            x = 1;
            r.r1 = y;
        }
    }

总结

volatile 写要用来收官,volatile 读要用来开篇.即volatile修饰的变量的写入操作要在一个线程的最后位置,volatile修饰的变量的读取操作要在一个线程的开始位置

VarHandler

安全发布

当创建了一个对象,并把它赋值给共享变量时,存在线程安全问题。

构造不安全

初始:

	/**
     * 案例1: 测试不安全的构造
     */
    @JCStressTest
    @Outcome(id = {"16", "-1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    @Outcome(expect = Expect.ACCEPTABLE_INTERESTING, desc = "INTERESTING")
    @State
    public static class Case1 {
        Holder f;

        int v = 1;

        @Actor
        public void a1() {
            f = new Holder(v);
        }

        @Actor
        void a2(I_Result r) {
            Holder o = this.f;
            if (o != null) {
                r.r1 = o.x8 + o.x7 + o.x6 + o.x5 + o.x4 + o.x3 + o.x2 + o.x1;
                r.r1 += o.y8 + o.y7 + o.y6 + o.y5 + o.y4 + o.y3 + o.y2 + o.y1;
            } else {
                r.r1 = -1;
            }
        }

        static class Holder {
            int x1, x2, x3, x4;
            int x5, x6, x7, x8;
            int y1, y2, y3, y4;
            int y5, y6, y7, y8;

            public Holder(int v) {
                x1 = v;
                x2 = v;
                x3 = v;
                x4 = v;
                x5 = v;
                x6 = v;
                x7 = v;
                x8 = v;
                y1 = v;
                y2 = v;
                y3 = v;
                y4 = v;
                y5 = v;
                y6 = v;
                y7 = v;
                y8 = v;
            }
        }
    }

测试结果:
在这里插入图片描述
原因:对象初始化的指令和赋值操作的指令发生重排序。

在这里插入图片描述

使用final修饰

使用final修饰类的任意一个成员变量

    /**
     * 案例2: 使用 final 改进
     */
    @JCStressTest
    @Outcome(id = {"16", "-1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    @Outcome(expect = Expect.FORBIDDEN, desc = "FORBIDDEN")
    @State
    public static class Case2 {
        Holder f;

        int v = 1;

        @Actor
        public void a1() {
            /**
             * 对象发布
             * 1.分配空间
             * 2.调用构造方法
             * 3,将引用赋值
             */
            f = new Holder(v);
        }

        @Actor
        void a2(I_Result r) {
            Holder o = this.f;
            if (o != null) {
                r.r1 = o.x8 + o.x7 + o.x6 + o.x5 + o.x4 + o.x3 + o.x2 + o.x1;
                r.r1 += o.y8 + o.y7 + o.y6 + o.y5 + o.y4 + o.y3 + o.y2 + o.y1;
            } else {
                r.r1 = -1;
            }
        }

        static class Holder {
            final int x1;
            int x2, x3, x4;
            int x5, x6, x7, x8;
            int y1, y2, y3, y4;
            int y5, y6, y7, y8;

            public Holder(int v) {
                x1 = v;
                x2 = v;
                x3 = v;
                x4 = v;
                x5 = v;
                x6 = v;
                x7 = v;
                x8 = v;
                y1 = v;
                y2 = v;
                y3 = v;
                y4 = v;
                y5 = v;
                y6 = v;
                y7 = v;
                y8 = v;
            }
        }
    }

测试结果:
在这里插入图片描述
原因:final修饰任意的一个变量时,会在构造方法结束前,添加StoreStore屏障。
在这里插入图片描述

使用volatile修饰


```java
    /**
     * 案例4: 使用 volatile 改进
     */
    @JCStressTest
    @Outcome(id = {"16", "-1"}, expect = Expect.ACCEPTABLE, desc = "ACCEPTABLE")
    @Outcome(expect = Expect.FORBIDDEN, desc = "FORBIDDEN")
    @State
    public static class Case4 {
        Holder f;

        int v = 1;

        @Actor
        public void a1() {
            f = new Holder(v);
        }

        @Actor
        void a2(I_Result r) {
            Holder o = this.f;
            if (o != null) {
                r.r1 = o.x8 + o.x7 + o.x6 + o.x5 + o.x4 + o.x3 + o.x2 + o.x1;
                r.r1 += o.y8 + o.y7 + o.y6 + o.y5 + o.y4 + o.y3 + o.y2 + o.y1;
            } else {
                r.r1 = -1;
            }
        }

        static class Holder {
            volatile int x1;
            int x2, x3, x4;
            int x5, x6, x7, x8;
            int y1, y2, y3, y4;
            int y5, y6, y7, y8;

            public Holder(int v) {
                x2 = v;
                x3 = v;
                x4 = v;
                x5 = v;
                x6 = v;
                x7 = v;
                x8 = v;
                y1 = v;
                y2 = v;
                y3 = v;
                y4 = v;
                y5 = v;
                y6 = v;
                y7 = v;
                y8 = v;
                x1 = v;
            }
        }
    }

结果:
在这里插入图片描述
原因:
在这里插入图片描述

注意:使用volatile必须修饰最后一个赋值操作的变量。

DCL安全单例

初始:

public class Singleton {
    private static Singleton INSTANCE = null;
   
	public static Singleton getInstance() {    
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

原因:
在这里插入图片描述

  1. synchronized可以防止{}中代码重排序到外面去,但是不能防止{}里面的代码在{}内重排序。
  2. 如果INSTANCE = t,重排序到t.x = 1和t.y = 1之间。
  3. 代码执行到INSTANCE = t后,cpu开始执行红色线程,这样红色线程判断INSTATCHE != null,因此,会返回一个没有完全构造的单例对象。

解决方案:
因为不能使用final修饰INSTANCE,所以只能使用volatile修饰INSTANTCE.

在这里插入图片描述
volatile修饰INSTANCE后,可以保证synchronized {}内的INSTACNE = t执行前,成员变量的赋值操作全部结束。这样,就可以实现安全的单例模式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值