原文出处:http://hukai.me/android-training-course-in-chinese/performance/smp/index.html

Java中的”synchronized”与”volatile”关键字

“synchronized”关键字提供了Java一种内置的锁机制。每一个对象都有一个相对应的“monitor”,这个监听器可以提供互斥的访问。

“synchronized”代码段的实现机制与自旋锁(spin lock)有着相同的基础结构: 他们都是从获取到CAS开始,以释放CAS结束。这意味着编译器(compilers)与代码优化器(code optimizers)可以轻松的迁移代码到“synchronized”代码段中。一个实践结果是:你不能判定synchronized代码段是执行在这段代码下面一部分的前面,还是这段代码上面一部分的后面。更进一步,如果一个方法有两个synchronized代码段并且锁住的是同一个对象,那么在这两个操作的中间代码都无法被其他的线程所检测到,编译器可能会执行“锁粗化lock coarsening”并且把这两者绑定到同一个代码块上。

另外一个相关的关键字是“volatile”。在Java 1.4以及之前的文档中是这样定义的:volatile声明和对应的C语言中的一样可不靠。从Java 1.5开始,提供了更有力的保障,甚至和synchronization一样具备强同步的机制。

volatile的访问效果可以用下面这个例子来说明。如果线程1给volatile字段做了赋值操作,线程2紧接着读取那个字段的值,那么线程2是被确保能够查看到之前线程1的任何写操作。更通常的情况是,任何线程对那个字段的写操作对于线程2来说都是可见的。实际上,写volatile就像是释放监听器,读volatile就像是获取监听器。

非volatile的访问有可能因为照顾volatile的访问而需要做顺序的调整。例如编译器可能会往上移动一个非volatile加载操作,但是不会往下移动。Volatile之间的访问不会因为彼此而做出顺序的调整。虚拟机会注意处理如何的内存栅栏(memory barriers)。

当加载与保存大多数的基础数据类型,他们都是原子的atomic, 对于long以及double类型的数据则不具备原子型,除非他们被声明为volatile。即使是在单核处理器上,并发多线程更新非volatile字段值也还是不确定的。

Examples

下面是一个错误实现的单步计数器(monotonic counter)的示例: (Java theory and practice: Managing volatility).

class Counter {private int mValue;public int get() {return mValue;}public void incr() {mValue++;}
}

假设get()与incr()方法是被多线程调用的。然后我们想确保当get()方法被调用时,每一个线程都能够看到当前的数量。最引人注目的问题是mValue++实际上包含了下面三个操作。

reg = mValue
reg = reg + 1
mValue = reg

如果两个线程同时在执行incr()方法,其中的一个更新操作会丢失。为了确保正确的执行++的操作,我们需要把incr()方法声明为“synchronized”。这样修改之后,这段代码才能够在单核多线程的环境中正确的执行。

然而,在SMP的系统下还是会执行失败。不同的线程通过get()方法获取到得值可能是不一样的。因为我们是使用通常的加载方式来读取这个值的。我们可以通过声明get()方法为synchronized的方式来修正这个错误。通过这些修改,这样的代码才是正确的了。

不幸的是,我们有介绍过有可能发生的锁竞争(lock contention),这有可能会伤害到程序的性能。除了声明get()方法为synchronized之外,我们可以声明mValue为“volatile”. (请注意incr()必须使用synchronize) 现在我们知道volatile的mValue的写操作对于后续的读操作都是可见的。incr()将会稍稍有点变慢,但是get()方法将会变得更加快速。因此读操作多于写操作时,这会是一个比较好的方案。(请参考AtomicInteger.)

下面是另外一个示例,和之前的C示例有点类似:

