Java 多线程之 synchronized 和 volatile 的比較
概述
在做多线程并发处理时,常常须要对资源进行可见性訪问和相互排斥同步操作。有时候,我们可能从前辈那里得知我们须要对资源进行 volatile 或是 synchronized 关键字修饰处理。但是,我们却不知道这两者之间的差别。我们无法分辨在什么时候应该使用哪一个关键字。
本文就针对这个问题,展开讨论。
版权说明
著作权归作者全部。
商业转载请联系作者获得授权,非商业转载请注明出处。
本文作者:Coding-Naga
发表日期: 2016年4月5日
本文链接:http://blog.csdn.net/lemon_tree12138/article/details/51062421
来源:CSDN
很多其它内容:分类 >> 并发与多线程
内存语义分析
happens-before 模型简单介绍
假设你单从字面上的意思来理解 happens-before 模型,你可能会认为这是在说某一个操作在还有一个操作之前运行。只是。学习完 happens-before 之后,你就不会还这样理解了。以下是《Java 并发编程的艺术》书上对 happens-before 的定义:
在 JMM(Java Memory Model) 中。假设一个操作运行的结果须要对还有一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既能够在一个线程之内,也能够是在不同的线程之间。
volatile 的内存语义
对于多线程编程来说,每一个线程是能够拥有共享内存中变量的一个拷贝。这一点在后面还是会讲到,这里就不作过多说明。
假设一个变量被 volatile 关键字修饰时,那么对这的变量的写是将本地内存中的拷贝刷新到共享内存中;对这个变量的读会有一些不同,读的时候是无视他的本地内存的拷贝的。仅仅是从共享变量中去读取数据。
synchronized 的内存语义
我们说 synchronized 实际上是对变量进行加锁处理。
那么无论是读也好,写也好都是基于对这个变量的加锁操作。假设一个变量被 synchronized 关键字修饰,那么对这的变量的写是将本地内存中的拷贝刷新到共享内存中;对这个变量的读就是将共享内存中的值刷新到本地内存,再从本地内存中读取数据。由于全过程中变量是加锁的。其它线程无法对这个变量进行读写操作。所以能够理解成对这个变量的不论什么操作具有原子性。即线程是安全的。
实例论证
上面的一些说明或是定义可能会有一些乏味枯燥,也不太好理解。这里我们就列举一些样例来说明,这样比較详细和形象一些。
volatile 可见性測试
RunThread.java
public class RunThread extends Thread {
private boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void setRunFlag(boolean flag) {
isRunning = flag;
}
@Override
public void run() {
System.out.println("I'm come in...");
boolean first = true;
while(isRunning) {
if (first) {
System.out.println("I'm in while...");
first = false;
}
}
System.out.println("I'll go out.");
}
}
MyRun.java
public class MyRun {
public static void main(String[] args) throws InterruptedException {
RunThread thread = new RunThread();
thread.start();
Thread.sleep(100);
thread.setRunFlag(false);
System.out.println("flag is reseted: " + thread.isRunning());
}
}
对于上面的样例仅仅是一个非常普通的多线程操作,这里我们非常easy就得到了 RunThread 线程在 while 中进入了死循环。
我们能够在 main() 方法里看到一句 Thread.sleep(100) ,结合前面说到的 happens-before 内存模型,可知以下的 thread.setRunFlag(false) 并不会 happens-before 子线程中的 while 。
这样一来,尽管主线程中对 isRunning 进行了改动。然而对子线程中的 while 来说。并没有改变,所以这就会引发在 while 中的死循环。
在这样的情况下,线程工作时的内存模型像以下这样
在这里。可能你会奇怪。为什么会有两个“内存块”?这是出于多线程的性能考虑的。
尽管对象以及成员变量分配的内存是在共享内存中的,只是对于每一个线程而言,还是能够拥有这个对象的拷贝,这样做的目的是为了加快程序的运行,这也是现代多核处理器的一个显著特征。从上面的内存模型能够看出。Java的线程是直接与它自身的工作内存(本地内存)交互,工作内存再与共享内存交互。这样就形成了一个非原子的操作。在Java里多线程的环境下非原子的操作是非常危急的。这个我们都已经知道了,由于这可能会被异步的读写操作所破坏。
这里工作内存被 while 占用,无法去更新主线程对共享内存 isRunning 变量的改动。
所以。假设我们想要打破这样的限制。能够通过 volatile 关键字来处理。通过 volatile 关键字修饰 while 的条件变量,即 isRunning。
就像以下这样改动 RunThread.java 代码:
private volatile boolean isRunning = true;
这样一来, volatile 改动了 isRunning 的可见性,使得主线程的 thread.setRunFlag(false) 将会 happens-before 子线程中的 while 。
终于。使得子线程从 while 的循环中跳出,问题解决。
以下我们来看看 volatile 是怎样改动了 isRunning 的可见性的吧。
这里,由于 isRunning 被 volatile 修饰,那么当子线程想要訪问工作内存中的 inRunning 时,被强制地直接从共享内存中获取。而共享内存中的 isRunning 被主线程改动过了,已经被改动成了 false 。while 被打破。这样子线程就从 while 的循环中跳出来了。
volatile 原子性測试
volatile 确实有非常多长处,但是它却有一个致命的缺点。那就是 volatile 并非原子操作。
也就是在多线程的情况。仍然是不安全的。
可能,这个时候你会发问说。既然 volatile 保证了它在线程间的可见性。那么在什么时候改动它,怎么改动它。对于其它线程是可见的,某一个线程读到的都会是改动过的值,为什么还要说它还是不安全的呢?
我们通过一个样例来说明吧。这样更形象一些。大家看以下这样一段代码:
public class DemoNoProtected {
static class MyThread extends Thread {
static int count = 0;
private static void addCount() {
for (int i = 0; i < 100; i++) {
count++;
}
System.out.println("count = " + count);
}
@Override
public void run() {
addCount();
}
}
public static void main(String[] args) {
MyThread[] threads = new MyThread[100];
for (int i = 0; i < 100; i++) {
threads[i] = new MyThread();
}
for (int i = 0; i < 100; i++) {
threads[i].start();
}
}
}
count = 300
count = 300
count = 300
count = 400
... ...
count = 7618
count = 7518
count = 9918
这是一个未经不论什么处理的,非常直白的过程。但是它的结果,也非常直白。事实上这个结果并不让人意外,从我们学习Java的时候,就知道Java的多线程并不安全。
是不是从上面的学习中,你感觉这个能够通过 volatile 关键字解决?既然你这么说,那么我们就来试一试。给 count 变量加入 volatile 关键字。例如以下:
public class DemoVolatile {
static class MyThread extends Thread {
static volatile int count = 0;
... ...
}
public static void main(String[] args) {
... ...
}
}
count = 100
count = 300
count = 400
count = 200
... ...
count = 9852
count = 9752
count = 9652
... ...
count = 8154
count = 8054
不知道这个结果是不是会让你感觉到意外。
对于 count 的混乱的数字倒是好理解一些,应该多个线程同一时候改动时就发生这样的事情。但是我们在结果为根本找不到逻辑上的最大值“10000”。这就有一些奇怪了。
由于从逻辑上来说, volatile改动了 count 的可见性。对于线程 A 来说。它是可见线程 B 对 count 的改动的。
仅仅是从结果中并没有体现这一点。
我们说,volatile并没有保证线程安全。在上面子线程中的 addCount() 方法里,运行的是 count++ 这样一句代码。而像 count++ 这样一句代码从学习Java变量自增的第一堂课上,老师就应该强调过它的运行过程。count++ 能够类比成以下的过程:
int tmp = count;
tmp = tmp + 1;
count = tmp;
可见,count++ 并非原子操作。
不论什么两个线程都有可能将上面的代码分离进行,安全性便无从谈起了。
所以,到这里我们知道了 volatile 能够改变变量在线程之间的可见性,却不能改变线程之间的同步。而同步操作则须要其它的操作来保证。
synchronized 同步測试
上面说到 volatile 不能解决线程的安全性问题,这是由于 volatile 不能构建原子操作。而在多线程编程中有一个非常方便的同步处理。就是 synchronized 关键字。以下来看看 synchronized 是怎样处理多线程同步的吧。代码例如以下:
public class DemoSynchronized {
static class MyThread extends Thread {
static int count = 0;
private synchronized static void addCount() {
for (int i = 0; i < 100; i++) {
count++;
}
System.out.println("count = " + count);
}
@Override
public void run() {
addCount();
}
}
public static void main(String[] args) {
MyThread[] threads = new MyThread[100];
for (int i = 0; i < 100; i++) {
threads[i] = new MyThread();
}
for (int i = 0; i < 100; i++) {
threads[i].start();
}
}
}
count = 100
count = 200
count = 300
... ...
count = 9800
count = 9900
count = 10000
通过 synchronized 我们能够非常easy就获得了理想的结果。
而关于 synchronized 关键字的内存模型能够这样来表示:
某一个线程在訪问一个被 synchronized 修饰的变量时,会对此变量的共享内存进行加锁,那么这个时候其它线程对其的訪问就会被相互排斥。
synchronized 的内部实现事实上也是锁的概念。
Ref
- 《Java多线程编程核心技术》
- 《Java并发编程的艺术》
Java 多线程之 synchronized 和 volatile 的比較的更多相关文章
- Java多线程同步方法Synchronized和volatile
11 同步方法 synchronized – 同时解决了有序性.可见性问题 volatile – 结果可见性问题 12 同步- synchronized synchronized可以在任意对象上加 ...
- 【java多线程】synchronized和volatile
文章目录 一.synchronized 1.synchronized使用的方法 2.注意 3.不要以字符串作为锁的对象 4.`synchronized`锁的是什么? 二.volatile 1.引出问题 ...
- java线程安全— synchronized和volatile
java线程安全— synchronized和volatile package threadsafe; public class TranditionalThreadSynchronized { pu ...
- Java多线程-同步:synchronized 和线程通信:生产者消费者模式
大家伙周末愉快,小乐又来给大家献上技术大餐.上次是说到了Java多线程的创建和状态|乐字节,接下来,我们再来接着说Java多线程-同步:synchronized 和线程通信:生产者消费者模式. 一.同 ...
- Java多线程:synchronized的可重入性
从Java多线程:线程间通信之volatile与sychronized这篇文章中我们了解了synchronized的基本特性,知道了一旦有一个线程访问某个对象的synchronized修饰的方法或代码 ...
- Java多线程编程那些事:volatile解惑--转
http://www.infoq.com/cn/articles/java-multi-thread-volatile/ 1. 前言 volatile关键字可能是Java开发人员“熟悉而又陌生”的一个 ...
- JAVA多线程学习- 三:volatile关键字
Java的volatile关键字在JDK源码中经常出现,但是对它的认识只是停留在共享变量上,今天来谈谈volatile关键字. volatile,从字面上说是易变的.不稳定的,事实上,也确实如此,这个 ...
- Java多线程同步 synchronized 关键字的使用
代表这个方法加锁,相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B(或者C D等),有的话要等正在使用这个方法的线程B(或者C D)运行完这个方法后再运行此线程A, ...
- java 多线程8 : synchronized锁机制 之 方法锁
脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量或者全局静态变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数 ...
随机推荐
- 【 Linux 】为lnmp架构添加memcached支持
一.首先搭建lnmp平台,这里不再演示.通过php页面来进行测试如下: [root@node1 ~]# vim /usr/local/nginx/html/info.php <?php $lin ...
- 【 Linux 】Keepalived实现双主模型高可用集群
要求: 1. 两台web服务器安装wordpress,数据库通过nfs共享 2. 使用keepalived实现双主模型 环境: 主机: 系统:CentOS6.7 x64 ...
- krpano--控制热点跳转到场景的指定视角
krpano课堂(肥宗) · 2015-07-13 19:32 有这么一种情况,假设我们用三个场景,这三个场景恰好是一条街道的同一方向的三个拍摄点.如上图. 我们可以通过设置A.B.C三个场景中的vi ...
- magento smtp设置
我安装的版本是ASchroder_SMTPPro-2.0.6.tgz 然后测试 但是没成功,会有报错,报错: 提示没有默认模板,原因是该插件没有带模板,所有会有这样的提示.
- HDU 6322.Problem D. Euler Function -欧拉函数水题(假的数论题 ̄▽ ̄) (2018 Multi-University Training Contest 3 1004)
6322.Problem D. Euler Function 题意就是找欧拉函数为合数的第n个数是什么. 欧拉函数从1到50打个表,发现规律,然后勇敢的水一下就过了. 官方题解: 代码: //1004 ...
- Codeforces Round #283 (Div. 2) D. Tennis Game(模拟)
D. Tennis Game time limit per test 2 seconds memory limit per test 256 megabytes input standard inpu ...
- CentOS7安装bind域名服务
安装Bind Chroot DNS 服务器 yum install bind-chroot bind -y 拷贝bind相关文件,准备bind chroot 环境 cp -R /usr/share/d ...
- 【数论】【欧拉函数】bzoj2190 [SDOI2008]仪仗队
由图可知,一个人无法被看到时,当且仅当有 人与原点 的斜率与他相同,且在他之前. ∴一个人可以被看到,设其斜率为y/x,当且仅当y/x不可再约分,即gcd(x,y)=1. 考虑将图按对角线划分开,两部 ...
- Bootstrap-datetimepicker日期插件简单使用
写在前面: 日期组件有很多,这里简单的记录下bootstrap的一个日期插件datetimepicker,使用方法比较简单,基本上看一些就会了,但是还是记录下. 这个就不过多的说了,简单粗暴上代码 & ...
- 《深入理解Spark-核心思想与源码分析》(一)总体规划和第一章环境准备
<深入理解Spark 核心思想与源码分析> 耿嘉安著 本书共计486页,计划每天读书20页,计划25天完成. 2018-12-20 1-20页 凡事豫则立,不豫则废:言前定,则不跲:事 ...