java内存模型(jmm)

并发

Posted by CHuiL on May 20, 2021

共享内存多核系统

在多处理器的系统中,没饿过处理器都有自己的高速缓存,而他们又共享同一主内存,这种系统成为共享内存多核系统;
由于各个核在计算的过程,都需要将主内存中的数据读取到核私有的高速缓存中,如果多个处理器都运算到了同一块主内存的数据,那么该以哪一块为主?这就导致了缓存不一致,需要解决一致性问题;

java 主内存和工作内存

首先先放一张jvm内存布局图 jvm内存布局

java内存模型规定所有的变量(这里的变量都指具体的对象数据,而不是引用)都存储在主内存中,而线程对变量的所有操作(读取,赋值)都必须在工作内存中进行,而不能直接读写主内存中的数据

这里与前面的共享内存多核系统中进行类比,会发现主内存可以对应系统中的物理主内存,而工作内存可以对应处理器的高速缓冲;而查看jvm内存布局图,可以发现主内存主要对应jvm堆中的对象实例部分,而工作内存则是虚拟机栈中的部分区域;

线程的工作内存保持了该线程使用的变量的主内存副本(并不是一个完整的副本,这个对象的引用,对象中某个在线程访问到的字段是有可能被复制的),因为工作内存不能直接读取,赋值工作内存中的数据,所以就会出现共享内存多核系统同样的问题,数据一致性的问题;

为了获得更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓冲中,因为程序运行时主要访问的时工作内存;

volatile关键字

通过前面关于工作内存的了解,我们知道了不同线程对于同一变量都可能有自己的副本,如果其中一个线程修改了它的值,但是没有即使同步到主内存,其他线程读取了旧值,就会导致数据不一致问题;
一旦变量被该值修饰,他有两个语意

此变量的修改对所有线程可见

其实关于这个关键字的描述,已经见过很多次了,但是从来都是似懂非懂的感觉;volatile变量对所有线程是立即可见的,对它的写操作能==立即反映到其他线程==中,注意这里,不仅仅是说,对变量的修改要立即写回到主内存中,其他线程在工作内存中每次使用对应变量之前都要先刷新,以获取最新的值(要use,必须先load,在assign后,必须write),,这样就保证了每个线程对该变量的使用都是一致的了(非要说的话,在线程使用该变量之前,可能存在一段实际的不一致,但竟然都没使用,自然也就没啥问题)

但是即使没有了一致性问题,也不能说就线程安全了;以最简单i++ 为例子,volatile毕竟只是保证了各个线程工作内存之间的数据一致,但是i++这种操作并不是原子性的,他同样会被拆成 读取数据,计算数据,存储数假如加入在读取到==栈顶==之后,其他线程修改了该值,此时栈顶的值又不一致了,执行计算,存储数据,就又导致了并发问题;

所以volatile变量只能保证线程之间工作内存的可见性,在不符合下面的运算场景中,我们仍然需要加锁同步

  • 运算结果并不依赖变量的当前值;或者 能够确保只有单一的线程修改变量的值(其他线程只读,如控制开关)
  • 变量不需要与其他的状态变量共同参与不变约束(???)

禁止指令重排优化

java内存模型中存在“线程内表现为串行的语义”;其实就是,==代码被实际编译为具体的机器指令后,并不一定会根据我们代码的顺序去执行==,指令可能被重排,但是最终结果是一致的;
举个例子,比如指令1为:把地址A的值+10;指令2为:把地址A的值*2; 指令3为:把地址B中的值设置为true;这里指令12的顺序肯定是不能被重排的,因为重排结果就不一样了,但是3与12的顺序就没关系了,3可以放在12指令之前,也可以放在12之间;假设12为我们另外一个线程执行的前提条件,该线程在循环等待地址B中的值为true,那么3被充拍到12之前,就会发生地址A中的值还没准备好,但是B已经被设置为true,导致该线程提前执行导致错误;
如果我们将该地址B中的值变量加上关键字volatile,那么在赋值操作之后,会多加一条指令,该指令会将修改同步到内存中(称为内存屏障),同时意味着所有前面的操作都已经执行完成,后面的指令不能重排序到该指令之前;这样就保证了,只要地址B更新成功,那么前面指令12也必须已经执行完毕;

典型的使用场景

public class Singleton {
    private volatile static Singleton instance;

    public static Singleton getSingleton() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

单例的双重锁结构;这里的问题出在instance = new Singleton()这句赋值语句;该赋值语句可以被拆成多条指令

1.分配对象的内存空间
2.初始化对象
3.设置instance指向刚分配的内存地址,当instance指向分配地址时,instance不为空

这里1肯定要在23之前,但是23顺序可以调整,假设3被调整到2之前,那么instance这个引用变量便不再是null了,其他线程在进入到这个getSingleton()方法后,判断instance !=null,返回了该变量并且直接使用,但是该变量引用实际指向的对象并未初始化,从而导致了错误;
所以这里也需要加上volatile来保证23的指向顺序;

volatile boolean action;
...
 while(!action){
     doSomething();
 }

asleep变量如果不使用volatile,可能因为重排序,导致提前进入,或者其他线程已经修改了该变量,但是对该线程却不可见;

double和long的读取原子性问题

一些虚拟机实现对于doble和long值的读写可能被拆分为两次,分别读写前32位和后32位;这可能导致有多个线程同时写了前32位,而其他线程写了后32位;使用volatile可以使double和long值得读写均为原子性的.但是实际上,除非该数据有明确可知的线程竞争,否则这种非原子性读写的问题几乎不会发生;
引用的读写都是原子性的;(指引用本身?还是引用指向的的对象??)