文章目录
- 但凡做事高效的人,总能在串行性和异步性之间找到合理的平衡,对于程序来说同样如此。
一、线程安全
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
1.1 无状态对象
无状态对象一定是线程安全的
:它既不包含任何域,也不包含任何对其它类中域的引用。
@ThreadSafe
public class StatelessFactorizer implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
}
1.2 原子性
- 竞态条件(Race Condition):由于不恰当的执行时序而出现不正确的结果。
@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
private long count = 0;
public void service(ServletRequest req, ServletResponse resp) {
...
++count;
...
}
}
- 先检查后执行(Check-Then-Act):通过一个可能失效的观测结果来决定下一步动作。
@NotThreadSafe
public class LazyInitRace implements Servlet {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if(instance == null)
instance = new ExpensiveObject();
return instance;
}
}
- 原子操作:对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是以原子方式执行的操作。
@ThreadSafe
public class CountingFactorizer implements Servlet {
private final AtomicLong count = new AtomicLong(0);
public long getCount() { return count.get(); }
public void service(ServletRequest req, ServletResponse resp) {
...
count.incrementAndGet();
...
}
}
1.3 锁
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由 同一个锁 来保护。
- 在没有足够原子性保证的情况下对其最近计算结果进行缓存。
@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber.get()))
encodeIntoResponse(resp, lastFactors.get());
else {
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp, factors);
}
}
}
- 内置锁:每个 Java 对象都可以用作一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。
@ThreadSafe
@RunsSlow
public class CachedFactorizer implements Servlet {
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
public synchronized void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
if (i.equals(lastNumber.get()))
encodeIntoResponse(resp, lastFactors);
else {
BigInteger[] factors = factor(i);
lastNumber = i;
lastFactors = factors;
encodeIntoResponse(resp, factors);
}
}
}
- 重入:如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。
// 线程在调用每个 doSomething 方法时都会尝试获取实例上的锁
// Widget widget = new LoggingWidget();
// widget.doSomething();
public class Widget {
public synchronized void doSomething() { ... }
}
public class LoggingWidget extends Widget {
@Overrride
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
- 通过缩小同步代码的作用范围,做到既保证 Servlet 的并发性同时又维护线程安全性。
@ThreadSafe
public class CachedFactorizer implements Servlet {
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
@GuardedBy("this") private long hits;
@GuardedBy("this") private long cacheHits;
public synchronized long getHits() { return hits; }
public synchronized double getCacheHitRatio() {
return (double) cacheHits / (double) hits;
}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = null;
synchronized (this) {
++hits;
if (i.equals(lastNumber)) {
++cacheHits;
factors = lastFactors.clone();
}
}
if (factors == null) {
factors = factor(i);
synchronized (this) {
lastNumber = i;
lastFactors = factors.clone();
}
}
encodeIntoResponse(resp, factors);
}
}
二、对象的共享
内存可见性(Memory Visibility):我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其它线程能够看到发生的状态变化。
2.1 可见性
- 重排序(Reordering):在缺少同步的情况下,Java 内存模型允许编译器对操作顺序进行重排序,并将数值缓存在寄存器中。
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready)
Thread.yield();
// case 1. ReaderThread 看不到 MainThread 对 ready 变量修改,一直循环下去;
// case 2. ReaderThread 先看到 ready (指令重排),输出 0;
// case 3. 期望的情况,输出 42 。
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
- 失效数据:除非在每次访问变量时都使用同步,否则很可能获得该变量的一个失效值。
@NotThreadSafe
public class MutableInteger {
private int value;
public int get() { return value; }
public void set(int value) { this.value = value; }
}
@ThreadSafe
public class SynchronizedInteger {
@GuardedBy("this") private int value;
/**
* 加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,
* 所有执行读操作或者写操作的线程都必须在同一个锁上同步。
*/
public synchronized int get() { return value; }
public synchronized void set(int value) { this.value = value; }
}
- Volatile 变量:声明后编译器与运行时都会注意到这个变量是共享的,不会将该变量上的操作与其它内存操作一起重排序;加锁机制既可以确保可见性又可以确保原子性,而 volatile 变量只能确保可见性。
volatile boolean asleep;
...
while(!asleep)
countSomeSheep();
2.2 发布与逸出
发布(Publish):使对象能够在当前作用域之外的代码中使用;
逸出(Escape):发布内部状态破坏了封装性,并使得程序难以维持不变性条件。
- 内部的可变状态逸出
class UnsafeStates {
private String[] states = new Sring[] {"AK", "AL", ...};
public String[] getStates() { return states; }
}
- 隐式地使用 this 引用逸出
public class ThisEscape {
/**
* 当对象在其构造函数中创建一个线程时,无论是显示创建(通过将它传给构造函数)
* 还是隐式创建(将 Thread 或 Runnable 作为该对象的一个内部类),this 引用都
* 会被新创建的线程共享。
* 在 “完全构造” 之前,新的线程就可以看见它。
* /
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
- 使用工厂方法来防止 this 引用在构造过程中逸出
public class SafeListener {
private final EventListener listener;
pirvate SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
2.3 线程封闭
- 栈封闭:只能通过局部变量才能访问对象。
public int loadTheArk(Collection<Animal> candidates) {
SortedSet<Animal> animals;
int numPairs = 0;
Animal candidate = null;
// animals confined to method, don't let them escape!
animals = new TreeSet<Animal>(new SpeciesGenderComparator());
animals.addAll(candidates);
for (Animal a : animals) {
if (candidate == null || !candidate.isPotentialMate(a))
candidate = a;
else {
ark.load(new AnimalPair(candidate, a));
++numPairs;
candidate = null;
}
}
return numPairs;
}
- ThreadLocal : 为每个使用该变量的线程都存有一份独立的副本,因此 get 总是返回由当前执行线程在调用 set 时设置的最新值。
public class ConnectionDispenser {
static String DB_URL = "jdbc:mysql://localhost/mydatabase";
private ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
try {
return DriverManager.getConnection(DB_URL);
} catch (SQLException e) {
throw new RuntimeException("Unable to acquire Connection", e);
}
};
};
public Connection getConnection() {
return connectionHolder.get();
}
}
2.4 不变性
如果某个对象在被创建后其状态就不能被改变,那么这个对象就称为不可变对象。
- 每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据。
// 因式分解将执行两个原子操作:1. 更新缓存结果; 2. 判断缓存的数值是否等于请求的数值。
// 构建对数值及其因数分解的结果进行缓存的不可变容器类,不可变对象一定是线程安全的。
@Immutable
class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
OneValueCache(BigInteger i, BigInteger[] factors) {
this.lastNumber = i;
this.lastFactors = Arrays.copyOf(factors, factors.length);
}
public BigInteger[] getFactors(BigInteger i) {
if (lastNumber == null || !lastNumber.equals(i))
return null;
else
return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
- 使用指向不可变容器对象的 volatile 类型引用以缓存最新的结果。
@ThreadSafe
public class volatileCachedFactorizer implements Servlet {
private volatile OneValueCache cache = new OneValueCache(null, null);
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = cache.getFactors(i);
if (factors == null) {
factors = factor(i);
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(resp, factors);
}
}
2.5 安全发布
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其它线程可见。
- 在静态初始化函数中初始化一个对象引用;
- 将对象的引用保存到 volatile 类型的域或者 AtomicReference 对象中;
- 将对象的引用保存到某个正确构造对象的 final 类型域中;
- 将对象的引用保存到一个由锁保护的域中(如Vector、BlockingQueue)。
三、对象的组合
我们并不希望对每一次内存访问都进行分析以确保程序是线程安全的,而是希望将一些现有的线程安全组件组合为更大规模的组件或程序。
- 设计线程安全的类
- 找出构成
对象状态
的所有变量; - 找出约束状态变量的
不变性条件
; - 建立对象状态的
并发访问管理策略
;
3.1 实例封闭
- 基于监视器模式的车辆追踪
- 每台车都由一个 String 对象来标识,并且拥有坐标 (x, y),根据 GPS 设备数据实时更新坐标;
@ThreadSafe
public class MonitorVehicleTracker {
@GuardedBy("this")
private final Map<String, MutablePoint> locations;
public MonitorVehicleTracker(Map<String, MutablePoint> locations) {
this.locations = deepCopy(locations);
}
// 复制数据,当车辆位置发生变化后,不会实时更新到返回后的容器中。
public synchronized Map<String, MutablePoint> getLocations() {
return deepCopy(locations);
}
public synchronized MutablePoint getLocation(String id) {
MutablePoint loc = locations.get(id);
return loc == null ? null : new MutablePoint(loc);
}
public synchronized void setLocation(String id, int x, int y) {
MutablePoint loc = locations.get(id);
if (loc == null)
throw new IllegalArgumentException("No such ID: " + id);
loc.x = x;
loc.y = y;
}
private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m) {
Map<String, MutablePoint> result = new HashMap<>();
for(String id : m.keySet())
result.put(id, new MutablePoint(m.get(id)));
return Collections.unmodifiableMap(result);
}
}
- 返回客户代码之前复制可变的数据来维持线程安全性
@NotThreadSafe
public class MutablePoint {
public int x, y;
public MutablePoint() { x = 0; y = 0; }
public MutablePoint(MutablePoint p) {
this.x = p.x;
this.y = p.y;
}
}
3.2 安全性委托
- 不使用显式同步,所有对状态的访问都有委托对象完成
监视器模式当车辆容器非常大时将极大地降低性能
@Immutable
public class Point {
public final int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
@ThreadSafe
public class DelegatingVehicleTracker {
private final ConcurrentMap<String, Point> locations;
private final Map<String, Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String, Point> points) {
locations = new ConcurrentHashMap<String, Map>(points);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
// 返回的是一个不可修改但却实时的车辆位置视图
public Map<String, Point> getLocations() {
return unmodifiableMap;
}
// 发布是一个不可变状态的引用,不会破坏封装性
public Point getLocation(String id) {
return locations.get(id);
}
public void setLocation(String id, int x, int y) {
if (locations.replace(id, new Point(x, y)) == null)
throw new IllegalArgumentException("invalid vehicle name: " + id);
}
}
3.3 安全发布底层状态
如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量。
@ThreadSafe
public class SafePoint {
@GuardedBy("this") private int x, y;
private SafePoint(int[] a) { this(a[0], a[1]); }
public SafePoint(SafePoint p) { this(p.get()); }
public SafePoint(int x, int y) {
this.x = x;
this.y = y;
}
public synchronized int[] get() {
return new int[] {x, y};
}
public synchronized void set(int x, int y) {
this.x = x;
this.y = y;
}
}
@ThreadSafe
public class PublishingVehicleTracker {
private final Map<String, SafePoint> locations;
private final Map<String, SafePoint> unmodifiableMap;
public PublishingVehicleTracker(Map<String, SafePoint> locations) {
this.locations = new ConcurrentHashMap<>(locations);
this.unmodifiableMap = Collections.unmodifiableMap(this.locations);
}
public Map<String, SafePoint> getLocations() {
return unmodifiableMap;
}
// 如果需要在车辆位置的变化进行判断或者当位置变化时执行一些操作,那么发布底层状态并不可行。
public SafePoint getLocation(String id) {
return locations.get(id);
}
public void setLocation(String id, int x, int y) {
if (!locations.containsKey(id))
throw new IllegalArgumentException("invalid vehicle name: " + id);
locations.get(id).set(x, y);
}
}