Tips

书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code

注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

并发

线程允许多个活动同时进行。 并发编程比单线程编程更难,因为更多的事情可能会出错,并且失败很难重现。 你无法避免并发。 它是平台中固有的,也是要从多核处理器获得良好性能的要求,现在无处不在。本章包含的建议可帮助你编写清晰,正确,文档完备的并发程序。

78. 同步访问共享的可变数据

synchronized关键字确保一次只有一个线程可以执行一个方法或代码块。许多程序员认为同步只是一种互斥的方法,以防止一个线程在另一个线程修改对象时看到对象处于不一致的状态。在这个观点中,对象以一致的状态创建(条目 17),并由访问它的方法锁定。这些方法观察状态,并可选地引起状态转换,将对象从一个一致的状态转换为另一个一致的状态。正确使用同步可以保证没有任何方法会观察到处于不一致状态的对象。

这种观点是正确的,但它只说明了一部分意义。如果没有同步,一个线程的更改可能对其他线程不可见。同步不仅阻止线程观察处于不一致状态的对象,而且确保每个进入同步方法或块的线程都能看到由同一锁保护的所有之前修改的效果。

语言规范保证读取或写入变量是原子性(atomic)的,除非变量的类型是long或double [JLS, 17.4, 17.7]。换句话说,读取long或double以外的变量,可以保证返回某个线程存储到该变量中的值,即使多个线程在没有同步的情况下同时修改变量也是如此。

你可能听说过,为了提高性能,在读取或写入原子数据时应该避免同步。这种建议大错特错。虽然语言规范保证线程在读取属性时不会看到任意值,但它不保证由一个线程编写的值对另一个线程可见。同步是线程之间可靠通信以及互斥所必需的。这是语言规范中称之为内存模型(memory model)的一部分,它规定了一个线程所做的更改何时以及如何对其他线程可见[JLS, 17.4;Goetz06, 16)。

即使数据是原子可读和可写的,未能同步对共享可变数据的访问的后果也是可怕的。 考虑从另一个线程停止一个线程的任务。 Java类库提供了Thread.stop方法,但是这个方法很久以前就被弃用了,因为它本质上是不安全的——它的使用会导致数据损坏。 不要使用Thread.stop。 从另一个线程中停止一个线程的推荐方法是让第一个线程轮询一个最初为false的布尔类型的属性,但是第二个线程可以设置为true以指示第一个线程要自行停止。 因为读取和写入布尔属性是原子的,所以一些程序员在访问属性时不需要同步:

// Broken! - How long would you expect this program to run?
public class StopThread {
private static boolean stopRequested; public static void main(String[] args)
throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested)
i++;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}

你可能希望这个程序运行大约一秒钟,之后主线程将stoprequired设置为true,从而导致后台线程的循环终止。然而,在我的机器上,程序永远不会终止:后台线程永远循环!

问题是在没有同步的情况下,无法确保后台线程何时(如果有的话)看到主线程所做的stopRequested值的变化。 在没有同步的情况下,虚拟机将下面代码:

   while (!stopRequested)
i++;

转换成这样:

if (!stopRequested)
while (true)
i++;

这种优化称为提升(hoisting,它正是OpenJDK Server VM所做的。 结果是活泼失败( liveness failure):程序无法取得进展。 解决问题的一种方法是同步对stopRequested属性的访问。 正如预期的那样,该程序大约一秒钟终止:

// Properly synchronized cooperative thread termination
public class StopThread {
private static boolean stopRequested; private static synchronized void requestStop() {
stopRequested = true;
} private static synchronized boolean stopRequested() {
return stopRequested;
} public static void main(String[] args)
throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested())
i++;
}); backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}

注意,写方法(requestStop)和读方法(stop- required)都是同步的。仅同步写方法是不够的!除非读和写操作同步,否则不能保证同步工作。有时,只同步写(或读)的程序可能在某些机器上显示有效,但在这种情况下,表面的现象是具有欺骗性的。

即使没有同步,StopThread中同步方法的操作也是原子性的。换句话说,这些方法上的同步仅用于其通信效果,而不是互斥。虽然在循环的每个迭代上同步的成本很小,但是有一种正确的替代方法,它不那么冗长,而且性能可能更好。如果stoprequest声明为volatile,则可以省略StopThread的第二个版本中的锁定。虽然volatile修饰符不执行互斥,但它保证任何读取属性的线程都会看到最近写入的值:

// Cooperative thread termination with a volatile field
public class StopThread {
private static volatile boolean stopRequested; public static void main(String[] args)
throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested)
i++;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}

在使用volatile时一定要小心。考虑下面的方法,该方法应该生成序列号:

// Broken - requires synchronization!
private static volatile int nextSerialNumber = 0; public static int generateSerialNumber() {
return nextSerialNumber++;
}

该方法的目的是保证每次调用都返回一个唯一值(只要调用次数不超过232次)。 方法的状态由单个可原子访问的属性nextSerialNumber组成,该属性的所有可能值都是合法的。 因此,不需要同步来保护其不变量。 但是,如果没有同步,该方法将无法正常工作。

问题是增量运算符(++)不是原子的。 它对nextSerialNumber属性执行两个操作:首先它读取值,然后它写回一个新值,等于旧值加1。 如果第二个线程在线程读取旧值并写回新值之间读取属性,则第二个线程将看到与第一个线程相同的值并返回相同的序列号。 这是安全性失败(safety failure):程序计算错误的结果。

修复generateSerialNumber的一种方法是将synchronized修饰符添加到其声明中。 这确保了多个调用不会交叉读取,并且每次调用该方法都会看到所有先前调用的效果。 完成后,可以并且应该从nextSerialNumber中删除volatile修饰符。 要保护该方法,请使用long而不是int,或者在nextSerialNumber即将包装时抛出异常。

