一、QA
1、如何设计一个线程安全的类?
- 找出构成对象状态的所有变量
- 如果在对象的域中引用了其他对象,那么该对象的状态将包含被引用对象的域。例如,LinkedList的状态就包括该链表中所有节点对象的状态。
- 找出约束状态的不变性条件、先验条件和后验条件,这些需要额外的同步与封装。
- 不变性条件:约束了对象状态的取值范围,或者状态与状态之间存在不变性条件(例如obj.min必须要小于obj.max)。
- 先验条件:一个对象状态必须处于某个值才能进行下一步操作。
- 后验条件:一个对象状态的下一个值必须是某个值。
- 建立对象状态的并发访问策略
- 定义如何在不违背对象不变性条件或后验条件的情况下对状态的访问操作进行协同。同步策略规定了如何将不变性条件、线程封闭与加锁机制等结合起来以维护线程的安全性,并且还规定了哪些变量由哪些锁保护。要确保开发人员可以对这个类进行分析与维护,就必须将同步策略写为正式文档。
2、什么是实例封闭?
- 如果A类要使用一个线程不安全的B类作为A的状态变量,那么可以使B被封装在A中,不将B发布出去。也就是说将B的实例封装在了A实例中。这就是实例封闭。
- 即使外部要访问B也是通过A提供出来的入口去访问,此时,结合合适的加锁策略,可以确保以线程安全的方式来使用非线程安全的对象。
3、写一个通过实例封闭的机制来确保线程安全的例子
public class PersonSet {
// HashSet 本身并不是线程安全的,但是此对象是封装在PersonSet对象中的,外部不能直接访问
private final Set<Person> mySet = new HashSet<>();
// 实例封闭结合加锁机制来访问非线程安全的对象
public synchronized void addPerson(Person person) {
this.mySet.add(person);
}
public synchronized boolean containsPerson(Person person) {
return mySet.contains(person);
}
// 这个示例并未对Person的线程安全性做任何假设,但如果Person类是可变的,
// 那么再访问从PersonSet中获得的Person对象时,还需要额外的同步。
static class Person {
// TODO 。。。
}
}
4、Java监视器是什么?Java监视器模式又是什么?
- 在进入和退出同步代码块的字节指令也称为monitorenter和monitorexit,而java的内置锁也称为监视器锁或监视器。
- java监视器模式是从线程封闭原则及其逻辑推论中得出的。遵循java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。
5、写一个Java监视器模式的例子
public class PrivateLock {
private final Object myLock = new Object();
private Widget widget;
void someMethod() {
synchronized (myLock) {
// 访问或修改widget的状态
}
}
static class Widget {}
}
6、什么线程安全性的委托?如果一个类中的所有状态都是线程安全的,那么这个类是线程安全的吗?
- 比如A类中的所有状态都是线程安全的,并且各个状态之间不存在耦合关系,那么这种可以说是将A类的线程安全性委托给了它的几个线程安全的状态变量。
- 即使一个类的所有状态都是线程安全的,此类也不一定是线程安全的。比如这个类含有复合操作,那么仅靠委托线程安全的状态变量并不足以实现线程安全性。需要通过加锁机制来维护不变性条件以确保其线程安全性。
7、如何对现有的线程安全类添加新功能(不能修改源代码)?
- 第一种方式:扩展这个类。在添加新方法时需要考虑到原来这个类的同步策略。
- 这种方式缺点就是太依赖底层实现了:这个类是final的就无法扩展、持有的锁对象是私有的就无法与此类使用相同的同步策略、如果此类的同步策略变了也要跟着变
- 第二种方式:客户端加锁机制。如果要使用这种方式,一定要注意与被保护对象使用同一把锁,否则你加的锁只是个假象。
- 第三种方式:代理。与这个类实现相同的接口,将这个类组合到我的新类中,并对外提供一致的加锁机制来访问这个线程安全的类。此时可以对这个新类添加新的功能,只要加锁机制一致即可。
二、其他
要保证一个类的线程安全性,必须要先了解该类的不变性条件与后验条件。
良好的封装可以更好的保证线程安全,因为所有代码路径都是已知的。
即便状态变量本身并不是线程安全的,也可以通过实例封闭保证安全性。
私有的锁对象可以将锁封装起来,使客户代码无法得到锁,但客户代码可以通过公有方法来访问锁,以便(正确的或不正确的)参与到它的同步策略中。如果客户代码错误的获得了另一个对象的锁,那么可能会产生活跃性问题。此外,要想验证某个公有访问的锁在程序中是否被正确的使用,则需要检查整个程序,而不是单个类。
通过代理模式去添加新功能不要依赖底层实现。
同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁来保护复合操作。
对同步容器类在进行迭代时,如果有线程并发的更改容器中的元素,会抛出ConcurrentModificationException。此时可以通过客户端加锁的方式,在迭代期间不允许其他线程修改容器中的元素。如果不希望在迭代期间对容器进行加锁,那么一种替代的方法就是“克隆”容器,并在副本上进行迭代。(在克隆过程中仍然需要对容器加锁)。在克隆容器时存在显著的性能开销。这种方式的好坏取决于多个因素,包括容器的大小,在每个元素上执行的工作,迭代操作相对于容器其他操作的调用频率,以及在响应时间和吞吐量等方面的需求。
虽然加锁可以防止迭代器抛出ConcurrentModificationException,但是必须要记住在所有对共享容器进行迭代的地方都需要加锁。比如有些迭代操作比较隐蔽,比如对容器做toString操作,其内部也是迭代,如果此时有其他线程修改也会抛出异常。
三、示例代码
1、基于监视器的车辆跟踪器,性能较差
/**
* 车辆跟踪器(线程安全性基于监视器)
*
* 虽然MutablePoint不是线程安全的,但是追踪器是线程安全的。它所包含的Map对象和MutablePoint都未曾发布。
*
* 由于deepCopy是从一个synchronized方法中调用的,因此在执行时间较长的复制操作中,
* tracker的内置锁将一直被占有,当有大量车辆需要追踪时,会严重降低用户界面响应灵敏度。
*/
public class _4_4MonitorVehicleTracker {
/**
* 表示所有车辆的位置,key为车辆ID
*/
private final Map<String, MutablePoint> locations;
/**
* 初始化车辆位置,对输入参数做深拷贝,防止Map逸出。
*/
public _4_4MonitorVehicleTracker(Map<String, MutablePoint> locations) {
this.locations = deepCopy(locations);
}
/**
* 获取所有车辆的位置,返回深拷贝的locations,防止Map逸出。
*/
public synchronized Map<String, MutablePoint> getLocations() {
return deepCopy(locations);
}
/**
* 获取某个车辆的目前的位置,返回的是拷贝的位置对象,防止MutablePoint逸出。
*/
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;
}
/**
* 深拷贝输入的Map
*/
private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m) {
Map<String, MutablePoint> result = new HashMap<>();
for (Map.Entry<String, MutablePoint> entry : m.entrySet()) {
result.put(entry.getKey(), new MutablePoint(entry.getValue()));
}
return Collections.unmodifiableMap(result);
}
/**
* 可变的Point类,表示车辆的坐标
*/
static class MutablePoint {
public int x, y;
public MutablePoint() {
this.x = 0;
this.y = 0;
}
public MutablePoint(MutablePoint p) {
this.x = p.x;
this.y = p.y;
}
}
}
2、基于委托线程安全类的车辆追踪器,性能较高
/**
* 车辆跟踪器(线程安全性基于委托线程安全类)
*
* 由于Point类是不可变的,因而它是线程安全的,不可变的值可以被自由地共享与发布,因此在返回locations不需要复制。
*
* 在这个类中,没有使用任何显式的同步,所有对状态的访问都有ConcurrentHashMap来管理,而且Map的所有键和值都是不可变的。
*
* 在使用监视器模式的车辆追踪器中返回的是车辆的快照,
* 而在使用委托的车辆跟踪器中返回的是一个不可修改但却实时的车辆位置视图。
* 这可能是优点(更新的数据能够实时反映),也可能是缺点(可能导致不一致的车辆位置视图),这取决于具体需求。
*
* 如果需要一个不发生变化的车辆视图,那么getLocations可以返回对locations这个Map对象的一个浅拷贝,
* 因为Map的内容是不可变的,因此只需要复制Map的结构,不需要复制它的内容。
* public Map<String, Point> getLocations() {
* return Collections.unmodifiableMap(new HashMap<>(locations));
* }
*
*/
public class _4_6DelegatingVehicleTracker {
/**
* 保存车辆的位置信息,key为车辆ID,value为车辆坐标
*/
private final ConcurrentMap<String, Point> locations;
/**
* 是locations的一个不可修改的视图
*/
private final Map<String, Point> unmodifiableMap;
public _4_6DelegatingVehicleTracker(Map<String, Point> points) {
locations = new ConcurrentHashMap<>(points);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
/**
* 获取所有车辆的位置信息,返回的map是不可变的,所以也是防止locations直接对外暴露
*/
public Map<String, Point> getLocations() {
return unmodifiableMap;
}
/**
* 返回某个车辆的位置信息,由于Point是一个不可变的类,所以不怕暴露出去,因为别人无法修改
*/
public Point getLocation(String id) {
return locations.get(id);
}
/**
* 更新车辆的位置信息
*/
public void setLocation(String id, int x, int y) {
Point oldPoint = locations.replace(id, new Point(x, y));
if(oldPoint == null) {
throw new IllegalArgumentException("invalid vehicle name: " + id);
}
}
/**
* 表示车辆的坐标
*/
static class Point {
public final int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
}