java 关于volatile, 指令重排, synchronized的心得

volatile

volatile 有两种语义:
1. volatile 修饰的变量可以保证其内存可见性, 即在读写的时候都是操作主存, 而不是操作分配给各个线程的cache;
2. volatile 可以阻止指令重排. 此作用并不是因为语义1的原因.
内存原子操作
java的内存分为主内存和各个工作内存, 工作内存里存有该线程下的局部变量和从主内存里拷贝的共享变量副本. 当变量没有volatile修饰时, 线程操作的仅是主内存的副本, 这就导致其他线程拿不到实时的值.
volatile修饰的变量在读操作时, 会连续执行 read + load + use 三步; 写操作时会连续执行 assign + store + write 三步, 这样就保证了多线程下读写的值均是主内存的真实值.

java内存模型在多线程环境下需要解决两个问题, 一个是可见性, 另一个是有序性.
volatile只能保证内存可见性, 但不能保证顺序, 两个线程写的时候, 有可能是这两个线程中的任意一个值

例如经常讨论的多线程下 a++ 或者 a = a+1 这种指令, 即使变量a使用volatile修饰了, 最后的值也不一定会是什么样, 因为它们不是原子操作, 可以拆解为以下几步
1. 从主内存获取到a值, volatile修饰的情况下可以保证取到的是真实值
2. a+1, 把计算结果暂存到工作内存某位置
3. 把工作内存存储的值赋给主内存的a, 也能保证同步
但是注意, 123三步中间可能插有其他线程的操作, 假设原来 a=0, 线程A和线程B同时获取到了a的值0, 假设当A执行到2/3中间时 B已经执行完了 a=-1 的原子操作, 主内存中的a已经变为了 -1, 但是A在执行完毕后, 会把a值修改为在第二步保存下来的1, 这样线程B的操作全部无效, 这就是执行有序性的问题.

synchronized 可以同时保证变量的内存可见性以及修改的有序性, 类似下方的操作读写到的data都是真实值, 变量a也不会出错.

public synchronized Data getData(){
    a++;
    return data;
}
public synchronized void setData(Data data){
    a--;
    this.data = data;
} 

参考:
1. https://www.cnblogs.com/tangyanbo/p/6538488.html
2. https://www.cnblogs.com/wrencai/p/5704331.html
3. https://www.cnblogs.com/chihirotan/p/6486436.html
4. https://blog.csdn.net/sigangjun/article/details/47784213


指令重排

  • 指令重排是什么

    指令队列在CPU执行时不是串行的, 当某条指令执行时消耗较多时间时, CPU资源足够时并不会在此无意义的等待, 而是开启下一个指令. 开启下一条指令是有条件的, 即上一条指令和下一条指令不存在相关性. 例如下面这个例子:

    a /= 2;   // 指令A
    a /= 2;   // 指令B
    c++;      // 指令C

    这里的指令B是依赖于指令A的执行结果的, 在A处于执行阶段时, B会被阻塞, 直到A执行完成. 而指令C与A/B均没有依赖关系, 所以在A执行或者B执行的过程中, C会同时被执行, 那么C有可能在A+B的执行过程中就执行完毕了, 这样指令队列的实际执行顺序就是 C->A->B 或者 A->C->B.
    深入理解Java虚拟机 的相关讲解

  • java对象的初始化中的指令重排

    一条对象初始化语句 AA aa = new AA() 的实际语义其实分为三步:

        AA aa:  分配内存
        new AA(): 创建对象
        =: 把创建的对象指给分配的内存地址

    根据上面所说的, 2和3没有依赖关系, 在2执行的过程中, 3可能已经执行完毕了, 这时实际执行顺序为 1->3->2. 注意, 此时线程A操作的是aa在主内存的拷贝, 如果在3执行/2完毕的临界时间点时 此工作内存同步到了主内存中, 其他线程引用aa的时候就会得到一个初始化不完全的AA对象.
    volatile 的语义之一就是禁止指令重排, 使用 volatile 修饰AA对象, 可以保证 AA aa = new AA() 的三条语义是按照123的顺序执行的, 这时其他的线程在访问aa对象的时候只会得到 null 或者初始化完成的 aa.
    注意, 此处是利用的volatile的禁止指令重排语义, 并不是内存可见性, 这也是为什么单例模式中使用了synchronized的同时还需要volatile的原因.

参考:
1. https://blog.csdn.net/aigoogle/article/details/40793947
2. https://www.cnblogs.com/wrencai/p/5704331.html


synchronized

synchronized是对象同步锁, 可以保证多线程情境下的内存可见性和执行有序性.
各种类型各种用法本质上都是锁住的对象, 当执行synchronized fun1时, synchronized fun2 也会等待.

  1. 这两种用法是锁住的当前实例对象

    synchronized void func0(){
        ...
    }
    
    void func1{
        synchronized(this){
            ...
        }
    }
  2. 这种是锁住的是当前类, 静态对象以及所有实例对象共用一把锁.

    void func1{
        synchronized(AA.class){
            ...
        }
    }
  3. 这种是锁住静态对象, 即所有的静态方法共用一把锁, 但是不影响其他的实例方法.

     synchronized static void func{
      ...    
     }
  4. 写单例的时候要给getInstance加锁, 用于防止同步问题, 一般写法是:

    public class AA{
        private volatile static AA instance; 
    
        private AA(){}
    
        public static AA getInstance(){
            if(instance == null) {
                synchronized(AA.class) {
                    if(instance == null) {
                        instance = new AA(); 
                    }
                }
            }
            return instance;
        } 
    
        // 其他方法不能加锁, 加了锁之后多线程下效率太差
        public void otherFunction(){...}
    }

猜你喜欢

转载自blog.csdn.net/j550341130/article/details/79930690