跳到主要内容

07、Java JUC 源码分析 - synchronized、volatile内存语义及内存可见性

1、JAVA中的线程安全的问题

谈到线程安全的问题,就不得不提共享资源。所谓的共享资源,其实就是说这个资源被多个线程持有,或者说多个线程可以访问该资源。

线程安全问题其实就是指,多个线程同时多谢一个共享资源并且没有任何同步措施的时候,导致了读脏数据,或者其他不可预见的问题。

Java内存模型规定,所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间,或者叫做本地内存,线程读写其实操作的是本地内存里面的变量。如果不太理解,我们来看下图:

 

如上图,线程A和线程B可以同时操作主内存中的共享变量。如果上面两个线程同时读取共享变量而不去修改,那么就不会存在线程安全问题,只有当至少一个线程修改共享变量才有可能出现线程安全问题。最典型的就是计数变量count。计数变量count本身是一个共享变量,然后多个线程对其进行递增,如果不使用同步措施,由于递增操作是分为三个步骤的:获取-计算-保存。因此可能导致计数不准。下面我么来看一个表格。

t1 t2 t3 t4
线程A 从主内存读取count值到线程本地内存 递增本线程count的值 写回主内存
线程B 从主内存读取count值到线程本地内存 递增本线程count的值 写回主内存

加入当前count=0,t1时刻线程A从主内存读取count值到本地内存countA。然后在t2时刻递增countA值为1,同时线程B从主内存读取count值到本地内存countB,此时countB为0(因为countA还没有被写入主内存)。t3时刻线程A才把countA的值写入主内存,至此线程A的一次计数完毕,同时线程B递增countB的值为1,然后在t4时刻线程B把countB的值写入主内存。至此线程B的一次计数完成。
这个时候,问题就来了,明明是做了两次计数,但是主内存里面的count却为1。其实这就是线程安全问题。如何解决这个问题呢?这就要用synchronized关键字来进行同步了。后面会讲解。

2、JAVA中的共享变量的内存可见性的问题

上图只是JAVA内存模型的一个抽象概念,那么实际实现中,线程的工作内存是怎么样的呢?我们来看下图:

 

图中所示是一个双核CPU系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行逻辑运算。每个核都有自己的一级缓存L1 Cache,还有一个所有核都共享的2级缓存L2 Cache。

当操作一个共享变量时,他首先从主内存复制到自己的本地内存,处理完后在更新到主内存。

那么假如线程A和线程B同时处理一个共享变量,会出现什么情况呢。如果我们使用上图的架构,将会导致内存不可见问题。具体分析如下:

  • 线程A首先获得了共享变量X的值,由于两级缓存都没有命中,所以加载主内存中X的值,假设为0。然后将X=0缓存到两级缓存,线程A修改两级缓存里面的X的值,修改为1,并刷新到主内存。线程A操作完毕后,线程A的两级缓存里面的内容为X=1。
  • 线程B要获得X的值,首先L1 Cache没有命中,然后查看L2 Cache,第二级缓存命中,所以返会X=1。到这里都是正常的,因为主内存中此时的X也为1。然后线程B开始修改X,将其修改为2,并将结果修改到两级缓存,然后更新到主内存中,主内存中X的值就为2。
  • 如果这时候线程A又要修改X的值,在获取时就会命中L1 Cache,并且X=1,到这里就出现了问题。明明线程B已经将X修改为2了,为何线程A拿到的X值还是1。也就是共享内存不可见的问题。可见性是指一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。

出现内存不可见的问题是因为线程每次获取变量的顺序为:L1 Cache > L2 Cache > 主内存。那么如何解决这个问题呢。使用JAVA中的关键字volatile

3、synchronized介绍

synchronized是JAVA中提供的一种原子性内内置锁。JAVA中每一个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫监视器锁。线程在执行synchronized块时会自动获得监视器锁。如果该锁已被其他线程获取了,那么当前线程会被阻塞。

3.1、synchronized主要有3种使用方式

  • 修饰实例方法,作用于当前实例,进入同步代码前需要先获取实例的锁
  • 修饰静态方法,作用于类的Class对象,进入修饰的静态方法前需要先获取类的Class对象的锁
  • 修饰代码块,需要指定加锁对象(记做lockobj),在进入同步代码块前需要先获取lockobj的锁

无论synchronized加在哪里,他锁住的是永远对象。

3.2、synchronized内存语义

进入synchronized块内存语义:把synchronized块中使用到的变量从线程本地内存中清除,这样synchronized块用到该变量时就会直接去主内存中拿。
退出synchronized块内存语义:把synchronized块内对共享变量的修改刷新回主内存。

4、volatile介绍

当一个变量被声明为volatile时,线程在写入变量时,不会把值缓存到两级缓存,而是会把值直接刷新到主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用本地内存的值。
那么一般在什么时候使用volatile关键字呢?

  • 写入变量值不依赖变量当前值。因为如果依赖当前值,将是获取-计算-保存这三步,这三步操作不是原子性的。volatile不保证操作的原子性。(原子性:一系列操作要么全部成功,要么全部失败)
  • 读写变量时没有加锁。因为加锁本身已经保证了内存可见性,这个时候不需要把变量声明为volatile。