原文地址:https://segmentfault.com/a/1190000004487149。感谢作者的无私分享。

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

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

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

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

java内存模型

内存模型

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

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

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

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

Java 内存模型

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

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

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

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

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

总结

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

如何使用 volatile, synchronized, final 进行线程间通信的更多相关文章

  1. Java多线程:线程间通信之volatile与sychronized

    由前文Java内存模型我们熟悉了Java的内存工作模式和线程间的交互规范,本篇从应用层面讲解Java线程间通信. Java为线程间通信提供了三个相关的关键字volatile, synchronized ...

  2. volatile关键字与线程间通信

    >>Java内存模型 现在计算机普遍使用多处理器进行运算,并且为了解决计算机存储设备和处理器的运算速度之间巨大的差距,引入了高速缓存作为缓冲,缓存虽然能极大的提高性能,但是随之带来的缓存一 ...

  3. Java多线程编程核心技术---线程间通信(二)

    通过管道进行线程间通信:字节流 Java提供了各种各样的输入/输出流Stream可以很方便地对数据进行操作,其中管道流(pipeStream)是一种特殊的流,用于在不同线程间直接传送数据,一个线程发送 ...

  4. Java多线程(二) —— 线程安全、线程同步、线程间通信(含面试题集)

    一.线程安全 多个线程在执行同一段代码的时候,每次的执行结果和单线程执行的结果都是一样的,不存在执行结果的二义性,就可以称作是线程安全的. 讲到线程安全问题,其实是指多线程环境下对共享资源的访问可能会 ...

  5. Java多线程:线程间通信之Lock

    Java 5 之后,Java在内置关键字sychronized的基础上又增加了一个新的处理锁的方式,Lock类. 由于在Java线程间通信:volatile与sychronized中,我们已经详细的了 ...

  6. Java多线程编程(三)线程间通信

    线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体.线程间的通信就是成为整体的必用方案之一,可以说,使线程间进行通信后,系统之间的交互性会更强大,在大大提高CPU利用率的同时 ...

  7. java线程基础巩固---线程间通信快速入门,使用wait和notify进行线程间的数据通信

    之前已经对于线程同步相关的知识点进行了详细的学习,这次来学习一下线程间的通信相关的知识,话不多说直接用代码进行演练,以一个简陋的生产者消费者模型来初步了解下线程间通信是怎么一回事. 生产消费者第一版: ...

  8. 0038 Java学习笔记-多线程-传统线程间通信、Condition、阻塞队列、《疯狂Java讲义 第三版》进程间通信示例代码存在的一个问题

    调用同步锁的wait().notify().notifyAll()进行线程通信 看这个经典的存取款问题,要求两个线程存款,两个线程取款,账户里有余额的时候只能取款,没余额的时候只能存款,存取款金额相同 ...

  9. java多线程系列5-死锁与线程间通信

    这篇文章介绍java死锁机制和线程间通信 死锁 死锁:两个或两个以上的线程在争夺资源的过程中,发生的一种相互等待的现象. 同步代码块的嵌套案例 public class MyLock { // 创建两 ...

随机推荐

  1. 浅谈 URI 及其转义

    URI URI,全称是 Uniform Resource Identifiers,即统一资源标识符,用于在互联网上标识一个资源,比如 https://www.upyun.com/products/cd ...

  2. KMP算法讲解

    老规矩,讲算法前,先说一道小问题吧 给你一个长串和短串,求短串在长串中出现的次数和位置. 设长串长度为len1,短串长度为len2. 如果len1*len2<=108,那就很简单了,直接暴力枚举 ...

  3. 【后缀数组】洛谷P3809模板题

    题目背景 这是一道模板题. 题目描述 读入一个长度为 n n n 的由大小写英文字母或数字组成的字符串,请把这个字符串的所有非空后缀按字典序从小到大排序,然后按顺序输出后缀的第一个字符在原串中的位置. ...

  4. PE文件详解(九)

    本篇文章转载自小甲鱼的一篇日志,原文地址 我们知道,Windows 将程序的各种界面定义为资源,包括加速键(Accelerator).位图(Bitmap).光标(Cursor).对话框(Dialog ...

  5. squashfs文件系统

    一.Squashfs文件系统简介 squashfs是以linux 内核源码补丁的形式发布,附带mksquashfs工具,用于创建squash文件系统.squashfs可以将整个文件系统或者某个单一的目 ...

  6. ubuntu 使用sudo apt-get update 出现 被配置多次导致无法升级错误解决方法

    这个周六周末在考虑升级自己GPU开发机,在琢磨使用docker来配置tensorflowGPU环境,在升级软件的时候爆出了如下错误 在 /etc/apt/sources.list.d/sogoupin ...

  7. mybatis foreach批量插入数据:Oracle与MySQL区别

    mybatis foreach批量插入数据:Oracle与MySQL不同点: 主要不同点在于foreach标签内separator属性的设置问题: separator设置为","分 ...

  8. 学习笔记-nodejs报错

    端口被占用 解决办法:打开控制管理器关掉node.exe进程 如果找不到可以使用命令行:netstat -ano | findstr 8000 找到进程的pid,然后到控制管理器找到进程杀掉. net ...

  9. c语言基础学习01

    =============================================================================每一种语言都有其独特的语法规则与语言定义. 指 ...

  10. 【LeetCode】476. Number Complement (java实现)

    原题链接 https://leetcode.com/problems/number-complement/ 原题 Given a positive integer, output its comple ...