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

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

  1. volatile变量的操做必定具备原子性吗?数组

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

  3. final定义的变量不变的究竟是什么?并发

java内存模型

内存模型

看java内存模型以前,咱们先来看看什么是内存模型?性能

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

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

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

Java 内存模型

知道了内存模型,那么应该能够更好的理解java内存模型。线程

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

  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. 线程对这个对象变量的域或者数据的元素的改变不具备线程可见性。

总结

有时候,咱们在学习一门技术过程当中,并不能仅仅局限于怎么用,知道怎么用以后,咱们应该深刻的探究一下,为何这么用以后就能获得咱们想要的结果呢?既要知其然,更要知其因此然。

相关文章
相关标签/搜索