阅读 276

反骨之Java是如何解决并发中的可见性问题的

前言

前段时间笔者写过一篇关于, 关于《反骨之Java是如何解决并发中的原子性问题》的博文。

其中,提出一个观点:Java中使用互斥锁和CAS解决了并发中的原子性问题。

那么,本篇博文则主要探讨的是:

Java中如何利用Java内存模型规范中的Volatile、synchronized、final关键字解决可见性问题。


正文

在开始重点之前,我们注意到上文提到的两个关键词,即内存模型可见性问题。

由于笔者在《反骨之硬件&软件为Java并发编程中挖的坑(可见性&原子性&有序性)》一文中,对可见性仅是一笔带过,所在笔者认为在本篇博文中,需要对可见性进行更进一步的解释。

所以,在谈解决可见性问题之前,我们需要聊聊可见性问题&Java内存模型。

什么是可见性问题呢?
  • 可见性,主要强调一个线程对某个共享变量进行更新之后,后续访问该共享变量的线程可能无法立即读取到更新后的最新结果,甚至永远也无法得知其他线程对该共享变量进行过修改操作。那么,这种问题,我们可以称其为:可见性问题。

    当然,可见性问题是也是线程安全性的表现形式之一。

    线程安全性的表现形式,即为:原子、有序、可见性。


什么是Java内存模型(JMM)?
  • 简单来说,Java内存模型是一组规范,这些规范告诉JVM如何解决原子性、有序性和可见性问题。

    我们知道Java的对象一般是放在堆内存中的,而堆内存是线程共享的,所以Java内存模型的影响范围一般只涉及堆内存。这里,JMM有一张比较形象的图1-1:

    企业微信20190903043012.png

我们可以看到,JMM处于线程和主内存中间,充当中介人的角色。

当线程和主内存只能通过JMM通信的时候,那么JMM就是唯一的主宰!

接下来是JMM的自我介绍:

大家好,我叫Java memory model(JMM)
为了更好的解决并发编程中的线程安全性问题,即保证并发中的原子性、有序性和可见性。
所以,我规定了以下规则。
规则一:每个线程都有独立的工作内存, 即工作内存(本地线程)
规则二:所有的共享变量都必须在主内存中, 且只能通过JMM进行控制访问。
规则三:所有的共享变量都必须在主内存中,每个线程都有自己的工作内存(本地内存),线程的所有操作都必须在工作内存中进行,而不能直接对主内存进行操作。
规则四:工作内存之间禁止互相访问。
复制代码

掌握绝对权力之后,那么JMM就可以制定可见性规范:

比如:当线程A想跟线程B通信的时候

  • 首先,线程A需要把自己本地内存中更新后的共享变量副本,刷新到主内存中。
  • 随后,线程B跳过读取本地内存,直接向主内存中读取共享变量的值,将主内存中读取的值放入自己的本地内存中。

从这里可以看出,JMM是通过控制主内存和每个线程的本地内存之间的交互,来达到可见性目的的。


众所周知,JMM是一组抽象的复杂的规范,那么如何把抽象的、复杂的规范变成现实可用的方法呢?

那么,Java到底是利用什么方法或措施,保证多线程环境下,共享变量是立即可见性的(锁&volatile&final)。
  • 手段一:synchronized或Lock——互斥锁

    在并发编程中,一旦使用互斥锁,一般能解决所有并发问题!

    所以,可见性可以使用互斥锁进行保障。

  • 手段二:volatile关键字

    在这里笔者并不讲volitale的底层实现原理,具体的底层实现细节笔者会单独写一篇博文来进行介绍。

    注意,一般被volitale关键字修饰的共享变量,具有两种语义(这里的语义,可以理解为潜规则)。

    • 语义一:保证可见性。即,保证线程对共享变量的修改,对其他线程是立即可见的。

      volatile关键字保证立即可见,其中有几点需要注意:

      其一,使用volatile关键字会强制将修改的值(共享变量)立即写/读入主内存。

      如何实现强制写入主内存?

      在图1-1中我们可以看到存在线程A和线程B。那么,当线程A对共享变量进行修改的时候,会导致线程B工作内存中的共享变量副本失效。

      如何实现强制读取主内存?

      一旦线程B工作内存中的共享变量副本失效,那么就必须重新从主内存中读取最新的值。

      当然,看到这里相信读者们已经看出来了,volitale的语义一是利用缓存一致性协议(MESI)来保证的。

    • 语义二:保证有序性。

      额..好像保证有序性在本篇博文,出现地有点不合时宜。

      反正,读者们只需要记住:

      volitale关键字利用禁止指令重排序和禁止编译优化,保证有序性。

  • 手段三:final关键字

    有final修饰的变量(基本类型)具有不可变性,当且仅有一次赋值,一旦赋值即不可变。

    不可变的变量或对象,我们可以称其为线程安全变量/对象。

    因为,final关键字修饰的变量是不可变的,在多线程环境中不管怎么操作,都是同一个值。

    所以,final关键字是保证可见性的手段之一。


总结

  • Java内存模型也称为内存一致性模型,是一些复杂规范的抽象集合,其中规定了工作内存和主内存的概念。

  • volitale关键字具有两种语义:保证可见性&有序性。

  • final关键字意味着不可变(基本数据类型byte, int…..),所以在多线程环境下是立即可见的。

  • synchronized、volitale、final三个关键字,可以看做是Java内存模型对可见性问题提出的解决方案。