更好的是,遵循条目 59条中建议并使用AtomicLong类,它是java.util.concurrent.atomic包下的一部分。 这个包为单个变量提供了无锁,线程安全编程的基本类型。 虽然volatile只提供同步的通信效果,但这个包还提供了原子性。 这正是我们想要的generateSerialNumber,它可能强于同步版本的代码:

// Lock-free synchronization with java.util.concurrent.atomic
private static final AtomicLong nextSerialNum = new AtomicLong(); public static long generateSerialNumber() {
return nextSerialNum.getAndIncrement();
}

避免此条目中讨论的问题的最佳方法是不共享可变数据。 共享不可变数据(条目 17)或根本不共享。 换句话说,将可变数据限制在单个线程中。 如果采用此策略,则必须对其进行文档记录,以便在程序发展改进时维护此策略。 深入了解正在使用的框架和类库也很重要,因为它们可能会引入你不知道的线程。

一个线程可以修改一个数据对象一段时间后,然后与其他线程共享它,只同步共享对象引用的操作。然后,其他线程可以在不进一步同步的情况下读取对象,只要不再次修改该对象。这些对象被认为是有效不可变的( effectively immutable)[Goetz06, 3.5.4]。将这样的对象引用从一个线程转移到其他线程称为安全发布(safe publication )[Goetz06, 3.5.3]。安全地发布对象引用的方法有很多:可以将它保存在静态属性中,作为类初始化的一部分;也可以将其保存在volatile属性、final属性或使用正常锁定访问的属性中;或者可以将其放入并发集合中(条目 81)。

总之,当多个线程共享可变数据时,每个读取或写入数据的线程都必须执行同步。 在没有同步的情况下,无法保证一个线程的更改对另一个线程可见。 未能同步共享可变数据的代价是活性失败和安全性失败。 这些失败是最难调试的。 它们可以是间歇性的和时间相关的,并且程序行为可能在不同VM之间发生根本的变化。如果只需要线程间通信,而不需要互斥,那么volatile修饰符是一种可接受的同步形式,但是正确使用它可能会比较棘手。

Effective Java 第三版——78. 同步访问共享的可变数据的更多相关文章

  1. 《Effective Java 第三版》目录汇总

    经过反复不断的拖延和坚持,所有条目已经翻译完成,供大家分享学习.时间有限,个别地方翻译得比较仓促,希望有疑虑的地方指出批评改正. 第一章简介 忽略 第二章 创建和销毁对象 1. 考虑使用静态工厂方法替 ...

  2. 《Effective Java 第三版》新条目介绍

    版权声明:本文为博主原创文章,可以随意转载,不过请加上原文链接. https://blog.csdn.net/u014717036/article/details/80588806前言 从去年的3月份 ...

  3. Effective Java 第三版——1. 考虑使用静态工厂方法替代构造方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  4. Effective Java 第三版——13. 谨慎地重写 clone 方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  5. Effective Java 第三版——19. 如果使用继承则设计,并文档说明,否则不该使用

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  6. Effective Java 第三版——21. 为后代设计接口

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  7. Effective Java 第三版——3. 使用私有构造方法或枚类实现Singleton属性

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  8. Effective Java 第三版——7. 消除过期的对象引用

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  9. Effective Java 第三版——9. 使用try-with-resources语句替代try-finally语句

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

随机推荐

  1. Spring Boot 项目实战(四)集成 Redis

    一.前言 上篇介绍了接口文档工具 Swagger 及项目监控工具 JavaMelody 的集成过程,使项目更加健壮.在 JAVA Web 项目某些场景中,我们需要用缓存解决如热点数据访问的性能问题,业 ...

  2. shell script exit if any command fails

    dd this to the beginning of the script: set -e This will cause the shell to exit immediately if a si ...

  3. 牛客练习赛35-函数的魔法-floyd

    函数的魔法 思路 :如果 可以从A到B最终 都会是233范围内的数字进行转换,注意 这里 建图 为单向图  这个运算未必符合交换关系. #include<bits/stdc++.h> us ...

  4. 测试中,重现偶发的BUG问题。

    1.严格按用例执行: 2.如果是作随机测试时,把测试步骤的点进行速记; 3.偶发BUG一般都是严重的,保留现场,让开发人员一起分析留下的现场(如数据的变化,界面窗口的变化等,找出问题的引子,那怕是千丝 ...

  5. [CC-SEAPERM2]Sereja and Permutations

    [CC-SEAPERM2]Sereja and Permutations 题目大意: 有一个\(n(n\le300)\)排列\(p\),将其中一个元素\(p_i\)拿掉,然后将原来大于\(p_i\)的 ...

  6. js怎么删数组固定的值

    <script type="text/javascript"> Array.prototype.indexOf = function(val) { for (var i ...

  7. Android 中的设计模式

    1.单例模式 ContentProvider是单例模式,多个ContentResolver操作的都是同一个ContentProvider.

  8. 数据结构 Sunday算法

    Sunday算法是Daniel M.Sunday于1990年提出的字符串模式匹配算法.相对比较KMP和BM算法而言,简单了许多. Sunday算法的思想类似于BM算法中的坏字符思想,有点像其删减版.差 ...

  9. 学习Struts--Chap06:Struts2之数据验证

    1.数据验证的概述 1.1.数据验证的重要性 数据验证是非常必要的,不但和我们的常识性理解有关系,还有可能涉及到一些非法输入等问题,所以我们需要进行必要的数据验证,以保证我们在数据输入的时候都是正确且 ...

  10. docker 报错: Job for docker.service failed because the control process exited with error code. See "systemctl status docker.service" and "journalctl -xe" for details.

    centos 启动docker服务报错: Job for docker.service failed because the control process exited with error cod ...