如何使用 volatile, synchronized, final 进行线程间通信

6

你是否真正理解并会用volatile, synchronized, final进行线程间通信呢,如果你不能回答下面的几个问题,那就说明你并没有真正的理解:

  1. volatile变量的操作一定具有原子性吗?

  2. synchronized所谓的加锁,锁住的是什么?

  3. final定义的变量不变的到底是什么?

java内存模型

内存模型

看java内存模型之前,我们先来看看什么是内存模型?

在多处理器系统中,处理器通常有多级缓存,因为这些缓存离处理器更近并且可以存储一部分数据,所以缓存可以改善处理器获取数据的速度和减少对共享内存数据总线的占用。缓存虽然能极大的提高性能,但是同时也带来了诸多挑战。例如,当两个处理器同时操作同一个内存地址的时候,该如何处理?这两个处理器在什么条件下才能看到相同的值?

对于处理器而言,一个内存模型就是定义一些充分必要的规范,这些规范使得其他处理器对内存的写操作对当前处理器可见,或者当前处理器的写操作对其他处理器可见。

其他处理器对内存的写一定发生在当前处理器对同一内存的读之前,称之为其他处理器对内存的写对当前处理器可见。

Java 内存模型

知道了内存模型,那么应该可以更好的理解java内存模型。

简单的讲,java内存模型指的就是一套规范,现在最新的规范为JSR-133。这套规范包含:

  1. 线程之间如何通过内存通信

  2. 线程之间通过什么方式通信才合法,才能得到期望的结果

Java 内存模型中的内存结构

我们已经知道 java 内存模型就是一套规范,那么在这套规范中,规定的内存结构是什么样的呢?

简单的讲,Java 内存模型将内存分为共享内存和本地内存。共享内存又称为堆内存,指的就是线程之间共享的内存,包含所有的实例域、静态域和数组元素。每个线程都有一个私有的,只对自己可见的内存,称之为本地内存。

java内存模型中的内存结构如下图所示:
Java 内存模型中的内存结构

共享内存中共享变量虽然由所有的线程共享,但是为了提高效率,线程并不直接使用这些变量,每个线程都会在自己的本地内存中存储一个共享内存的副本,使用这个副本参与运算。由于这个副本的参与,导致了线程之间对共享内存的读写存在可见性问题。

为了方便线程之间的通信,java 提供了 volatile, synchronized, final 三个关键字供我们使用,下面我们来看看如何使用它们进行线程间通信

volatile

volatile 定义的变量,特殊性在于:

一个线程对 volatile 变量的写一定对之后对这个变量的读的线程可见。

等价于

一个线程对 volatile 变量的读一定能看见在它之前最后一个线程对这个变量的写。

为了实现这些语义,Java 规定,(1)当一个线程要使用共享内存中的 volatile 变量时,如图中的变量a,它会直接从主内存中读取,而不使用自己本地内存中的副本。(2)当一个线程对一个 volatile 变量进行写时,它会将这个共享变量的值刷新到共享内存中。

我们可以看到,其实 volatile 变量保证的是一个线程对它的写会立即刷新到主内存中,并置其它线程的副本为无效,它并不保证对 volatile 变量的操作都是具有原子性的。

由于

 public void add(){
     a++;         #1
 } 

等价于

 public void add() {
    temp = a;        
    temp = temp +1;  
    a = temp;         
 } 

代码1并不是一个原子操作,所以类似于 a++ 这样的操作会导致并发数据问题。

volatile 变量的写可以被之后其他线程的读看到,因此我们可以利用它进行线程间的通信。如

volatile int a;

public void set(int b) {
    a = b; 
}

public void get() {
    int i = a; 
}

线程A执行set()后,线程B执行get(),相当于线程A向线程B发送了消息。

synchronized

如果我们非要使用 a++ 这种复合操作进行线程间通信呢?java 为我们提供了synchronized。

 public synchronized void add() {
    a++; 
 }

synchronized 使得

它作用范围内的代码对于不同线程是互斥的,并且线程在释放锁的时候会将共享变量的值刷新到共享内存中。

我们可以利用这种互斥性来进行线程间通信。看下面的代码,

public synchronized void add() {
    a++; 
}

public synchronized void get() {
    int i = a; 
}

当线程A执行 add(),线程B调用get(),由于互斥性,线程A执行完add()后,线程B才能开始执行get(),并且线程A执行完add(),释放锁的时候,会将a的值刷新到共享内存中。因此线程B拿到的a的值是线程A更新之后的。

volatile 和 synchronized比较

根据以上的分析,我们可以发现volatile和synchronized有些相似。

  1. 当线程对 volatile变量写时,java 会把值刷新到共享内存中;而对于synchronized,指的是当线程释放锁的时候,会将共享变量的值刷新到主内存中。

  2. 线程读取volatile变量时,会将本地内存中的共享变量置为无效;对于synchronized来说,当线程获取锁时,会将当前线程本地内存中的共享变量置为无效。

  3. synchronized 扩大了可见影响的范围,扩大到了synchronized作用的代码块。

final 变量

final关键字可以作用于变量、方法和类,我们这里只看final 变量。

final变量的特殊之处在于,

final 变量一经初始化,就不能改变其值。

这里的值对于一个对象或者数组来说指的是这个对象或者数组的引用地址。因此,一个线程定义了一个final变量之后,其他任意线程都拿到这个变量。但有一点需要注意的是,当这个final变量为对象或者数组时,

  1. 虽然我们不能讲这个变量赋值为其他对象或者数组,但是我们可以改变对象的域或者数组中的元素。

  2. 线程对这个对象变量的域或者数据的元素的改变不具有线程可见性。

总结

有时候,我们在学习一门技术过程中,并不能仅仅局限于怎么用,知道怎么用之后,我们应该深入的探究一下,为什么这么用之后就能得到我们想要的结果呢?既要知其然,更要知其所以然。

你可能感兴趣的

bauerctu · 2016年11月24日

“线程对这个对象变量的域或者数据的元素的改变不具有线程可见性。” 怎么理解?

回复

tsogeorge · 2016年12月29日

共享内存中共享变量虽然由所有的线程共享,但是为了提高效率,线程并不直接使用这些变量,每个线程都会在自己的本地内存中存储一个共享内存的副本,使用这个副本参与运算。
这句话是扯鸡巴蛋吧?

回复

载入中...