Java并发编程实战读书笔记-Chapter4
到目前为止,我们已经介绍了关于线程安全与同步的一些基础知识。然而,我们并不希望对每一次内存访问都进行分析以确保程序说线程安全的,而是希望将一些现有的线程安全组件组合成为更大规模的组件或程序。本章将介绍一些组合模式,这些模式能够使一个类更容易成为线程安全的,并且在维护这些类时不会无意中破坏类的安全性保证
4.1 设计线程安全的类
在设计线程安全的类中,需要包含以下三个基本要素:
- 找出构成对象状态的所有变量。
- 找出约束状态变量的不变性条件。
- 建立对象状态的并发访问管理策略。
要分析对象的状态,首先从对象的域开始。如果对象中所有的域都
是基本类型的变量,那么这些域将构成对象的全部状态。程序清单4-1
中的Counter只有一个域value,因此这个域就是Counter的全部状态。对
于含有n个基本类型域的对象,其状态就是这些域构成的n元组。例如,
二维点的状态就是它的坐标值(x, y)。如果在对象的域中引用了其他
对象,那么该对象的状态将包含被引用对象的域。
示例1:使用Java监视器模式的线程安全计数器
@ThreadSafe
public final class Counter {
@GuardedBy("this")private long value=0;
public synchronized long getValue() {
return value;
}
public synchronized long increment() {
if(value==Long.MAX_VALUE){
throw new IllegalStateException("counter overflow");
}
return ++value;
}
}
4.2 实例封闭
如果某对象不是线程安全的,那么可以通过多种技术使其在多线程
程序中安全地使用。你可以确保该对象只能由单个线程访问(线程封
闭),或者通过一个锁来保护对该对象的所有访问。
封装简化了线程安全类的实现过程,它提供了一种实例封闭机制
(Instance Confinement),通常也简称为“封闭”。当一个对
象被封装到另一个对象中时,能够访问被封装对象的所有代码路径都是
已知的。与对象可以由整个程序访问的情况相比,更易于对代码进行分
析。通过将封闭机制与合适的加锁策略结合起来,可以确保以线程安全
的方式来使用非线程安全的对象。
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,
从而更容易确保线程在访问数据时总能持有正确的锁。
示例2:通过封闭机制来确保线程安全
@ThreadSafe
public class PersonSet {
@GuardedBy("this")
private final Set<Person> mySet=new HashSet<Person>();
public synchronized void addPerson(Person p) {
mySet.add(p);
}
public synchronized boolean containsPerson(Person p) {
return mySet.contains(p);
}
}
PersonSet的状态由HashSet来管理的,而HashSet并非线程安全的。但由于mySet是私有的并且不会逸出,因此HashSet被封闭在PersonSet中。唯一能访问mySet的代码路径是addPerson与containsPerson,在执行它们时都要获得PersonSet上的锁。PersonSet的状态完全由它的内置锁保护,因而PersonSet是一个线程安全的类。
这个示例并未对Person的线程安全性做任何假设,但如果Person类是可变的,那么在访问从PersonSet中获得的Person对象时,还需要额外的同步。要想安全地使用Person对象,最可靠的方法就是使Person成为一个线程安全的类。另外,也可以使用锁来保护Person对象,并确保所有客户代码在访问Person对象之前都已经获得正确的锁。
实例封闭是构建线程安全类的一个最简单方式,它还使得在锁策略的选择上拥有了更多的灵活性。在PersonSet中使用了它的内置锁来保护它的状态,但对于其他形式的锁来说,只要自始至终都使用同一个锁,就可以保护状态。实例封闭还使得不同的状态变量可以由不同的锁来保护。
在Java平台的类库中还有很多线程封闭的示例,其中有些类的唯一用途就是将非线程安全的类转化为线程安全的类。一些基本的容器类并非线程安全的,例如ArrayList和HashMap,但类库提供了包装器工厂方法(例如Collections.synchronizedList及其类似方法),使得这些非线程安全的类可以在多线程环境中安全地使用。这些工厂方法通过“装饰器(Decorator)”模式将容器类封装在一个同步的包装器对象中,而包装器能将接口中的每个方法都实现为同步方法,并将调用请求转发到底层的容器对象上。只要包装器对象拥有对底层容器对象的唯一引用(即把底层容器对象封闭在包装器中),那么它就是线程安全的。在这些方法的Javadoc中指出,对底层容器对象的所有访问必须通过包装器来进行。
4.2.1 Java监视器模式
Java监视器模式的主要优势就在于它的简单性。Java监视器模式仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都使用该锁对象,都可以用来保护对象的状态。
示例3:通过一个私有锁来保护状态
public class PrivateLock {
private final Object myLock = new Object();
PrivateLock() {
}
void someMethod() {
synchronized (myLock) {
System.out.println("模拟输出-------");
}
}
public Object getMyLock() {
return myLock;
}
public static void main(String[] args) {
final PrivateLock privateLock = new PrivateLock();
new Thread(()-> {
//通过共有方法来访问锁
final Object lock = privateLock.getMyLock();
synchronized (lock) {//和synchronized (myLock) 是同一把锁
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(privateLock::someMethod).start();
}
}
使用私有的锁对象而不是对象的内置锁(或任何其他可通过公有方
式访问的锁),有许多优点。私有的锁对象可以将锁封装起来,使客户
代码无法得到锁,但客户代码可以通过公有方法来访问锁,以便(正确
或者不正确地)参与到它的同步策略中。如果客户代码错误地获得了另
一个对象的锁,那么可能会产生活跃性问题。此外,要想验证某个公有
访问的锁在程序中是否被正确地使用,则需要检查整个程序,而不是单
个的类。
4.3 线程安全性的委托
大多数对象都是组合对象。当从头开始构建一个类,或者将多个非线程安全的类组合为一个类时,Java监视器模式是非常有用的。但是,如果类中的各个组件都已经是线程安全的,会是什么情况呢?我们是否需要再增加一个额外的线程安全层?答案是“视情况而定”。在某些情况下,通过多个线程安全类组合而成的类是线程安全的,而在某些情况下,这仅仅是一个好的开端。
在前面的CountingFactorizer类中,我们在一个无状态的类中增加了一个AtomicLong类型的域,并且得到的组合对象仍然是线程安全的。由于CountingFactorizer的状态就是AtomicLong的状态,而AtomicLong是线程安全的,因此CountingFactorizer不会对counter的状态施加额外的有效性约束,所以很容易知道CountingFactorizer是线程安全的。我们可以说CountingFactorizer将它的线程安全性委托给AtomicLong来保证:之所以CountingFactorizer是线程安全的,是因为AtomicLong是线程安全的。
示例4:在DelegatingVehicleTracker中使用的不可变Point类
class Point {
public final int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public class DelegatingVehicleTracker {
private final ConcurrentMap<String, Point> locations;
private final Map<String, Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String, Point> points) {
this.locations = new ConcurrentHashMap<String, Point>(points);
this.unmodifiableMap = Collections.unmodifiableMap(locations);//不可修改的副本信息
}
//返回的是修饰后的locations(使用不可修改的Map进行包装)
public Map<String, Point> getLocations() {
return unmodifiableMap;
}
public Point getLocation(String id) {
return locations.get(id);
}
//直接将value替换为新值
public void setLocation(String id, int x, int y) {
if (locations.replace(id, new Point(x, y)) == null) {
throw new IllegalArgumentException();
}
}
public Map<String, Point> getLocationsAsStatic() {
return Collections.unmodifiableMap(new HashMap<String, Point>(locations));
}
public static void main(String[] args) {
final Point point1 = new Point(1, 1);
final Point point2 = new Point(2, 2);
final ConcurrentHashMap<String, Point> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put("car-1", point1);
concurrentHashMap.put("car-2", point2);
final DelegatingVehicleTracker tracker = new DelegatingVehicleTracker(concurrentHashMap);
new Thread(() -> {
for (Map.Entry<String, Point> entry : tracker.getLocations().entrySet()) {
System.out.println(entry.getKey() + "==>" + + entry.getValue().x + entry.getValue().y);
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("1秒后访问更新后的车辆位置信息");
for (Map.Entry<String, Point> entry : tracker.getLocations().entrySet()) {
System.out.println(entry.getKey() + "==>" + entry.getValue().x + entry.getValue().y);
}
}).start();
new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
tracker.setLocation("car-1", 5, 6);
}).start();
}
}
如果需要一个不发生变化的车辆视图,那么getLocations可以返回对locations这个Map对象的一个浅拷贝(Shallow Copy)。由于Map的内容是不可变的,因此只需复制Map的结构,而不用复制它的内容,如示例4所示(其中只返回一个HashMap,因为getLocations并不能保证返回一个线程安全的Map)。