如何使用 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 { // 创建两 ...
随机推荐
- MySQL之表操作
1 创建表 2 查看表结构 3 数据类型 4 表完整性约束 5 修改表 6 复制表 7 删除表 一创建表 语法: create table 表名( 字段名1 类型[(宽度) 约束条件], 字段名2 ...
- 表迁移工具的选型-复制ibd的方法
1.1. 场景 有的时候开放人员自己的库需要帮忙导一些数据,但是表的数据量又很大.虽然说使用mysqldump或mysqlpump也可以导.但是这耗时需要比较久. 记得之前建议开放人员可以直接使用na ...
- js 移动端上拉加载下一页通用方案
取页面三种高度 //取进度条到底部距离 var getScrollTop = function () { var scrollTop = 0; if (document.documentElement ...
- 使用Java内置的Http Server构建Web应用
一.概述 使用Java技术构建Web应用时, 我们通常离不开tomcat和jetty之类的servlet容器,这些Web服务器功能强大,性能强劲,深受欢迎,是运行大型Web应用的必备神器. 虽然Jav ...
- python编码问题和逻辑运算
1,回顾昨天课程及作业 #1.使用while循环输入 1 2 3 4 5 6 8 9 10 ''' count = 0 while count < 10: count += 1 # count ...
- Java DB 访问之 mybatis mapper xml 配置方式
1 项目说明 项目采用 maven 组织 ,jdbc 唯一的依赖就是 mysql-connector-java pom 依赖如下: mysql 数据连接 : mysql-connector-java ...
- Vijos P1113 不高兴的津津【模拟】
不高兴的津津 描述 津津上初中了.妈妈认为津津应该更加用功学习,所以津津除了上学之外,还要参加妈妈为她报名的各科复习班.另外每周妈妈还会送她去学习朗诵.舞蹈和钢琴.但是津津如果一天上课超过八个小时就会 ...
- linux中mv命令使用详解
mv命令是move的缩写,可以用来移动文件或者将文件改名(move (rename) files),是Linux系统下常用的命令,经常用来备份文件或者目录. 1.命令格式: mv [选项] 源文件或目 ...
- ubuntu sendmail配置发送邮件
ubuntu中sendmail函数可以很方便的发送邮件,ubuntu sendmail先要安装两个包. 必需安装的两个包: 代码 sudo apt-get install sendmail sudo ...
- c++(基数排序)
基数排序是另外一种比较有特色的排序方式,它是怎么排序的呢?我们可以按照下面的一组数字做出说明:12. 104. 13. 7. 9 (1)按个位数排序是12.13.104.7.9 (2)再根据十位排序1 ...