class MyGoodies {public int x, y;
}
class MyClass {static MyGoodies sGoodies;void initGoodies() {    // runs in thread 1MyGoodies goods = new MyGoodies();goods.x = 5;goods.y = 10;sGoodies = goods;}void useGoodies() {    // runs in thread 2if (sGoodies != null) {int i = sGoodies.x;    // could be 5 or 0....}}
}

这段代码同样存在着问题,sGoodies = goods的赋值操作有可能在goods成员变量赋值之前被察觉到。如果你使用volatile声明sGoodies变量,你可以认为load操作为atomic_acquire_load(),并且把store操作认为是atomic_release_store()。

(请注意仅仅是sGoodies的引用本身为volatile,访问它的内部字段并不是这样的。赋值语句z = sGoodies.x会执行一个volatile load MyClass.sGoodies的操作,其后会伴随一个non-volatile的load操作::sGoodies.x。如果你设置了一个本地引用MyGoodies localGoods = sGoodies, z = localGoods.x,这将不会执行任何volatile loads.)

另外一个在Java程序中更加常用的范式就是臭名昭著的“double-checked locking”:

class MyClass {private Helper helper = null;public Helper getHelper() {if (helper == null) {synchronized (this) {if (helper == null) {helper = new Helper();}}}return helper;}
}

上面的写法是为了获得一个MyClass的单例。我们只需要创建一次这个实例,通过getHelper()这个方法。为了避免两个线程会同时创建这个实例。我们需要对创建的操作加synchronize机制。然而,我们不想要为了每次执行这段代码的时候都为“synchronized”付出额外的代价,因此我们仅仅在helper对象为空的时候加锁。

在单核系统上,这是不能正常工作的。JIT编译器会破坏这件事情。请查看4)Appendix的“‘Double Checked Locking is Broken’ Declaration”获取更多的信息, 或者是Josh Bloch’s Effective Java书中的Item 71 (“Use lazy initialization judiciously”)。

在SMP系统上执行这段代码,引入了一个额外的方式会导致失败。把上面那段代码换成C的语言实现如下:

if (helper == null) {// acquire monitor using spinlockwhile (atomic_acquire_cas(&this.lock, 0, 1) != success);if (helper == null) {newHelper = malloc(sizeof(Helper));newHelper->x = 5;newHelper->y = 10;helper = newHelper;}atomic_release_store(&this.lock, 0);
}

此时问题就更加明显了: helper的store操作发生在memory barrier之前,这意味着其他的线程能够在store x/y之前观察到非空的值。

你应该尝试确保store helper执行在atomic_release_store()方法之后。通过重新排序代码进行加锁,但是这是无效的,因为往上移动的代码,编译器可以把它移动回原来的位置:在atomic_release_store()前面。 (这里没有读懂,下次再回读)

有2个方法可以解决这个问题:

  • 删除外层的检查。这确保了我们不会在synchronized代码段之外做任何的检查。
  • 声明helper为volatile。仅仅这样一个小小的修改,在前面示例中的代码就能够在Java 1.5及其以后的版本中正常工作。

下面的示例演示了使用volatile的2各重要问题:

class MyClass {int data1, data2;volatile int vol1, vol2;void setValues() {    // runs in thread 1data1 = 1;vol1 = 2;data2 = 3;}void useValues1() {    // runs in thread 2if (vol1 == 2) {int l1 = data1;    // okayint l2 = data2;    // wrong}}void useValues2() {    // runs in thread 2int dummy = vol2;int l1 = data1;    // wrongint l2 = data2;    // wrong}

请注意useValues1(),如果thread 2还没有察觉到vol1的更新操作,那么它也无法知道data1或者data2被设置的操作。一旦它观察到了vol1的更新操作,那么它也能够知道data1的更新操作。然而,对于data2则无法做任何猜测,因为store操作是在volatile store之后发生的。

useValues2()使用了第2个volatile字段:vol2,这会强制VM生成一个memory barrier。这通常不会发生。为了建立一个恰当的“happens-before”关系,2个线程都需要使用同一个volatile字段。在thread 1中你需要知道vol2是在data1/data2之后被设置的。(The fact that this doesn’t work is probably obvious from looking at the code; the caution here is against trying to cleverly “cause” a memory barrier instead of creating an ordered series of accesses.)

Java中的synchronized与volatile关键字相关推荐

  1. 一文读懂Java内存模型(JMM)及volatile关键字

    点赞再看,养成习惯,公众号搜一搜[一角钱技术]关注更多原创技术文章. 本文 GitHub org_hejianhui/JavaStudy 已收录,有我的系列文章. 前言 并发编程从操作系统底层工作的整 ...

