4.1 设计线程安全的类
程序清单4-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 实例封闭
程序清单4-2 通过封闭机制来确保线程安全
/**
1.PersonSet的状态由HashSet来管理,而HashSet并非线程安全的。
2.但由于mySet是私有的并且不会逸出,因此HashSet被封闭在PersonSet中。
3. 唯一能访问的路径是addPerson 与containsPerson方法,在执行他们时都需要获得PersonSet的内置锁
4. PersonSet完全由他的内置锁保护,因此他是一个线程安全类
*/
@ThreadSafe
public class PersonSet {
private final Set mySet = new HashSet<>();
public synchronized void addPerson(Person p) {
mySet.add(p);
}
public synchronized boolean containsPerson(Person p) {
return mySet.contains(p);
}
}
4.2.1Java监视器模式
遵循Java监视器模式的对象会把对象所有可变状态都封装起来,并由对象自己的内置锁来保护。eg: Vector,HashTable。
Java监视器模式仅仅是一种代码约定,只有自始至终都使用该锁对象,都可以用来保护对象的状态。
程序清单4-3 通过一个私有锁来保护状态
私有锁对象可以将锁封装起来,使用客户代码无法获得锁,但可以通过public方法来访问锁,以便参与到他的同步策略中
public class PrivateLock {
private final Object myLock = new Object();
Widget widget;
void someMethod() {
synchronized (myLock) {
//访问或修改widget的状态
}
}
}
4.2.2 示例:车辆追踪器
程序清单4-4 基于监视器模式车辆追踪
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import lombok.Data;
import lombok.experimental.Accessors;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.NotThreadSafe;
import net.jcip.annotations.ThreadSafe;
/**
虽然MutablePoint不是线程安全的,但是追踪器类是线程安全的
它所包含的Map对象和可变的Point对象都未曾发布。
当需要返回车辆的位置时,返回的是copy的位置对象,在位置容器特别大的情况下会极大地降低性能
*/
@ThreadSafe
public class MonitorVehicleTracker { //vehicle 英[ˈvi:əkl] n. 车辆; 交通工具;
@GuardedBy("this")
private final Map locations;
public MonitorVehicleTracker(Map locations) {
this.locations = locations;
}
//获取监视到的所有车辆的不可修改的副本信息
public synchronized Map getLocations(){
return deepCopy(locations);
}
//通过车辆id获取车辆位置信息的一个副本
public synchronized MutablePoint getLocation(String id){
MutablePoint loc = locations.get(id);
return loc == null ? null : new MutablePoint(loc);
}
//修改某id车辆的位置
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.setX(x).setY(y);
}
//拷贝参数传进来的所有车辆的位置信息并返回一个副本
private static Map deepCopy(Map m) {
Map result = new HashMap<>(); //参数副本
m.keySet().forEach(id->result.put(id, new MutablePoint(m.get(id))));//给副本赋值
return Collections.unmodifiableMap(result);//返回不可修改的map
}
}
//程序清单4-5 与Java.awt.Point类似的可变的Point类(不要这么做)
/**
车辆位置
*/
@Data
@NotThreadSafe
@Accessors(chain = true)
class MutablePoint{ //mutable 英[ˈmju:təbl] adj. 易变的,性情不定的;
private int x;
private int y;
public MutablePoint(MutablePoint mutalbePoint) {
this.x = mutalbePoint.getX();
this.y = mutalbePoint.getY();
}
}
4.3 线程安全性的委托
4.3.1 基于委托的车辆追踪器
程序清单4-7将线程安全委托给ConcurrentHashMap
/**
* 没有任何显示的同步,所有对状态的访问都由ConcurrentMap管理,而且map所有的键值都是不可变的
* ConcurrentMap的key 是final修饰的
*
* 由于Point类是不可变的,因此它是线程安全的。不可变的值可以被自由地分享与发布,因此返回location时不需要复制。
*
* 注释1、2意味着:
* 如果线程A调用【getLocations】而线程B在随后修改了【setLocation】某些点的位置,
* 那么在返回给线程A的Map中将反映出这些变化
* 优点:更新数据
* 缺点:可能导致不一致的车辆位置视图
*
*/
@ThreadSafe
public class DelegatingVehicleTracker {
private final ConcurrentMap locations;
private final Map unmodifiableMap;
public DelegatingVehicleTracker(Map points) {
locations = new ConcurrentHashMap<>(points);
unmodifiableMap = Collections.unmodifiableMap(points);
}
//1、在使用监视器模式的车辆追踪器中 返回的是 车辆的位置的快照
public Map getLocations() {
// return Collections.unmodifiableMap(new HashMap(locations)); //返回locations的静态拷贝
return unmodifiableMap; //返回实时拷贝
}
//2、而在使用委托的车辆追踪器中 返回的是 一个不可修改 但却是实时的车辆位置视图
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 : ");
}
}
4.3.2 独立的状态变量
程序清单4-9 将线程安全性委托给多个状态变量
/**
* keyListeners,mouseListeners每个状态都是线程安全的,并且两个状态之间不存在耦合关系
* 所以VisualComponent可以将他的线程安全性委托给keyListeners和mouseListeners
*/
public class VisualComponent {
//CopyOnWriteArrayList是一个线程安全列表,特别适合于管理监听器列表
private final List keyListeners = new CopyOnWriteArrayList<>();
private final List mouseListeners = new CopyOnWriteArrayList<>();
public void addKeyListener(KeyListener listener) {
keyListeners.add(listener);
}
public void addMouseListener(MouseListener mouseListener) {
mouseListeners.add(mouseListener);
}
public void removeKeyListener(KeyListener keyListener) {
keyListeners.remove(keyListener);
}
public void removeMouseListener(MouseListener mouseListener) {
mouseListeners.remove(mouseListener);
}
}
4.3.3 当委托失效时
程序清单4-10 NumberRange类并不足以保护它的不变性条件(不要这么做)
/**
* 假设:取值范围是(0,10),如果一个线程调用setLower(5), 一个线程调用setUpper(4),
* 那么:在一些错误的时序中,这两个调用都将通过检查,并且都能设置成功。
* 结果:得到的取值范围就是(5,4),这是一个无效的状态
* 所以:虽然AtomicInteger是线程安全的,但经过组合得到的类确不是。
* 由于:lower和upper不是彼此独立的,
* 因此:NumberRange不能将线程安全性委托给它的线程安全状态变量。
*/
import java.util.concurrent.atomic.AtomicInteger;
public class NumberRange {
//不变性条件 lower < upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) {
//注意:不安全的“先检查后执行”
if(i > upper.get())
throw new IllegalArgumentException("cant't set lower to " + i + " > upper");
lower.set(i);
}
public void setUpper(int i) {
//注意:不安全的“先检查后执行”
if(i < lower.get())
throw new IllegalArgumentException("cant't set lower to " + i + " < lower");
lower.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
4.3.5 示例:发布状态的车辆追踪器
程序清单4-11 线程安全且可变的Point类
/**
* 【发布底层的可变状态】
* 当把线程安全性委托给某个对象的底层状态变量时,
* 在什么情况下才可以发布这些变量从而使其它类可以修改他们?
* 答案仍然取决于在类中对这些变量施加了哪些不变性条件
*/
@ThreadSafe
public class SafePoint {
private int x,y;
public SafePoint(int x, int y) {
this.x = x;
this.y = y;
}
private SafePoint(int[] a) { this(a[0],a[1]); }
/*如果将拷贝的构造函数改为this.(p.x,p.y),则会产生竞态条件,因为其是public
*而private的构造函数则可以避免这种竞态条件
*/
public SafePoint(SafePoint p) { this(p.get()); }
/*
* 如果为x和y分别提供get 方法,那么获得这2个不同坐标的操作之间,
* x,y的值发生变化,从而导致调用者看到不一致的值:车辆从来没有到达过(x,y)。
*
* 通过使用SafePoint,可以构造一个发布其底层可变状态的车辆追踪器,
* 还能确保其线程安全性不被破坏。
*/
public synchronized int[] get() {
return new int[] {x,y};
}
public synchronized void set(int x,int y) {
this.x = x;
this.y = y;
}
}
程序清单4-12 安全发布底层状态的车辆追踪器
/**
* 程序清单4-7的差别:
* 1、底层状态类不同
* 4-7: Point:状态不可变的
* 4-12:SafePoint:线程安全且底层状态可变的
* 2.
* 4-7:setLocation()中replace()
* 4-12:setLocation()中containKey()
*/
@ThreadSafe
public class PublishingVehicleTracker {
private final Map locations;
private final Map unmodifyMap;
public PublishingVehicleTracker(Map locations) {
this.locations = new ConcurrentHashMap(locations);
this.unmodifyMap = Collections.unmodifiableMap(this.locations);
}
public Map getLocations() {
return unmodifyMap;
}
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);
}
}
4.4 在现有的线程安全类中添加功能
程序清单4-13 扩展Vector并增加一个 “若没有则添加”方法
//并非所有的类都像Vector一样将状态向子类公开,因此也就不适合采用这种办法
public class BetterVector extends Vector {
/**
* absent 英[ˈæbsənt]
* adj. 缺席的,不在场的; 缺少的,缺乏的; 不在意的,茫然的;
* vt. 缺席,不参加; 不在;
*/
public synchronized boolean putIfAbsent(E x) {
boolean absent = !contains(x);
if(absent)
add(x);
return absent;
}
}
4.4.1 客户端加锁机制
程序清单4-14 非线程安全的“若没有则添加”(不要这么做)
/**
* 【非线程安全】因为在错误的锁上进行了同步
* 方法上的synchronized 是内置锁
* 而Collections.synchronizedList(List list) 的锁是:
* Collectons.SynchronizedCollection.mutex (final)
* 不同的锁意味着 putIfAbsent相对于List的其它操作来说并不是原子的
*/
@NotThreadSafe
public class ListHelper {
public List list = Collections.synchronizedList(new ArrayList());
/**
* absent 英[ˈæbsənt]
* adj. 缺席的,不在场的; 缺少的,缺乏的; 不在意的,茫然的;
* vt. 缺席,不参加; 不在;
*/
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if(absent)
list.add(x);
return absent;
}
}
程序清单4-15 通过客户端加锁来实现”若没有则添加“
客户端加锁是指:
对于使用某个对象X的客户端代码,使用X对象本身用于保护其状态的锁来保护这段客户代码。
要使用客户端加锁,你必须要知道对象X使用的是哪一个锁
/**
* 通过添加一个原子操作类拓展类是脆弱的(BetterVector),因为它类的加锁代码分布到多个类中,
* 然而,客户端加锁却更加脆弱,因为它将客户端类的加锁代码放到了与客户端完全无关的其它类中
* 当在那些并不曾诺遵循加锁策略的类(本例中的list)上使用客户端加锁时,要特别小心
*/
@ThreadSafe
public class ListHelper {
public List list = Collections.synchronizedList(new ArrayList());
public boolean putIfAbsent(E x) {
synchronized (list) { //使用list自身的锁,可能是list的内置锁,也可能是其类中定义的其它对象
boolean absent = !list.contains(x);
if(absent)
list.add(x);
return absent;
}
}
}
4.4.2 组合
为现有的类添加一个原子操作时,有一种更好的办法:【组合】
程序清单4-16 通过组合实现”若没有则添加“
/**
* ImprovedList通过自身的内置锁增加了一层额外的加锁。
* 它并不关心底层的List是否是线程安全的,即使List不是线程安全的或者修改了它的加锁实现,
* ImprovedList也会提供一致的加锁机制来实现线程安全性。
* 虽然额外的同步层可能导致轻微的性能损失,但与模拟另一个对象的加锁策略相比,【ImprovedList更为健壮】。
* 事实上,我们使用了Java监视器模式来封装现有的List,并且只要在类中拥有指向底层List的唯一外部引用,就能确保安全。
*/
public class ImprovedList implements List {
private final List list;
public ImprovedList(List list) {
this.list = list;
}
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if(absent)
list.add(x);
return absent;
}
@Override
public synchronized void clear() {
list.clear();
}
//......按照类似的方式委托List的其它方法
}