多核与内存模型
在多核时代,如何提高CPU的性能成为了一个永恒的话题,而关于这个话题的讨论主要就是如何定义一个高性能的内存模型。内存模型用于定义处理器的各层缓存与共享内存的同步机制及线程和内存交互的规则。对于多线程系统而言,内存模型主要就是指定各个线程与内存之间的读写操作。
多核与内存模型
关于可见性
在多核多线程环境中,为了提升运行时数据访问性能,经常会使用多层缓存策略。这种架构也就带来了共享变量可见性问题,每个核或线程都有自己的本地副本,对于共享变量的读写操作可能不会被其它核或线程知晓,也就是不可见了。
可见性
Java内存模型
Java的世界也有属于它自己的内存模型,Java内存模型(Java Memory Model),简称JMM。由于Java被定义成一种跨平台的语言,所以在内存的描述上面也要能是跨平台的,Java虚拟机试图定义一种统一的内存模型,能将各种底层硬件及操作系统的内存访问差异进行封装,使Java程序在不同硬件及操作系统上都能达到相同的并发效果。它描述了程序中各个变量之间的关系,包括实例域、静态域、数据元素及在实际计算机系统中将变量存储到内存和从内存中取出变量的底层细节。
为更好理解JMM的工作机制,我们通过下图进行理解。从整体上看有几个比较重要的概念:主内存、工作(本地)内存、共享变量、共享变量副本、线程等。首先看主内存与工作内存及他们的关系,主内存保存了Java程序的所有变量,当然这个变量不包括局部变量和方法参数。工作内存则包含了这些共享变量的副本。其次是线程与工作内存的关系,每个线程都有一个属于自己的工作内存,不同线程之间的工作内存是互相不可见的,且线程对变量的操作也只能是针对自己的工作内存。最后是关于线程之间的通信机制,线程之间不可直接传递。假如某个线程对一个变量进行了重新赋值,那么该如何让另一个条线程知道呢?线程A将变量改变反应到主存中,线程B再从主存中读取,这样才能完成线程之间的通信。
Java内存模型
JMM主要操作
JMM定义了八个主要的内存操作来完成工作内存与主存的通信。假如一条线程准备对一个变量进行新的赋值操作,它可能会先用lock操作锁住主内存中的某个变量,不让其他线程获得此变量的锁,直至使用unlock操作释放该变量的锁。接着使用read操作将变量从主存读到工作内存中,紧接着load操作将得到的变量值放到工作内存中的变量副本。use操作则将变量值传给线程执行引擎进行运算操作,assign操作把新的变量值从线程执行引擎中传递到工作内存。store操作则把变量值从工作内存传送到主存中,接着write操作将得到的值写入主存相应的变量中,最后使用unlock操作释放变量的锁。
主要操作
JMM的原子性
Java内存模型保证了read、load、assign、use、store、write等操作具有原子性,我们可以认为除了long和double类型外,对其他基本数据类型所对应的内存单元的访问读写都是原子的。但由于这个原子性的颗粒度太小,通常情况下我们需要更大颗粒度的原子性,这时就需要用锁来保证了。
JMM的可见性
在Java内存模型中,如果一个线程更改了共享变量的值,其他线程能马上知道这个更改,则我们说这个变量具有可见性。一般来说有四种方式能保证变量的可见性,分别为volatile、synchronized、final和锁。
首先谈谈volatile,被此关键词声明的变量,每当有任何更改时都将立即同步到主存中,而每个线程要使用这个变量时都要重新从主存刷新到工作内存,这样就确保了变量的可见性。当然,普通变量最终也会同步到主存,再由主存同步到每个线程的工作内存,只是这个最终可能比较“长久”,不能保证可见性。
其次,关于synchronized,由于synchronized底层也是通过锁进行实现,所以synchronized和锁的本质是一样的。当一个线程释放一个锁时,将会强制刷新工作内存中的变量值到主存中。而当另一个线程获取此锁的时候将会强制重新装载此变量值。当然这两个线程获取的是同一个锁,这样就保证了变量的可见性。
最后,被final声明的变量一旦完成初始化,其他线程就能看到这个final变量。其实,可见性其实可以看成是一种机制,线程在进入/退出同步块程序时,它将发送/接收一个变量的更改。