美文网首页JVMJVM
JMM之Synchronized&Volatile

JMM之Synchronized&Volatile

作者: AlanKim | 来源:发表于2019-01-20 18:27 被阅读22次

    内存可见性

    • 可见性:如果一个线程对共享变量值的修改,能够及时的被其他线程看到,那么这个共享变量就是可见的
    • 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量

    JAVA内存模型 JMM(Java Memory Model)

    JMM描述了java程序中各种变量(就是指线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节

    • 主内存:整个JVM管理的内存
    • 工作内存:独属于每个线程的内存,保存该线程用到的所有变量的副本(即主内存中该变量的一个copy)


      Snip20180320_1.png
    • 线程只能与自己的工作内存打交道,对共享变量的所有操作不能在主内存中直接读写,必须在自己的工作内存中进行操作,注意,是read and write
    • 如果希望与主内存交互,那么必须先操作本身的工作内存中变量,然后通过工作内存与主内存的交互达到目的
    • 再强调一点,线程只能与自己的工作内存交互,不能访问其他线程的工作内存。
    • 如果需要在工作内存中传递变量值,需要通过主内存作为桥梁处理。

    共享变量内存可见性的实现原理

    如果线程1修改后的共享变量A想被线程2看到,那么需要经历以下步骤:

    1. 将工作内存1中修改过的A最新值 刷新到主内存中
    2. 将主内存中被修改过的A最新值更新到工作内存2中

    如果在任何一个步骤中出现问题,都会导致数据在不同的内存区域存在不同的值,也就是所谓的线程不安全。

    java语言层面的实现可见性的方式:

    • synchronized
    • volatile

    jdk1.5之后引入的concurrent下面的包属于另一种实现方式

    可见性保证前提:

    1. 线程修改后的共享变量能及时的刷新到主内存中
    2. 其他线程能够及时的把共享变量-最新值从主内存更新到自己的工作内存中

    synchronized

    特性:

    • 原子性---—即同步,保证同一时间只有一个线程可以访问锁内代码
    • 可见性
    JMM关于synchronized的两条规定
    • 线程解锁前,必须把共享变量的最新值刷新到主内存中
    • 线程加锁时,将清空工作内存中存储的共享变量的值,从而使用共享变量时,必须从主内存中重新读取最新的值。(注意:解锁和加锁,是指同一把锁)

    满足以上两条规定,也就意味着解锁前对共享变量的值的更新可以在,下次加锁时对其他线程是可见的。

    但是即使没加入synchronized修饰,主内存和工作内存之间的数据更新也不一定不会发生,因为cpu缓存的刷新是非常快的,只有在高并发的情况下才会出现线程不安全的情况。

    线程执行互斥代码的过程如下:
    1. 获取互斥锁
    2. 清空工作内存,把本线程所有工作内存中的共享变量都清除
    3. 从主内存copy变量的最新副本到工作内存
    4. 开始执行代码
    5. 如果有更新,则把更改后的共享变量值刷新到主内存中
    6. 释放互斥锁

    指令重排序

    代码书写的顺序和实际执行的顺序可能不同,指令重排序是编译器JIT或者处理器JVM为了提高程序性能而做的优化,主要有三种:

    1. 编译器优化的重排序,由编译器优化,主要在单线程环境下,在保证结果正确性之前对代码的执行顺序进行调整
    2. 指令级并行重排,处理器级别优化,cpu支持指令级并行技术,多核
    3. 内存系统的重排序,主要是处理器对读写缓存进行的优化,也就是上面说的主内存、工作内存之类的操作

    as-if-serial

    指无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致。

    java编译器、运行时(RunTime,JVM)和处理器都会保证java在单线程下遵循as-if-serial语义

    所以指令重排序不会导致单线程下出现内存可见性问题。

    但是在多线程中,如果代码交错执行,那么就有可能出现可见性问题

    只有数据依赖相关才会禁止重排序,逻辑上控制,比如if语句和if中的逻辑,也可能会出现重排序

    导致共享变量在线程间不可见的原因

    1. 线程的交叉执行
    2. 重排序+线程的交叉执行
    3. 共享变量更新后的值没有在工作内存与主内存之间即使更新
    synchronized的解决方案
    1. —>原子性,synchronized修饰的代码在一定时间内只能由一个线程持有,线程释放锁之后才会被其他线程占用
    2. —>原子性,重排序只能在单线程内部排,不会出现3.1—> 4.2 这种情况的重排
    3. —> synchronized 可见性保证,释放锁的时候会刷新到主内存

    代码:

    package com.alan.alanstatemachine;  
    import org.junit.Test;
    import java.lang.Thread;
    
      /**
         * 用于学习synchronized使用方法
      */
    public class SynchronizedDemo {
    
    // 首先定义三个变量, 都是共享变量
    boolean ready = false;
    int number = 2;
    int result = 0;
    
    /**
     * 写方法,更改共享变量的值
     */
    public void write() {
        ready = true; // 步骤1.1
        number = 4;   // 步骤1.2
        }
    
    /**
     * 读方法,打印共享变量的值
     */
    public void read() {
        if (ready) {             // 步骤2.1
            result = number * 3;  // 步骤2.2
        }
    
        System.out.println("current result = " + result);
    }
    
    /**
     * sync写 方法,更改共享变量的值
     */
    public synchronized void writeWithSync() {
        ready = true; // 步骤3.1
        number = 6;   // 步骤3.2
    }
    
    /**
     * sync读 方法,打印共享变量的值
     */
    public synchronized void readWithSynv() {
        if (ready) {             // 步骤4.1
            result = number * 3;  // 步骤4.2
        }
    
        System.out.println("sync current result = " + result);
    }
    
    // 创建一个内部线程类,用于启动多个线程测试内存可见性
    class ReadAndWriteThread extends Thread {
    
        boolean flag = false;
    
        ReadAndWriteThread(boolean outFlag) {
            this.flag = outFlag;
        }
    
        /**
         * If this thread was constructed using a separate
         * <code>Runnable</code> run object, then that
         * <code>Runnable</code> object's <code>run</code> method is called;
         * otherwise, this method does nothing and returns.
         * <p>
         * Subclasses of <code>Thread</code> should override this method.
         *
         * @see #start()
         * @see #stop()
         */
        @Override
        public void run() {
            if (flag) {
                write();
                writeWithSync();
            } else {
                read();
                readWithSynv();
            }
        }
    }
    
    @Test
    public void test() {
    
        SynchronizedDemo demo = new SynchronizedDemo();
    
        // 传入true,应该是写操作,修改工作内存中的共享变量值
        demo.new ReadAndWriteThread(true).start();
    
        // 传入false,读操作,看是否拿到了最新的修改后 的ready及number、result数据
        demo.new ReadAndWriteThread(false).start();
    
        // 保证可见性的情况
        // 如果执行顺序是 1.1 --> 2.1 --> 2.2 --> 1.2 那么结果是6
        // 如果执行顺序是 1.1 --> 1.2 --> 2.1 --> 2.2 那么结果是12
        // 如果重排序1,执行顺序是1.2 --> 2.1 --> 2.2 --> 1.1,那么结果是0
     }
    }
    

    Volatile保证内存可见性

    • Volatile可以保证共享变量的可见性
    • 但是并不能保证原子性
    • volatile通过内存屏障和禁止指令重排序来实现内存可见性
      • 线程在对volatile修饰的变量进行写操作之后,处理器会在写操作后加入一个store的屏障指令,会把处理器写缓冲区的缓存强制刷新到主内存中去,所以主内存中的变量值就是最新值。(??但是,不是写在工作内存中的吗?跟处理器缓存有啥关系?难道还是通过处理器缓存来实现的刷新同步么?)—注意,工作内存只是一个逻辑概念,并不真实存在,它是由写寄存器+写缓冲区实现的
      • store的屏障指令还能防止处理器将volatile修饰变量之前的操作重排序到volatile修饰变量之后
      • 对volatile变量进行读操作时,会在读操作前加入一条load屏障指令,也会强制缓冲区缓存失效,从而读到主内存中变量的最新值。同时load屏障指令也有禁止指令重排序的效果,不是禁止所有的,只是禁止volatile变量之前和之后的操作位置互换

    所以线程对volatile变量的操作步骤就如下:

    写:
    1. 改变工作内存中共享变量副本的值
    2. 将最新值及时刷新到主内存中去
    读:
    1. 失效工作内存中变量,强制从主内存中读取最新的变量值存入工作内存中,作为副本存在
    2. 使用时从工作内存中读取。

    JMM中定义了8条指令来完成主内存和工作内存的数据同步操作

    // TODO

    Volatile不能保证原子性

    private int number = 1;
    number++;
    

    其中number++并不是原子操作,它是以下三个分解操作的简写:

    1. get number value from number_var
    2. number_value + 1 to a temp var
    3. set new number value to number_var
    

    对于synchronized,

         synchronized(this){
            number++;
         }
    

    由于synchronized的语言特性,number++语句此时是一个原子操作

    而对于volatile:

    private volatile int number = 1;
    number++;
    

    无法保证原子性

    示例代码:

     /**
       * volatile为什么不能保证对共享变量的原子性操作
      */
    public class VolatileDemo {
    
    private volatile int number = 0;
    
    /**
     * increase
     */
    public void increase() {
        // 可见性是肯定的
        // 为了更直观看到无法保证原子性,休眠下
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        number++;
    }
    
    /**
     * get number
     *
     * @return current number value
     */
    public int getNumber() {
        return number;
    }
    
    @Test
    public void test() {
    
        // 刚开始线程数
        System.out.println("start thread count =" + Thread.activeCount() );
        int startThreadCount = Thread.activeCount();
      
        // 启动线程增加volatile变量
        for (int i = 0; i < 500; i++) {
            new Thread(() -> increase()).start();
        }
        // 如果还有子线程未执行完,则主线程通过yield()让出cpu资源
        while (Thread.activeCount() > startThreadCount) {
            System.out.println("current sub thread count=" + Thread.activeCount());
            Thread.yield();
        }
    
        System.out.println("current number value =" + getNumber());
    }
    }
    

    可以看下,基本上getNumber的值最后都是小于500的。问题就出在number++上

    因为volatile无法保证number++的三个分解操作的原子性,所以可能同时有三个线程过来操作这三个操作,整个过程就会串掉。

    假设某一时间点 number = 5:

    • 线程A获取到cpu资源,获取到number的值,然后释放掉cpu
    • 线程B获取到cpu资源,获取到nubmer的值
    • 线程B number +1
    • 线程B 将number写入到工作内存,由于volatile修饰了number变量,那么在主内存及线程B的工作内存中,number = 6
    • 但是线程A的工作内存中,number = 5,因为读取的时候确实是从主内存中读取的最新值,那么在后续操作的时候不需要再去主内存中再读一次
    • 此时如果线程A再获取到cpu资源,执行+1操作,并写入工作内存中,那么此时number = 6,再写入到主内存中,也还是6
    • 那么虽然对number进行了两次++操作,但是实际上在主内存中,只加了一个1
    保证number原子性的解决方案有:
    1. 使用synchronized关键字修饰number++代码
    2. 使用ReentrantLock(java.util.concurrent.locks)
    3. 使用AtomicInteger (java.util.concurrent.atomic)

    修改后的代码:

    import org.junit.Test;
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
    
    - volatile为什么不能保证对共享变量的原子性操作
      */
      public class VolatileDemo {
    
    private volatile int volatileNumber = 0;
    
    // 使用synchronized,不需要再使用volatile来保证可见性
    private int synchronizedNumber = 0;
    
    // 使用reentrantLock
    private int lockNumber = 0;
    
    private Lock lock = new ReentrantLock();
    
    /**
    
    - increase
      */
      public void volatileIncrease() {
      // 可见性是肯定的
      // 为了更直观看到无法保证原子性,休眠下
      try {
          Thread.sleep(20);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      this.volatileNumber++;
      }
    
    public void synchronizedIncrease() {
        // 可见性是肯定的
        // 为了更直观看到无法保证原子性,休眠下
        try {
            Thread.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        // 为什么不在方法定义上加synchronized呢?
        // 其实也可以,但是这样锁的粒度比较大,休眠也被锁住了,无法释放资源,等待时间就会比较久
        // 所以在代码块的基础上加synchronized关键字
        synchronized (this) {
            this.synchronizedNumber++;
        }
    }
    
    public void lockIncrease() {
        // 可见性是肯定的
        // 为了更直观看到无法保证原子性,休眠下
        try {
            Thread.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        // 使用reentrantLock
        lock.lock();
        try {
            this.lockNumber++;
        } finally {
            lock.unlock();
        }
    }
    
    /**
    
    - get number
      *
    - @return current number value
      */
      public int getVolatileNumber() {
      return this.volatileNumber;
      }
    
    public int getSynchronizedNumber() {
        return this.synchronizedNumber;
    }
    
    public int getLockNumber() {
        return this.lockNumber;
    }
    
    @Test
    public void test() {
        // 刚开始线程数目
        System.out.println("start thread count =" + Thread.activeCount() );
        int startThreadCount = Thread.activeCount();
    
        // 启动线程增加volatile变量
        for (int i = 0; i < 500; i++) {
            new Thread(() -> volatileIncrease()).start(); // volatile 肯定不准
            new Thread(() -> synchronizedIncrease()).start(); // synchronized  为啥这个也不准呢?
            new Thread(() -> lockIncrease()).start(); // Lock  为啥你也不准呢?
        }
    
        // 如果还有子线程未执行完,则主线程通过yield()让出cpu资源
        while (Thread.activeCount() > startThreadCount) {
            Thread.yield();
        }
    
        System.out.println("current volatile number value =" + getVolatileNumber());
        System.out.println("current synchronized number value =" + getSynchronizedNumber());
        System.out.println("current lock number value =" + getLockNumber());
    }
    }
    

    volatile使用的场景

    要在多线程中安全的使用volatile变量,需要满足以下两个条件:

    1. 对变量的写操作不依赖其当前值
      • 比如 count++,count=count+1这种,就不满足
      • 而对于boolean类型、温度变化场景,就满足
    2. 该变量不能包含在其他变量的不变式中
      • 比如有两个volatile变量,low 和 up,如果存在 low < up 的这种不变式比较,则不满足

    Synchronized VS volatile

    • volatile不需要加锁,不会阻塞线程,所以相对synchronized 更轻量级,性能更高
    • 从内存可见性角度讲,对volatile变量的读操作,相当于synchronized的加锁,即清空工作内存,从主内存中更新最新值到工作内存中
    • 而对volatile的写操作,相当于synchronized的解锁,将数据及时刷新到主内存中
    • synchronized同时保证可见性及原子性,而volatile只保证可见性

    补充:

    java中long和double都是64位的,而对这两种类型对象的操作可能并不是原子操作,因为jmm允许jvm对没有添加volatile修饰的64位对象操作分解为两次32位读写操作来操作,所以加上volatile可以解决这种问题。不过大多数情况下,多数jvm都已经实现了原子性,所以不需要特殊操作。

    另一种保证内存可见性的操作,是使用final修饰,这个后续再细看。

    相关文章

      网友评论

        本文标题:JMM之Synchronized&Volatile

        本文链接:https://www.haomeiwen.com/subject/pxffjqtx.html