目录
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中,该包提供了免锁定、线程安全的基本类型和操作。
最佳的做法还是不共享可变数据,要么共享不可变数据,要么压根不共享,将可变数据限制在但线程中。如果做到了这个,就不用担心采用的框架或库是否引入了你不知道的线程。
注意:
- 读和写操作必须同时被同步,否则无法保证同步起到作用。
- 如果执行线程间通讯,而无互斥操作,则可以采用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,需要将此域声明为volatile
private 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,需要将此域声明为volatile
private 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平台上最不可移植的特征。