目录

Item 78: Synchronize access to shared mutable data

Item 79: Avoid excessive synchronization

Item 80: Prefer executors, tasks, and streams to threads

Item 81: Prefer concurrency utilities to wait and notify

Item 82: Document thread safety

Item 83: Use lazy initialization judiciously

Item 84: Don't depend on the thread scheduler


Item 78: Synchronize access to shared mutable data

同步访问共享的可变数据

synchronized关键字可以保证在同一时间,只有一个线程可以执行某个方法或代码块,使操作是互斥的,保证一个线程的修改对另一个线程是可见的。

如果想在一个线程上终止另一个线程,不要使用Thread.stop方法,此方法会导致数据遭破坏,是不安全的。建议做法:在第1个线程上轮询(poll)一个boolean域,第2个线程通过改变该域的值来终止第1个线程。

java语言规范保证读写一个变量是原子的(actomic),除非变量是long或者double类型。但不保证一个线程写入的值对另一个线程时可见的,即不保证同步。这是由于java语言规范中的内存模型(memory model)决定的,它规定了一个线程所做的变化何时以及如何被另一个线程可见。如下错误做法:

// 预期是1秒后停,实际永远不会停
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;}
} 

JVM在编译上面代码时,会进行优化提升(hoisting)转变:

while (!stopRequested)i++;
转变为:
while (!stopRequested)while(true)i++;

正确做法是增加同步措施,如下:

// 方法1,加同步
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();  // 替换为写方法}
}// 方法2,给变量加volatile,无需加同步
private static boolean stopRequested;
改成
private static volatile boolean stopRequested;

上述示例中同步的方法即使没有同步也是原子的,增加同步只是为了通讯效果,而非互斥操作。这时可通过给stopRequested变量增加关键字volatile来代替同步设置。

volatile修饰的变量是说此变量可能会被意想不到地改变,这样编译器就不会去假设这个变量的值了,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

但使用volatile需要谨慎,容易出错,如下:

private static volatile int nextSerialNumber = 0;private static int generateSerialNumber() {return nextSerialNumber++;
}

多线程下会出现意料之外的错误,因为++是非原子操作,包含两个操作:先读取值,然后写回加1后的新值。这属于安全性失败(safety failure),即程序计算出错误的结果。

正确做法是给方法加同步措施,或者使用AtomicLong来代替int/long:

private static final AtomicLong nextSerialNumber = new AtomicLong();private static int generateSerialNumber() {return nextSerialNumber.getAndIncrement();
}

AtomicLong在包java.util.concurrent.atomic中,该包提供了免锁定、线程安全的基本类型和操作。

最佳的做法还是不共享可变数据,要么共享不可变数据,要么压根不共享,将可变数据限制在但线程中。如果做到了这个,就不用担心采用的框架或库是否引入了你不知道的线程。

注意:

  1. 操作必须同时同步,否则无法保证同步起到作用。
  2. 如果执行线程间通讯,而无互斥操作,则可以采用volatile代替同步,但务必小心使用,因为容易错误使用。

Item 79: Avoid excessive synchronization

避免过度同步

术语:

  • recall:回调,类将自己作为参数传递给自己调用的外部函数,如在类A内部调用类B方法b.recall(this)(this是A的,该调用在A内部)。
  • multi-catch:多重捕获,如 cateh (Exception1 | Exception2 e) { ... }
  • reentrant lock:可重入锁
  • open call:开发调用,指在同步区域外被调用的外部方法。

过度使用同步可能导致性能低下、死锁,甚至不确定的行为。

为了避免活性和安全性失败(liveness and safety failure),在一个被同步的方法或代码块中,永远不要放弃对客户端的控制,即不要调用override重写方法或由客户端以函数对象形式提供的方法。

java类库提供了一个并发集合叫CopyOnWriteArrayList,是通过拷贝低层数组的新副本来解决同步问题的(读写分离的并发策略)。但该集合适合较少写、经常读或者遍历的场合,否则大量使用会严重影响性能。

在同步区域内做尽可能少的工作。

过度同步时耗:在多核系统中,过度同步的时间时耗并不是因为获取锁所花费的时间,而是因为失去了并行的机会、确保每个核有一致性的内存视图而导致的延迟、限制虚拟机优化代码的能力。

对于可变mutable类的同步,有两种思路:

  • 外部同步法:在调用的客户端代码处同步;
  • 内部同步法:在类方法实现内进行同步,可获得更高的性能。

Item 80: Prefer executors, tasks, and streams to threads

exector、task、steam优先于线程

Executor Framwork工作示例:

