JMM内存模型--重排序、可见性、原子性

为什么需要JMM(Java Memory Model)?

  • C语言不存在内存模型的概念,很多行为依赖于处理器,不同处理器的操作会导致运行结果不一样,所以不能保证并发安全
  • JMM是一组规范,也是工具类和关键字的原理
  • JMM最重要的三点内容:重排序、可见性、原子性

一、重排序

1、重排序例子

  • 下面是一个演示重排序的例子,一个线程执行a = 1; x = b,一个线程执行b = 1; x = b

    public class OutOfOrderExecution {
          
          
        private static int x = 0, y = 0;
        private static int a = 0, b = 0;
    
        public static void main(String[] args) throws InterruptedException {
          
          
            //计数器
            int i = 0;
            for (;;) {
          
          
                i++;
                x = 0;
                y = 0;
                a = 0;
                b = 0;
    
                //保证两个线程同时开始执行
                CountDownLatch latch = new CountDownLatch(3);
    
                Thread one = new Thread(new Runnable() {
          
          
                    @Override
                    public void run() {
          
          
                        try {
          
          
                            latch.countDown();
                            latch.await();
                        } catch (InterruptedException e) {
          
          
                            e.printStackTrace();
                        }
    
                        a = 1;
                        x = b;
                    }
                });
    
                Thread two = new Thread(new Runnable() {
          
          
                    @Override
                    public void run() {
          
          
                        try {
          
          
                            latch.countDown();
                            latch.await();
                        } catch (InterruptedException e) {
          
          
                            e.printStackTrace();
                        }
    
                        b = 1;
                        y = a;
                    }
                });
    
                two.start();
                one.start();
                latch.countDown();
                one.join();
                two.join();
    
                String result = "第" + i + "次(" + x + "," + y + ")";
                //四种情况:0,1/1,0/1,1/0,0
                if (x == 0 && y == 0){
          
          
                    System.out.println(result);
                    break;
                } else {
          
          
                    System.out.println(result);
                }
            }
        }
    
    }
    
  • 分析会出现以下三种情况:

    1. a = 1; x = b(0); b = 1; y = a(1)
    2. b = 1; y = a(0); a = 1; x = b(1)
    3. b = 1; a = 1; x = b(1); y = a(1)
  • 但是还发生了一种情况,x = 0; y = 0,原因是发生了一种可能的重排序,可以使用volatile解决,即保证可见性
    在这里插入图片描述
    在这里插入图片描述

2、重排序的好处和三种情况

  • 重排序的好处就是能对指令进行优化,提高处理速度
  • 重排序有三种情况:
    • 编译器优化
    • CPU指令重排
    • 由于内存的多级缓存,线程之间出现可见性问题,导致“重排序”这一现象出现

二、可见性

1、可见性演示例子

  • 以下例子演示的是可见性,即一个线程无法看见其他线程的全部操作,以下代码中,一个线程执行change方法,另一个线程执行print方法

    public class FieldVisibility {
          
          
    
        int a = 1;
        int b = 2;
    
        private void change() {
          
          
            a = 3;
            b = a;
        }
    
        private void print() {
          
          
            System.out.println("b=" + b + ";a=" + a);
        }
    
        public static void main(String[] args) {
          
          
            while (true) {
          
          
                FieldVisibility test = new FieldVisibility();
                new Thread(new Runnable() {
          
          
                    @Override
                    public void run() {
          
          
                        try {
          
          
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
          
          
                            e.printStackTrace();
                        }
                        test.change();
                    }
                }).start();
    
                new Thread(new Runnable() {
          
          
                    @Override
                    public void run() {
          
          
                        try {
          
          
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
          
          
                            e.printStackTrace();
                        }
                        test.print();
                    }
                }).start();
            }
        }
    }
    
  • 运行过程中会出现三种常见情况:

    1. a = 3; b = 3
    2. a = 1; b = 2
    3. a = 3; b = 2
    4. a = 1; b = 3 这是一种不常见的情况,出现的原因就是因为一个线程执行了a = 3; b = a;,而另一个线程无法看见该线程的全部操作,忽略了a = 3\

