2 线程安全性
同步机制关键字synchronized,volatile类型的变量,显示锁(Explicit Lock)以及原子变量。
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:
- 不在线程之间共享该状态变量
- 将状态变量修改为不可变的变量
- 在访问状态变量时使用同步
2.1 什么是线程安全性
某个类的行为与其规范完全一致。在良好的规范中通常会定义各种不变性条件(Invariant)来约束对象的状态,以及定义各种后验条件(Postcondition)来描述对象操作的结果。
无状态的Servlet是线程安全的,因此无状态对象一定是线程安全的。
2.2 原子性
++count这是一个“读取-修改-写入”的操作序列,并且其结果状态依赖于之前的状态。
2.2.1 竟态条件Race Condition
延迟初始化:先检查后执行判断对象是否为空new一个对象。
递增运算:读取-修改-写入++count/count++
2.2.2 复合操作(原子操作)
递增运算:原子变量类AtomicLong
延迟初始化:synchronized
2.3 加锁机制
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
2.3.1 内置锁
同步代码块(Synchronized Block)
2.3.2 重入
内置锁是可重入的。子类改写了父类的synchronized方法,然后调用父类中的方法,如果没有重入的锁,那么这段代码将产生死锁。
2.4 用锁来保护状态
对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。
2.5 活跃性与性能
当执行时间较长的计算或者可能无法快速完成的操作时(网络I/O或者控制台I/O),一定不要持有锁。
3 对象的共享
内存可见性(Memory Visibility)。我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。
3.1 可见性
public c
lass NoVisibilityDemo {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread{
@Override
public void run() {
while(!ready){
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
3.1.1 失效的数据
JavaBean不是线程安全的,get和set方法存在失效数据,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性称为最低安全性。
3.1.2 非原子的64位操作
非volatile类型的64位数值变量(double、long)不符合最低安全性。
3.1.3 加锁与可见性
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
3.1.4 volatile变量
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
当且仅当满足以下所有条件时,才应该使用volatile变量:
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
- 该变量不会与其他状态变量一起纳入不变性条件中。
- 在访问变量时不需要加锁。
3.2 发布(Publish)与逸出(Escape)
不要在构造过程中使this引用逸出。
3.3 线程封闭
局部变量和ThreadLocal类
3.3.1 Ad-hoc线程封闭
完全由程序实现来承担。
3.3.2 栈封闭
只能通过局部变量才能访问对象。
3.3.3 TheadLocal类
能使线程中的某个值与保持值得对象关联前来,使每一个使用该变量线程都存有一份独立的副本。
3.4 不变性
不可变对象一定是线程安全的。
当满足以下条件时,对象才是不可变的:
- 对象创建以后其状态就不能修改。
- 对象的所有域都是final类型。
- 对象是正确创建的(在对象的创建期间,this引用没有逸出)。
3.4.1 Final域
除非需要更高的可见性,否则应将所有的域都声明为private;
除非需要某个域是可变的,否则应将其声明为final;
3.4.2 使用Volatile类型来发布不可变对象
3.5 安全发布
3.5.1 不正确的发布:正确的对象被破坏
3.5.2 不可变对象与初始化安全性
可以通过任意机制来发布。
3.5.3 安全发布的常用模式
- 在静态初始化函数中初始化一个对象引用。
- 将对象的引用保存到volatile类型的域或者AtomicReferance对象中。
- 将对象的应用保存到某个正确构造对象的final类型域中。
- 将对象的引用保存到一个由锁保护的域中。
3.5.4 事实不可变对象
public Map
必须通过安全方式来发布。
3.5.5 可变对象
必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。
3.5.6 安全地共享对象
- 线程封闭
- 只读共享
- 线程安全共享
- 保护对象
4 对象的组合
4.1 设计线程安全的类
- 找出构成对象状态的所有变量。
- 找出约束状态变量的不变性条件。
- 建立对象状态的并发访问管理策略。
4.2 实例封闭
4.3 线程安全性的委托
4.4 在现有的线程安全类中添加功能
/**
* 扩展Vector并添加一个"若没有则添加"方法
* @param
*/
public class BetterVector extends Vector {
public synchronized boolean putIfAbsent(E e){
boolean absent = !contains(e);
if(absent){
add(e);
}
return absent;
}
}
/**
* 非线程安全的"若没有则添加"
* putIfAbsent相对于List的其他操作来说并不是原子的。
* @param
*/
public class ListHelper {
public List list = Collections.synchronizedList(new ArrayList());
public synchronized boolean putIfAbsent(E x){
boolean absent = !list.contains(x);
if(absent){
list.add(x);
}
return absent;
}
}
/**
* 通过客户端加锁来实现"若没有则添加"
* @param
*/
public class ListHelper {
public List list = Collections.synchronizedList(new ArrayList());
public boolean putIfAbsent(E x){
synchronized(list){
boolean absent = !list.contains(x);
if (absent) {
list.add(x);
}
return absent;
}
}
}
/**
* 通过组合来实现"若没有则添加"
* @param
*/
public class ListHelper extends ArrayList {
private final List list;
public ListHelper(List list) {
this.list = list;
}
public synchronized boolean putIfAbsent(E x){
boolean contains = !list.contains(x);
if (contains) {
list.add(x);
}
return contains;
}
}
4.5 将同步策略文档化
例如SimpleDateFormat并不是线程安全的,但JDK1.4之前的Javadoc并没有提到这点。
5 基础构建模块
5.1 同步容器类
Hashtable和Vector、Collections.synchronizedXxx等工厂方法。
5.1.1 同步容器类的问题
同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁来保护复合操作。
5.1.2 迭代器与ConcurrentModificationException
通过CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap完美解决。
package com.dhm;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
public class ConcurrentModificationExceptionDemo {
public static void main(String[] args) {
List list = new CopyOnWriteArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
System.out.println("List遍历前:");
for(String s:list){
list.add("e");
System.out.println(s);
list.remove(s);
}
System.out.println("List遍历后:");
for(String s:list){
System.out.println(s);
}
Map map = new ConcurrentHashMap<>();
map.put("a","a");
map.put("b","b");
map.put("c","c");
map.put("d","d");
System.out.println("Map遍历前:");
for(Map.Entry entry:map.entrySet()){
map.put("e","e");
System.out.println("key:"+entry.getKey()+";value:"+entry.getValue());
map.remove(entry.getKey());
}
System.out.println("Map遍历后:");
for(Map.Entry entry:map.entrySet()){
System.out.println("key:"+entry.getKey()+";value:"+entry.getValue());
}
Set set = new CopyOnWriteArraySet<>();
set.add("a");
set.add("b");
set.add("c");
set.add("d");
System.out.println("Set遍历前:");
for(String s:set){
set.add("e");
System.out.println(s);
set.remove(s);
}
System.out.println("Set遍历后:");
for(String s:set){
System.out.println(s);
}
}
}
5.1.3 隐藏迭代器
hashCode和equals、containsAll、RemoveAll、RetainAll都会对容器进行迭代。
5.2 并发容器
5.2.1 ConcurrentHashMap
ConcurrentHashMap中没有实现对Map加锁以提供独占访问。在Hashtable和synchronizedMap中,获得Map的锁能防止其他线程访问这个Map。只有当应用程序需要加锁Map以进行独占访问时,才应该放弃使用ConcurrentHashMap。
5.2.2 额外的原子Map操作
ConcurrentMap
5.2.3 CopyOnWriteArrayList、CopyOnWriteArraySet
写入时复制
5.3 阻塞队列和生产者 - 消费者模式
5.4 阻塞方法与中断方法
5.5 同步工具类
5.5.1 闭锁 CountDownLatch
5.5.2 FutrueTask
5.5.3 信号量 Counting Semaphore
5.5.4 栅栏Barrier
5.6 构建高效且可伸缩的结果缓存