ExecutorService exec = Executors.newSingleThreadExecutor();  // 创造线程executor service
exec.execute(runnable);  // 执行线程
exec.shutdown();  // 如果没关闭,VM不会退出

尽量不要自己编写工作队列,也不要直接使用线程。当直接使用线程时(直接通过new Thread或其子类创建),线程既是工作单元(称为task)有时执行机制。应该使用Executor Framwork,它将二者分开,executor是执行机制,任务类型有两种类型:Runable、Callable(有返回值和能抛出异常)。

java 7中,Executor Framwork支持fork-join任务。

并发的strream是基于fork join池上编写的。

更多并发的知识参考《Java Concurrency in Practice》

Item 81: Prefer concurrency utilities to wait and notify

并发工具优于wait和notify

术语:

  • thread starvation deadlock:线程饥饿死锁

不推荐使用wait和nitify的原因是很难去正确使用它们,因此推荐使用更高级的并发工具来代替。

java.util.concurrent包中的并发工具可以分为3类:Eexcutor Framework、并发集合concurrent collection、同步器synchronizer。

concurrent collection有ConcurrentHashMap、BlockingQueue(含阻塞操作的集合)等。

synchronizer同步器是使线程能够等待另一个线程的对象,允许它们协调活动。有CountDownLatch倒计数锁存器、Semaphore、Phaser、CyclicBarrier等。

对于间歇式的定时,应优先使用System.nanoTime,而不是System.currentTomeMillis。前者更精确,且不受系统时间调整的影响。

真的需要使用wait的时候(如维护旧代码,必须在synchronized区域内调用wait方法,且应该使用wait循环模式来调用wait方法(避免被意外或恶意唤醒),即

synchronized (obj) {while (跳出条件不满足) {obj.wait();  // 释放锁,等待被唤醒}...// 执行唤醒后的其他操作
}

从保守角度看,建议使用notifyAll,从优化角度看,建议使用notify方法。但还是建议使用notifyAll,可防止不相关线程意外或恶意的等待。

Item 82: Document thread safety

用文档描述线程安全性

private lock object:私有锁对象

如果未对并发时的线程安全性进行描述,使用者容易出错,如缺乏同步或者过度同步。

类文档中应该清楚地说明它所支持的线程安全性级别。常见的级别有:

  • 不可变的(immutable):类的实例是不可变的。
  • 无条件的线程安全(unconditionally thread-safe):类实例是可变的,但有充分的内部同步措施。
  • 有条件的线程安全(conditionally thread-safe):部分方法需要增加外部同步措施,且一般会说明使用哪种类型的锁。
  • 非线程安全(not thread-safe):所有方法都要求增加外部同步措施。
  • 线程对立的(thread-hostile):即使增加外部同步措施,也不能保证并发时线程安全。原因在于没有从内部同步修改静态static域数据,如以下方法(同Item 78示例):
private static volatile int nextSerialNumber = 0;private static int generateSerialNumber() {return nextSerialNumber++;
}

一般在类文档注释说明类的线程安全性;有特殊线程安全属性的方法则在对应的方法文档注释中写明;静态工厂必须说明线程安全性(除非返回类型非常明显),如Collections.synchronizedMap的注释文档:

    /*** Returns a synchronized (thread-safe) map backed by the specified* map.  In order to guarantee serial access, it is critical that* <strong>all</strong> access to the backing map is accomplished* through the returned map.<p>** It is imperative that the user manually synchronize on the returned* map when traversing any of its collection views via {@link Iterator},* {@link Spliterator} or {@link Stream}:* <pre>*  Map m = Collections.synchronizedMap(new HashMap());*      ...*  Set s = m.keySet();  // Needn't be in synchronized block*      ...*  synchronized (m) {  // Synchronizing on m, not s!*      Iterator i = s.iterator(); // Must be in synchronized block*      while (i.hasNext())*          foo(i.next());*  }* </pre>* Failure to follow this advice may result in non-deterministic behavior.** <p>The returned map will be serializable if the specified map is* serializable.** @param <K> the class of the map keys* @param <V> the class of the map values* @param  m the map to be "wrapped" in a synchronized map.* @return a synchronized view of the specified map.*/public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {return new SynchronizedMap<>(m);}

优先使用私有锁对象(private lock object),而不是公共可访问的锁对象(影响性能):

private final Object lock = new Object();  // 私有锁对象public void foo() {synchronized(lock) {...}
}

锁lock域必须声明为final。

私有锁对象只能用于无条件的线程安全类。 有条件的线程安全类必须要用文档来说明获取锁的类型。

私有锁对象特别适用于面向继承的类。在继承中如果采用的是其他锁(如对象锁),子类容易无意中妨碍父类的操作(举例? 子类实例对象锁与父类实例对象锁的关系 - 亮仔的程序园 - 博客园 、 Java多线程(2):synchronized 锁重入、锁释放、锁不具有继承性_保暖大裤衩LeoLee的博客-CSDN博客)。

Item 83: Use lazy initialization judiciously

慎用延迟初始化

术语

lazy initialization:延迟初始化,一种优化技术,指将域的初始化操作延迟到需要用到域值的时候再进行。

延迟初始化是把双刃剑,除非需要,否则不要去用。虽然降低了初始化类或者创建实例的开销,但增加了访问被延迟初始化的域的开销。

在大多数情况下,正常初始化要优于延迟初始化。

延迟初始化示例:

// 正常初始化
private final FieldType field = computerFieldValue();// 延迟初始化
private FieldType field;
private synchronized FieldType getField() {if (field == null) {field = computerFieldValue();}return field;
}

如果出于性能考虑需要对静态域使用延迟初始化,就使用lazy initialization holder class模式。

// 用于static域的lazy initialization holder class模式
private static class FieldHolder {static final FieldType field = computerFieldValue();
}private static FieldType getField() {return FieldHolder.field;
}

getField()方法不需要进行同步,因为VM初始化静态类时会同步域的访问。

如果出于性能考虑需实例域使用延迟初始化,就使用双重检查(double-check)模式。

// 用于实例域的双重检查模式
private volatile FieldType field;  // 因为当域被初始化后没有锁lock,需要将此域声明为volatileprivate FieldType getField() {FieldType result = filed;if (result == null) {  // 第1次检查synchronized (this) {if (result == null) {  // 第2次检查field = result = computerFieldValue(); }   }}return result;
}