2、为什么会有可见性问题?

在这里插入图片描述

  • CPU有多级缓存,导致读的数据过期
    1. 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在主内存和CPU之间多了cache层
    2. 线程间的对于共享变量的可见性问题不是由多核引起的,而是由多级缓存引起的
    3. 如果所有核心都只用一个缓存,那么不会存在内存可见性问题
    4. 每个核心都会将自己需要的数据读到独占内存中,数据修改后也是写入到缓存中,然后等待刷入主存中。所以会导致有些核心读取的值是一个过期的值

3、JMM的抽象:主存和本地内存

  • 什么是主内存和本地内存?

    本地内存并不是真正为每个线程分配一块内存,而是对于寄存器、一级缓存、二级缓存等的抽象 在这里插入图片描述

  • 主内存和本地内存的关系

    1. 所有变量都存储在主内存中,同时每个线程也有独立的工作内存,工作内存中的变量内容是主内存中的拷贝
    2. 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中
    3. 主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,要借助主内存中转来完成

4、Happens-Before原则

  • 第一种解释:happens-before规则是用来解决可见性问题的,在时间上,动作A发生在动作B之前,B保证能看见A
  • 第二种解释:如果第一个操作happens-before于另一个操作,那么第一个操作对于第二个操作是可见的

5、volatile关键字

  • volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile不会发生上下文切换等开销很大的行为;但volatile做不到synchronized那样的原子保护,仅能在有限的场景下才能发挥作用

  • volatile适用场合一:纯赋值操作

    如果一个共享变量自始至终只被各个线程赋值,而没有其他操作,那么可以用volatile来代替synchronized或者代替原子变量,因为赋值自身具有原子性,而volatile又保证了可见性,所以足以保证线程安全

    下面例子中,在 setDone() 方法中自始至终都是赋值操作done = true;,所以无论运行多少次,结果都是true

    public class UseVolatile implements Runnable {
          
          
        volatile boolean done = false;
        static AtomicInteger realA = new AtomicInteger();
    
        public static void main(String[] args) throws InterruptedException {
          
          
            Runnable r = new UseVolatile();
            Thread t1 = new Thread(r);
            Thread t2 = new Thread(r);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(((UseVolatile) r).done);
            System.out.println(realA.get());
        }
    
        @Override
        public void run() {
          
          
            for (int i = 0; i < 10000; i++) {
          
          
                setDone();
                realA.incrementAndGet();
            }
        }
    
        private void setDone() {
          
          
            done = true;
        }
    }
    

    但是下面这个例子就不一样了,flipDone() 方法中,done = !done,这个不是赋值操作,即使加了volatile关键字也无法保证线程安全

    public class NoVolatile2 implements Runnable {
          
          
        volatile boolean done = false;
        static AtomicInteger realA = new AtomicInteger();
    
        public static void main(String[] args) throws InterruptedException {
          
          
            Runnable r = new NoVolatile2();
            Thread t1 = new Thread(r);
            Thread t2 = new Thread(r);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(((NoVolatile2) r).done);
            System.out.println(realA.get());
        }
    
        @Override
        public void run() {
          
          
            for (int i = 0; i < 10000; i++) {
          
          
                flepDone();
                realA.incrementAndGet();
            }
        }
    
        private void flepDone() {
          
          
            done = !done;
        }
    }
    
  • 适用场合二:触发器

    在可见性的演示例子中,b 就是起到了触发器的作用,它保证了b = 0;之前的操作对其它线程可见

    volatile int b = 2;
    ...
    private void change() {
          
          
            ...
            ...
            a = 3;
        	//写入volatile变量
            b = 0;
    }
    
    private void print() {
          
          
        	//读出
            if (b == 0) {
          
          
                System.out.println("b=" + b + ";a=" + a);
            }
    }
    ...
    
  • volatile的两点作用

    1. 可见性:读到一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存中读取最新值,写一个volatile属性会立即刷入到主内存
    2. 禁止指令重排序优化:解决单例双重锁乱序问题

三、原子性

