美文网首页
Java内存模型 | synchronized与volatile

Java内存模型 | synchronized与volatile

作者: 寒食君 | 来源:发表于2018-04-29 15:45 被阅读83次

    JMM

    Java内存模型描述了Java程序中各种变量(共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取这些变量的底层细节。

    • 主存:所有共享变量都保存在主存中。
    • 工作内存:每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本。
    两条规定:
    • 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主存中读写。
    • 不同线程的工作内存之间无法直接相互访问,线程之间的变量传递,必须通过主存来完成。

    可见性

    首先了解一下什么是共享变量,假如一个变量被多个线程使用到,那么这个共享变量会在多个线程的工作内存中都存在副本。

    当一个共享变量被一个线程修改,能够及时被其他线程看到,这叫做可见性。

    要实现可见性,需要保证两点:
    • 共享变量被修改后,能及时刷新到主存中去。
    • 其他线程能及时将主存中更新的信息刷新到自己的工作内存中。

    synchronized实现可见性

    原子性:

    通过互斥锁来实现。

    可见性:
    • 线程解锁前,必须把共享变量的最新值刷新到主存中去。
    • 线程加锁时,会清空当前工作内存中共享变量的值,从主存中重新独取最新的值。
    流程:
    1. 获得互斥锁
    2. 清空工作内存
    3. 从主存拷贝共享变量的最新副本到工作内存
    4. 执行代码
    5. 将更改后的共享变量的值刷新到主存
    6. 释放互斥锁
    指令重排序:

    实际执行的代码顺序和程序员书写的顺序是不一样的,编译器或处理器为了提高性能,在不影响程序结果的前提下,会进行执行顺序的优化。

    • 编译器优化重排序(编译器)
    • 指令级并行重排序(处理器)
    • 内存系统重排序(处理器)
    as-if-serial语义:
    • 无论怎么重排序,程序执行的结果必须是与未排序情况下一致的。(Java保证在单线程情况下遵循词语义)
    • 多线程中程序交错执行时,重排序可能会导致内存可见性问题。
    导致不可见的原因:
    • 线程的交叉执行(原子性问题)
    • 重排序结合线程交叉执行(原子性问题)
    • 共享变量更新后的值,没有在工作内存和主存之间得到及时的更新。(可见性问题)

    volatile实现可见性

    能够保证volatile变量的可见性,但是不能保证volatile变量复合操作的原子性。
    volatile通过加入内存屏障和禁止指令重排序来实现可见性的。对volatile变量执行写操作时,会在写入后加一条store的屏障指令;对volatile变量执行读操作时,会在读操作前加入一条load屏障指令。

    线程写入volatile变量的过程:
    1. 改变线程工作内存中volatile变量副本的值
    2. 将改变后的副本的值从工作内存刷新到主存
    线程读volatile变量的过程:
    1. 从主存中独取volatile变量的最新的值到工作内存中。
    2. 从工作内存中独取变量的副本
    volatile不能保证volatile变量符合操作的原子性:

    举一个例子:

    public class VolatileDemo{
        private volatile int num=0;
        public int getNumber(){
            return this.num;
        }
        public void increase(){
            this.num++;
        }
        
        public static void main(String[] args){
            final VolatileDemo v=new VolatileDemo();
            for(int i;i<500;i++){
                new Thread(new Runnable(){
                
                public void run(){
                    v.increase();
                }
                }).start();
            }
            
            //主线程主动让出资源让500个子线程运行。这个‘1’指的是主线程
            while(Thread.activeCount()>1){
                Thread.yield();
            }
            System.out.println(v.getNumber());
        }
    }
    

    这个程序是开启500个线程,每个线程执行一次increase()操作,给变量num加一。运行这个程序多次,发现并不是每次输出结果都是500。

    发生了什么问题?

    因为this.num++这条语句,其实是三步操作,不具备原子性。假设一个运行场景:

    1. 此时num=1
    2. 线程A读取num为1,A工作内存中num=1
    3. 线程B独取num为1,B工作内存中num=1
    4. 线程B进行加1操作,写入B工作内存,B工作内存中num=2,更新到主存,主存中num=2.
    5. 线程A进行加1操作,写入A工作内存,A工作内存中num=2, 更新到主存,主存中num=2.

    可见,进行了两次加1操作,但是主存中的num只增加了1。怎么解决呢?我们要保证num自增操作的原子性。

    volatile注意事项
    1. 对变量的写入操作不能依赖当前值。
    2. 该变量没有包含在具有其他变量的不变式中。

    两者比较

    1. volatile 不需要加锁,比synchronized更轻量级,不会阻塞线程。
    2. 从可见性角度讲,volatile读相当于加锁,写相当于解锁。(前文提到的屏障指令)
    3. synchronized可以保证原子性和可见性,volatile只保证了可见性。
    image

    相关文章

      网友评论

          本文标题:Java内存模型 | synchronized与volatile

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