需要对代码中的局部变量result的必须性进行解释, 它的作用时确保field域在已经初始化的情况下只被读取一次。虽然严格意义上这不是必须的,但可以提升性能,并提供一个标准,使初级并发编程更加优雅(作者实验表明,使用了局部变量后,程序性能提升了1.4倍)。

如果不介意重复初始化域,可删去第2次检查,这种变形称为单重检查(single-check)模式:

// 双重检查变形1:用于实例域单重检查模式,会导致重复初始化现象(不介意的话)
private volatile FieldType field;  // 因为域被初始化后没有锁lock,需要将此域声明为volatileprivate FieldType getField() {FieldType result = filed;if (result == null) {  // 第1次检查field = result = computerFieldValue();   }return result;
}// 双重检查变形2:当域的类型为基本类型时,可将volatile去掉,该模式称为racy singel-check

更进一步,如果域的类型为基本类型,不是long、double,可去掉域的volatile修饰符,这种变形体称为racy single-check模式。可加速域的访问速度,但增加额外初始化的开销。且这种变形不常用。

对于基本类型的域或者对象引用域,用null判断,对于数值基本类型域,用0判断。

Item 84: Don't depend on the thread scheduler

不要依赖于线程调度器

线程调度器(thread scheduler):当多个线程可运行时,由线程调度器决定哪些线程将会执行以及执行多久。

任何依赖于线程调度器来达到正确性或者性能要求的程序,很可能都是不可移植的。

为了编写出健壮、响应良好、可移植的程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量(如何合理设置线程池大小_lsz冲呀的博客-CSDN博客_线程池大小设置)。

可运行(runnable)的线程数不等于线程总数,还有一部分线程是处于等待状态。

应适当规定线程池大小,并使任务适当小,也不能太小,否则分配的开销会影响整体性能。

线程不应该一直处于忙-等(busy-wait)的状态,即反复检查一个共享状态,等待某些状态的改变(通过类似让出cpu-唤醒方式代替busy-wait?)。

不要通过调用Thread.yield来修正程序(yield的作用是让出线程自己的cpu执行时间),它没有可测试的语义(testable semantic),不可移植,执行效果有不确定性(因为让出cup后可能又被自己抢到)。更好的办法是减少并发运行的线程数。

不要通过调整线程优先级来修正程序(但可用来提高一个已经正常工作的程序的服务质量)。线程优先级是java平台上最不可移植的特征。