1、什么是原子性?

  • 一系列的操作,要么全部执行成功,要么全部不执行,操作是不可分割的
  • 原子操作有哪些?
    1. 除了long和double之外的基本类型的赋值操作
    2. 所有引用reference的赋值操作
    3. java.concurrent.Atomic.*包中所有类的原子操作
  • 简单地把原子操作组合在一起,不能保证整体依然具有原子性

2、单例模式

  • 饿汉式(静态常量):可以保证线程安全

    public class Singleton1 {
          
          
    
        private final static Singleton1 INSTANCE = new Singleton1();
    
        private Singleton1() {
          
          
    
        }
    
        public static Singleton1 getInstance() {
          
          
            return INSTANCE;
        }
    
    }
    
  • 饿汉式(静态代码块):线程安全

    public class Singleton2 {
          
          
    
        private final static Singleton2 INSTANCE;
    
        static {
          
          
            INSTANCE = new Singleton2();
        }
    
        private Singleton2() {
          
          
        }
    
        public static Singleton2 getInstance() {
          
          
            return INSTANCE;
        }
    }
    
  • 懒汉式:线程不安全,会出现多个线程竞争

    public class Singleton3 {
          
          
    
        private static Singleton3 instance;
    
        private Singleton3() {
          
          
    
        }
    
        public static Singleton3 getInstance() {
          
          
            //如果多个线程同时竞争,这时instance为空,会创建多个实例,不是单例
            if (instance == null) {
          
          
                instance = new Singleton3();
            }
            return instance;
        }
    }
    
  • 使用同步方法的懒汉式:线程安全,但是有性能问题

    public class Singleton4 {
          
          
    
        private static Singleton4 instance;
    
        private Singleton4() {
          
          
    
        }
    
        public synchronized static Singleton4 getInstance() {
          
          
            if (instance == null) {
          
          
                instance = new Singleton4();
            }
            return instance;
        }
    }
    
  • 使用synchronized代码块的懒汉式:线程安全,但会多次创建实例,不是单例的效果

    多个线程进入了if判断条件里,即运行到了“**”的位置,在同步代码块里一个线程执行完了又到另一个线程执行,会创建多个实例

    public class Singleton5 {
          
          
    
        private static Singleton5 instance;
    
        private Singleton5() {
          
          
    
        }
    
        public static Singleton5 getInstance() {
          
          
            if (instance == null) {
          
          
                //**
                synchronized (Singleton5.class) {
          
          
                    instance = new Singleton5();
                }
            }
            return instance;
        }
    }
    
  • 双重检查:在同步代码块中再检查一次,并且要使用volatile关键字来防止新建对象时发生重排序,推荐使用

    public class Singleton6 {
          
          
    
        private volatile static Singleton6 instance;
    
        private Singleton6() {
          
          
    
        }
    
        public static Singleton6 getInstance() {
          
          
            if (instance == null) {
          
          
                synchronized (Singleton6.class) {
          
          
                    if (instance == null) {
          
          
                        instance = new Singleton6();
                    }
                }
            }
            return instance;
        }
    }
    

    新建对象时有三个步骤,如果不用volatile来修饰创建的对象,编译器有可能发生指令重排序,构造方法还没执行,对象已经具有了内存地址,值不是null

    正常顺序 重排序
    1.分配对象的内存空间 1.分配对象的内存空间
    2.初始化对象 2.设置instance指向刚才刚分配的内存地址
    3.设置instance指向刚才刚分配的内存地址 3.初始化对象
  • 静态内部类:懒汉行为,在加载外部类时并不会立刻实例化内部类;并且内部类只实例化一次,保证了单例效果;推荐使用

    public class Singleton7 {
          
          
    
        private Singleton7() {
          
          
        }
    
        private static class SingletonInstance {
          
          
    
            private static final Singleton7 INSTANCE = new Singleton7();
        }
    
        public static Singleton7 getInstance() {
          
          
            return SingletonInstance.INSTANCE;
        }
    }
    
  • 枚举单例,推荐使用,最佳

    public enum Singleton8 {
          
          
        INSTANCE;
    
        public void whatever() {
          
          
    
        }
    }
    

猜你喜欢

转载自blog.csdn.net/weixin_44863537/article/details/112784242