如何使用 volatile, synchronized, final 进行线程间通信
原文地址:https://segmentfault.com/a/1190000004487149。感谢作者的无私分享。
你是否真正理解并会用volatile, synchronized, final
进行线程间通信呢,如果你不能回答下面的几个问题,那就说明你并没有真正的理解:
对
volatile
变量的操作一定具有原子性吗?synchronized
所谓的加锁,锁住的是什么?final
定义的变量不变的到底是什么?
java内存模型
内存模型
看java内存模型之前,我们先来看看什么是内存模型?
在多处理器系统中,处理器通常有多级缓存,因为这些缓存离处理器更近并且可以存储一部分数据,所以缓存可以改善处理器获取数据的速度和减少对共享内存数据总线的占用。缓存虽然能极大的提高性能,但是同时也带来了诸多挑战。例如,当两个处理器同时操作同一个内存地址的时候,该如何处理?这两个处理器在什么条件下才能看到相同的值?
对于处理器而言,一个内存模型就是定义一些充分必要的规范,这些规范使得其他处理器对内存的写操作对当前处理器可见,或者当前处理器的写操作对其他处理器可见。
其他处理器对内存的写一定发生在当前处理器对同一内存的读之前,称之为其他处理器对内存的写对当前处理器可见。
Java 内存模型
知道了内存模型,那么应该可以更好的理解java内存模型。
简单的讲,java内存模型指的就是一套规范,现在最新的规范为JSR-133。这套规范包含:
线程之间如何通过内存通信;
线程之间通过什么方式通信才合法,才能得到期望的结果。
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有些相似。
当线程对 volatile变量写时,java 会把值刷新到共享内存中;而对于synchronized,指的是当线程释放锁的时候,会将共享变量的值刷新到主内存中。
线程读取volatile变量时,会将本地内存中的共享变量置为无效;对于synchronized来说,当线程获取锁时,会将当前线程本地内存中的共享变量置为无效。
synchronized 扩大了可见影响的范围,扩大到了synchronized作用的代码块。
final 变量
final关键字可以作用于变量、方法和类,我们这里只看final 变量。
final变量的特殊之处在于,
final 变量一经初始化,就不能改变其值。
这里的值对于一个对象或者数组来说指的是这个对象或者数组的引用地址。因此,一个线程定义了一个final变量之后,其他任意线程都拿到这个变量。但有一点需要注意的是,当这个final变量为对象或者数组时,
虽然我们不能讲这个变量赋值为其他对象或者数组,但是我们可以改变对象的域或者数组中的元素。
线程对这个对象变量的域或者数据的元素的改变不具有线程可见性。
总结
有时候,我们在学习一门技术过程中,并不能仅仅局限于怎么用,知道怎么用之后,我们应该深入的探究一下,为什么这么用之后就能得到我们想要的结果呢?既要知其然,更要知其所以然。
如何使用 volatile, synchronized, final 进行线程间通信的更多相关文章
- Java多线程:线程间通信之volatile与sychronized
由前文Java内存模型我们熟悉了Java的内存工作模式和线程间的交互规范,本篇从应用层面讲解Java线程间通信. Java为线程间通信提供了三个相关的关键字volatile, synchronized ...
- volatile关键字与线程间通信
>>Java内存模型 现在计算机普遍使用多处理器进行运算,并且为了解决计算机存储设备和处理器的运算速度之间巨大的差距,引入了高速缓存作为缓冲,缓存虽然能极大的提高性能,但是随之带来的缓存一 ...
- Java多线程编程核心技术---线程间通信(二)
通过管道进行线程间通信:字节流 Java提供了各种各样的输入/输出流Stream可以很方便地对数据进行操作,其中管道流(pipeStream)是一种特殊的流,用于在不同线程间直接传送数据,一个线程发送 ...
- Java多线程(二) —— 线程安全、线程同步、线程间通信(含面试题集)
一.线程安全 多个线程在执行同一段代码的时候,每次的执行结果和单线程执行的结果都是一样的,不存在执行结果的二义性,就可以称作是线程安全的. 讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会 ...
- Java多线程:线程间通信之Lock
Java 5 之后,Java在内置关键字sychronized的基础上又增加了一个新的处理锁的方式,Lock类. 由于在Java线程间通信:volatile与sychronized中,我们已经详细的了 ...
- Java多线程编程(三)线程间通信
线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体.线程间的通信就是成为整体的必用方案之一,可以说,使线程间进行通信后,系统之间的交互性会更强大,在大大提高CPU利用率的同时 ...
- java线程基础巩固---线程间通信快速入门,使用wait和notify进行线程间的数据通信
之前已经对于线程同步相关的知识点进行了详细的学习,这次来学习一下线程间的通信相关的知识,话不多说直接用代码进行演练,以一个简陋的生产者消费者模型来初步了解下线程间通信是怎么一回事. 生产消费者第一版: ...
- 0038 Java学习笔记-多线程-传统线程间通信、Condition、阻塞队列、《疯狂Java讲义 第三版》进程间通信示例代码存在的一个问题
调用同步锁的wait().notify().notifyAll()进行线程通信 看这个经典的存取款问题,要求两个线程存款,两个线程取款,账户里有余额的时候只能取款,没余额的时候只能存款,存取款金额相同 ...
- java多线程系列5-死锁与线程间通信
这篇文章介绍java死锁机制和线程间通信 死锁 死锁:两个或两个以上的线程在争夺资源的过程中,发生的一种相互等待的现象. 同步代码块的嵌套案例 public class MyLock { // 创建两 ...
随机推荐
- Python(三)字典的增删改查和遍历
一.增加
- TabHost用法
tabhost用两种方法 方法一:Activity继承TabActivity后用getTabHost()方法来获取tabhost(前提:Activity的setContentView要删除,这样布局才 ...
- PE文件详解(五)
在前面几节中经常提到相对虚拟地址RVA,在这篇博客中主要说明这个概念.本来是想接着转载小甲鱼的,但是我自己根据这篇文章和他的视频来学习的时候,发现在RVA与文件的相对偏移地址进行转化的时候,那块我看不 ...
- windows 多任务与进程
多任务,进程与线程的简单说明 多任务的本质就是并行计算,它能够利用至少2处理器相互协调,同时计算同一个任务的不同部分,从而提高求解速度,或者求解单机无法求解的大规模问题.以前的分布式计算正是利用这点, ...
- 解决mariadb grant ERROR 1045 (28000): Access denied for user
下面我们一起来看一篇解决mariadb grant ERROR 1045 (28000): Access denied for user问题,希望文章能够帮助到各位朋友. 用mariadb也有一段 ...
- Python3 决策树
# -*- coding: utf-8 -*-"""Created on Fri Dec 29 10:18:04 2017 @author: markli"&q ...
- Zabbix实战-简易教程--正则(Regxp)
一.正则表达式(Regx) 1.概述 正则表达式概念就不解释了.请参考:https://en.wikipedia.org/wiki/Regular_expression#POSIX_extended ...
- Java string和各种格式互转 string转int int转string
Java string和各种格式互转 string转int int转string 简单收集记录下 其他类型转String String s = String.valueOf( value); // 其 ...
- LABjs、RequireJS、SeaJS 哪个最好用?为什么?- 玉伯的回答
LABjs 的核心是 LAB(Loading and Blocking):Loading 指异步并行加载,Blocking 是指同步等待执行.LABjs 通过优雅的语法(script 和 wait)实 ...
- Prim最小生成树板子
普里姆算法可以称为"加点法",每次迭代选择代价最小的边对应的点,加入到最小生成树中.算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点. 邻接矩阵存图 时间复杂度O(n^2 ...