读书笔记:Effective Java-第11章 并发Concurrency相关推荐

  1. [读书笔记]Effective Java 第四章

    使类和成员的可访问性最小化 规则很简单:尽可能地使每个类或者成员不被外界访问.实例域(非final)决不能是公有的.当需要暴露出不可变的实例时通常会把这个实例做成不可变或者是把这个实例变成私有,同时提 ...

  2. 《深入理解计算机系统》读书笔记-016(第 12 章 并发编程)

    <深入理解计算机系统>读书笔记-016(第 12 章 并发编程) 太惨了,这章真心不大看得懂啊--等把前面的补上之后把读书笔记重新整理一下吧.这样看了跟没看也没啥区别了. 在线程中,不同于 ...

  3. Thinking in java 第11章 持有对象 笔记+习题

    Thinking in java 第11章 持有对象 学习目录 11.1 泛型和类型安全的容器 1. 当你制定了某个类型作为泛型参数时,你并不仅限于只能将该确切类型的对象放置到容器中.向上转型也可一样 ...

  4. [读书笔记]Effective C++ - Scott Meyers

    [读书笔记]Effective C++ - Scott Meyers 条款01:视C++为一个语言联邦 C++四个次语言: 1. C Part-of-C++,没有模板.异常.重载. 2. Object ...

  5. mysql函桌为之一的_MYSQL必知必会读书笔记第十和十一章之使用函数处

    mysql简介 MySQL是一种开放源代码的关系型数据库管理系统(RDBMS),MySQL数据库系统使用最常用的数据库管理语言--结构化查询语言(SQL)进行数据库管理. 拼接字段 存储在数据库表中的 ...

  6. 英语读书笔记-Book Lovers Day 11

    英语读书笔记-Book Lovers Day 11 Part 1 And anyone who'd miss that book's obvious potentional is arguably i ...

  7. Effective java 总结11 - 序列化

    Effective java 总结11 - 序列化 序列化:对象 -> 字节流 反序列化:字节流 -> 对象 第85条 其他方法优先于java序列化 序列化的根本问题在于:攻击面过于庞大, ...

  8. 《资本论》读书笔记(2)第二卷第一章:资本形态变化及其循环

    <资本论>读书笔记(2)第二卷第一章:资本形态变化及其循环 +BIT祝威+悄悄在此留下版了个权的信息说: 货币资本的循环 第一阶段:资本家用手里的钱买来设备.原材料,雇来一批工人,或者说, ...

  9. Java 第11章 常用类库

    Java 第11章 常用类库 ​ Java是一种面向对象语言,Java中的类把方法与数据连接在一起,构成了自包含式的处理单元.为了提升Java程序的开发效率,Java的类包中提供了很多常用类以方便开发 ...

  10. 【读书笔记】《Effective Java》第二章 第2条:遇到多个构造器参数时要考虑使用Builder

    一.前言 <Effective Java>读书笔记系列 第二章 第1条:创建和销毁对象 第二章 第2条:遇到多个构造器参数时要考虑使用Builder 二.介绍 我们开发中偶尔会遇到一些需要 ...

最新文章

  1. 《编程之美》读书笔记19: 3.9 重建二叉树
  2. Oracle里default什么意思,ORACLE中默认值default的使用方法.doc
  3. 【项目管理】常见缩写(术语)
  4. 【推荐系统】深入理解YouTube推荐系统算法
  5. C语言学习之求两个整数的最大值
  6. unity案例入门(二)(坦克大战)
  7. jq双击放大图片_痘痘肌肤反馈图片,平时注意这3个就可以
  8. bash shell 快捷键
  9. Flex 4中组件背景设置(填充方式)group为例子
  10. 【Python】安装方法小结
  11. Java 杨辉三角的简单实现
  12. 老板要我开发一个简单的工作流,15 次需求变更,我干到秃了。。
  13. 【Strurts框架】第一节Action-通配符
  14. ntpwedit 提示密码未修改_关于开启密码控制策略所引发的一些问题
  15. us、ms、s 单位转换,不会的都是大傻子!!!
  16. 为什么视频无法播放,视频无法播放的原因是什么
  17. docker logs-查看docker容器日志
  18. 搜索与回溯 1215:迷宫
  19. Vue + Vuetify使用感受以及部分自定义组件
  20. SQL 中 TRIM()函数用法

热门文章

  1. 研究生如何做好科研和发表文章(三)
  2. 苹果电脑macos Monterey 12.3.1(21E258)dmg原版引导版镜像下载
  3. Kodak Imgedit.ocx控件显示tif图像
  4. pyyaml 3.11版本的安装
  5. 链表问题全面汇总与解析
  6. 输出100~1000的回文素数 - 简单题
  7. java后端视频流接口和前端video标签
  8. android合并 工具下载,m3u8合并工具安卓版
  9. 计算机房安全防范措施,数据中心的机房安全管理要做到“六防政策”
  10. Quadratic Probing:二次方探查法