  2. 全面理解Java内存模型(JMM)及volatile关键字

    [版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772461 出自[zejian ...

  3. Java中的break和continue关键字使用总结

    java中的break和continue关键字使用总结 一.作用和区别 break的作用是跳出当前循环块(for.while.do while)或程序块(switch).在循环块中的作用是跳出当前正在 ...

  4. java中用于定义小数的关键字_Java 中用于定义小数的关键字有两个:( ) 和 ( )。_学小易找答案...

    [填空题]列举至少三种中药饮片现代贮藏方法 [单选题]用于薄壁形零件联接时,应采用 . [填空题]若螺纹的直径和螺纹副的摩擦系数一定, 则拧紧螺母时的效率取决于螺纹的 和 . [单选题]以太网采用的介 ...

  5. 简要解析Java中的throw和throws关键字

    解析Java中的throw和throws关键字 1 throws关键字 1.1 作用 向上抛异常,把异常交给调用处处理,实际上自身并没有处理异常. 1.2 原理 一旦方法体出现异常,仍会在异常代码出生 ...

  6. java volatile lock_Java并发学习笔记 -- Java中的Lock、volatile、同步关键字

    Java并发 一.锁 1. 偏向锁 1. 思想背景 来源:HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同 一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁 ...

  7. Java 多线程之 synchronized 和 volatile 的比较

    概述 在做多线程并发处理时,经常需要对资源进行可见性访问和互斥同步操作.有时候,我们可能从前辈那里得知我们需要对资源进行 volatile 或是 synchronized 关键字修饰处理.可是,我们却 ...

  8. 【Java线程】深入理解Volatile关键字和使用

    目录 背景 volatile原理 volatile特性 可见性 有序性 原子性 使用场景 背景 理解volatile底层原理之前,首先介绍关于缓存一致性协议的知识. 背景:计算机在执行程序时,每条指令 ...

  9. 【Java并发编程 】同步——volatile 关键字

    英 /ˈvɒlətaɪl/ 我了太噢(记不住单词怎么读) 一.volatile的介绍? volatile是一个轻量级的synchronized,一般作用与变量,在多处理器开发的过程中保证了内存的可见性 ...

最新文章

  1. php gzipstream,c# – 在WebRequest中发送gzip数据?
  2. 修图动口不动手,有人把StyleGAN和CLIP组了个CP,能听懂修图指令那种
  3. Python中定义函数的三种形式
  4. 如何构建一个分布式爬虫:实战篇
  5. linux收缩java位置,找到linux中当前java的安装位置
  6. 金融项目app服务器配置,云在金融的应用
  7. Get sdcard directory by adb
  8. windows拷贝内容到ubuntu中
  9. row_number()over函数的使用(转)
  10. 1432: 【蓝桥杯】:剪格子(迷宫问题变体)
  11. Maven下载jar包失败的原因- 解决方法汇总
  12. Eclipse中对一个项目进行复制粘贴为一个新项目
  13. Windows服务器远程桌面访问(两种方法)
  14. 蓝桥杯c语言必备知识点,C语言知识点大汇总
  15. python报错: list object has no attribute shape的解决
  16. 2020年东三省数学建模联赛(辽宁赛区)获奖名单
  17. 凸优化和非凸优化的区别
  18. 转:著名的100个管理定律点评9 - 成也细节,败也细节略
  19. week7 TT的旅行日记
  20. kiv8测量方法_measure_测量 | measure_Scikit image_参考手册_非常教程

热门文章

  1. ​redis实现消息队列
  2. dedecms模板中首页实现分页的方法
  3. 节省公司的宽带接入成本
  4. SDN控制器策略制定和表项下发—Vecloud
  5. 来自iSpy整理的最全海康大华IPC的RTSP连接地址
  6. CF449B Jzzhu and Cities 迪杰斯特拉最短路算法
  7. 图片的赖加载(lazyLoad)
  8. 第二阶段冲刺第六天(6月5号)
  9. webservice客户端开发
  10. wxPython 笔记(3)基本结构