如何使用 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 { // 创建两 ...
随机推荐
- CentOS7下安装Docker-Compose
Docker-Compose是一个部署多个容器的简单但是非常必要的工具. 安装Docker-Compose之前,请先安装 python-pip 安装 python-pip 1.首先检查linux有没有 ...
- Java学习笔记14(面向对象七:final、static)
final:意为最终,不可变,是一个修饰词 有时候一个类地功能被开发好了,不想让子类重写,修改,这里就会用到final关键字 final修饰类: 不可以被继承,但是可以继承其他类 示例: public ...
- TypeScript体验
TypeScript 在线玩 http://www.typescriptlang.org/play/index.html ts最终编译成js 网站最终还是要引用js. ts面向对象的感念更加直观, ...
- select模型
在Windows中所有的socket函数都是阻塞类型的,也就是说只有网络中有特定的事件发生时才会返回,在没有发生事件时会一直等待,虽说我们将它们设置为非阻塞状态,但是在对于服务器段而言,肯定会一直等待 ...
- IdentityServer(15)- 第三方快速入门和示例
这些示例不由IdentityServer团队维护. IdentityServer团队提供链接到了社区示例,但不能对示例做任何保证. 如有问题,请直接与作者联系. 各种ASP.NET Core安全示例 ...
- EMMC与nand flash的区别【转】
1.NAND Flash 是一种存储介质,要在上面读写数据,外部要加主控和电路设计. 2.eMMC是NAND flash+主控IC ,对外的接口协议与SD.TF卡类似:对厂家而言简化了电路设计,降低了 ...
- 七牛php-sdk使用-文件上传
使用七牛进行文件上传可以有多种方式: 直接form表单上传,需要自己按照文档做配置 使用七牛jssdk,部署较简单,大文件分片上传 php-sdk后台上传 首先,所有的上传方法以及所有的跟七牛接口相关 ...
- Android基础_多媒体
一.MediaPlayer Android的MediaPlayer包含了Audio和video的播放功能,在Android的界面上,Music和Video两个应用程序都是调用MediaPlayer实现 ...
- 大数据Hadoop学习之搭建Hadoop平台(2.1)
关于大数据,一看就懂,一懂就懵. 一.简介 Hadoop的平台搭建,设置为三种搭建方式,第一种是"单节点安装",这种安装方式最为简单,但是并没有展示出Hadoop的技术优势,适合 ...
- python算法运算
>>> b = 10>>> b /= 8>>> b1.25>>> 10 // 81>>> **幂运算 > ...