《Java并发编程实战》一书的第4章中提到了“客户端加锁”,针对该内容记录一些笔记加深理解。
客户端加锁:
对于使用某个对象x的客户端代码,使用“x本身用于保护其状态的锁”来保护这段客户端代码。
要使用客户端加锁,必须知道x对象使用的是哪一个锁。
通过客户端加锁机制,实现常见的“若没有则添加”的操作:
向容器中添加元素之前,首先检查元素是否已经存在,如果存在就不再添加;因此,如果要保证线程安全就要求“若没有则添加”是原子操作。
示例1:客户端加锁使用的锁不对,就不能保证线程安全
@NotThreadSafe
public class ListHelper<E> {
public List<E> list =
Collections.synchronizedList(new ArrayList<E>());
...
public synchronized boolean putIfAbsent(E x) {
boolean absent = !List.contains(x);
if(absent)
list.add(x);
return absent;
}
}
上述代码中,list是线程安全synchronizedList,putIfAbsent方法也使用了同步,为什么“若没有则添加”的操作不是线程安全的?
原因是,synchronized获取到的锁是ListHelper的内置锁,而不是list的内置锁,同时list又是pubilc的,可以被其他线程访问到。并发条件下可能会出现在A线程的putIfAbsent方法中判断x不存在,但是B线程也判断x不存在而且向list中添加了x,随后在A线程的putIfAbsent也会添加x,出现list中有两个x的情况。
因此,synchronized无法保证“若没有则添加”是线程安全的。
示例2:使用正确的锁,就可以实现线程安全:
@ThreadSafe
public class ListHelper<E> {
public List<E> list =
Collections.synchronizedList(new ArrayList<E>());
...
public boolean putIfAbsent(E x) {
synchronized(list) {
boolean absent = !List.contains(x);
if(absent)
list.add(x);
return absent;
}
}
}
虽然list依然是public的,但synchronized持有了list的内置锁时,其他线程就无法拿到list的锁了,因此能保证“若没有则添加”操作是线程安全的。
示例3:使用“组合”实现线程安全的“若没有则添加”:
@ThreadSafe
public class ImprovedList<T> implements List<T> {
private final List<T> list;
// 组合
public ImproveList(List<T> list) {
this.list = list;
}
public synchronized boolean putIfAbsent(T x) {
boolean contains = list.contains(x);
if (contains) {
list.add(x);
}
return !contains;
}
}
使用上述方式更加符合“合成复用”的设计原则:
- 组合或聚合关系是指将已有的对象(也称为成员对象)纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使成员对象的内部实现细节对新对象不可见,所以也称“黑箱”复用
- 如果两个类是“Has-A”的关系应使用组合或聚合;如果是“Is-A”的关系可以使用继承
- 组合或聚合相对于继承关系,耦合度更低,因此推荐优先使用