目录
不可变对象
JMM中final域的内存语义
实例限制
委托线程安全
向线程安全类添加功能
满足不可变对象的有三点:
1.它的状态(对象关联的成员属性)不能在构造后被修改,除构造方法外没有提供修改状态的代码途径。
2.所有域都是final的。
3.它被正确地创建,即:构造期间没有发生过this引用的逸出。
下面的讲解摘自外网:http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-2.html
1.the object is transitively reachable from a final field 表示状态的对象(final字段)可以通过final字段来传递式地获取到。
可以通过跟随零个或多个引用从final字段到达您想要的不可变的数据。如下类你可以从bytes字段访问字节数组:
class String {
private final byte[] bytes;
public String(byte[] value) {
byte[] newArray = new byte[value.length];
System.arraycopy(value, 0, newArray, 0, value.length);
bytes = newArray;
}
}
而如下类,仍然可以通过final的bytes字段来传递式地访问字节数组。
class String {
static class BytesHolder {
byte[] bytes;
}
final BytesHolder holder;
}
实际上,您可以根据需要添加任意数量的间接层 - 只要您可以通过跟踪字段中的引用传递式地访问那里,它仍然可以从字段中被传递式地访问到。
2.the object has not changed since the final field was set 自从final字段被设置以来,表示状态的对象(final字段)从没有改变过。
这里有个String类,其字节不再是不可变的。如果两个线程调用setZerothElt方法,则可能上演各种糟糕的线程马戏。
class String {
private final byte[] bytes;
public String(byte[] value) {
byte[] newArray = new byte[value.length];
System.arraycopy(value, 0, newArray,
0, value.length);
bytes = newArray;
}
public setZerothElt(byte b) {
byte[0] = b;
}
}
事实证明,如果您希望数据是线程安全的,那么在设置final字段后就不应该更改它,不应该提供修改final字段的代码途径。而且,在设置final字段后还可以更改其状态,不可变对象在语义上只是在构造函数结束后无法更改其final字段。
class String {
// Absolutely fine.
private final byte[] bytes;
public String(byte[] value) {
bytes = new byte[value.length];
System.arraycopy(value, 0,
bytes, 0, value.length);
}
}
您可以将构造函数的结尾视为“冻结”操作,对象的final字段的值必须在构造函数的末尾冻结。
3.A reference to the object containing the final field did not escape the constructor 对包含final字段的对象的引用没有在构造函数里逸出。
逸出构造函数意味着在构造函数中,你存储了一个对正在构造的对象的引用,而另一个线程获取了这个引用,并且正在使用它。例如:
class String {
// Don't do this.
static Set allStrings =
Collections.synchronizedSet(new HashSet());
private final byte[] bytes;
public String(byte[] value) {
allStrings.add(this);
bytes = new byte[value.length];
System.arraycopy(value, 0,
bytes, 0, value.length);
}
}
另一个线程使用allStrings中的”this”时,bytes这个final字段可能还没准备好数据(还没达到想要的状态),你可能想到把allStrings.add(this);放到构造方法的结尾处,所以我们又有如下逸出构造方法的例程:
class String {
// Don't do this, either.
static String lastConstructed;
private final byte[] bytes;
public String(byte[] value) {
bytes = new byte[value.length];
System.arraycopy(value, 0,
bytes, 0, value.length);
lastConstructed = this;
}
}
当读到lastConstructed=this时,你会一直认为它总会给你最后构造的String实例。设想一下多线程来使用这个lastConstructed的情景,在实际测试中线程有可能会看到未初始化的bytes数据,这是编译器愚弄了你,编译器把lastConstructed = this;这句移动到了bytes = new byte[value.length];之前。
所以,我们要避免不可变对象的引用逸出构造方法。
不可变对象构造方法中为什么不能“逃逸”this?其实这是final域的内存语义要求(JMM的规范),它有一套读写final域的重排序规则,如下:
1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
也就是:在写final域之后构造方法返回之前编译器会插入StoreStore内存屏障指令,在读不可变对象引用之后读final域之前编译器会插入LoadLoad内存屏障指令。
至于什么是内存屏障指令,什么是重排序,话题会很长,这里不展开,具体参见《并发编程的艺术》一书。
封装简化了类的安全化工作,封装就是一种限制,因为当一个对象被另一个对象封装时,所有访问被封装对象的代码途径全部在封装对象中可知,而对外部整个系统来说暴露只是访问被封装对象的接口,并没有暴露被封装对象,这样地限制在类实例中,再与适当的锁策略相结合,就可以确保程序以线程安全的方式访问其他非线程安全对象。
总结一句,将数据封装在对象内部,把对数据的访问限制在对象的方法上,更容易确保线程在访问数据时总能获得正确的锁。即,封装更易于做线程安全化工作。
可以把对象(数据)限制在类实例(比如私有类成员)、语汇范围(比如本地变量)、线程(比如对象在线程内部从一个方法传递到另一个方法,该对象不被跨线程共享)中。下面的例子示范了限制和内部锁一起确保了一个类的线程安全性:
/**
* PersonSet
*
* Using confinement to ensure thread safety
*
* @author Brian Goetz and Tim Peierls
*/
@ThreadSafe
public class PersonSet {
@GuardedBy("this") 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);
}
interface Person {
}
}
封装或者可以说是包装。平台类库中有许多限制和锁结合的例子,就是Collections.synchronized…这样的方法产生的包装器对象,只要包装器对象有着对被包装对象的唯一引用,那么包装器对象就是线程安全的类。
发布其他对象也可能间接地发布了本应该受限制的被封装(包装)对象,比如你发布了被封装对象的迭代器,或者你发布了一个内部类实例(内部类实例有使用被封装对象的引用),这样被封装对象就逸出了。
看举例:
基于监视器(内部锁)的实现类:
/**
* MonitorVehicleTracker
*
* Monitor-based vehicle tracker implementation
*
* @author Brian Goetz and Tim Peierls
*/
@ThreadSafe
public class MonitorVehicleTracker {
@GuardedBy("this") private final Map locations;
public MonitorVehicleTracker(Map locations) {
this.locations = deepCopy(locations);
}
public synchronized Map 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 deepCopy(Map m) {
Map result = new HashMap();
for (String id : m.keySet())
result.put(id, new MutablePoint(m.get(id)));
return Collections.unmodifiableMap(result);
}
}
以及非不可变对象:
/**
* MutablePoint
*
* Mutable Point class similar to java.awt.Point
*
* @author Brian Goetz and Tim Peierls
*/
@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;
}
}
为了配合讲解委托是什么,我们修改MutablePoint为不可变对象Point(它是线程安全的):
/**
* Point
*
* Immutable Point class used by DelegatingVehicleTracker
* 这个类在构造完成后,没有提供修改其状态的接口(代码途径)
* @author Brian Goetz and Tim Peierls
*/
@Immutable
public class Point {
public final int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
然后我们再把MonitorVehicleTracker的线程安全问题委托给ConcurrentMap和unmodifiableMap去考虑:
/**
* DelegatingVehicleTracker
*
* Delegating thread safety to a ConcurrentHashMap
*
* @author Brian Goetz and Tim Peierls
*/
@ThreadSafe
public class DelegatingVehicleTracker {
private final ConcurrentMap locations;
private final Map unmodifiableMap;
public DelegatingVehicleTracker(Map points) {
locations = new ConcurrentHashMap(points);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map 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);
}
// Alternate version of getLocations (Listing 4.8)
public Map getLocationsAsStatic() {
return Collections.unmodifiableMap(
new HashMap(locations));
}
}
注:如果我们还用MutablePoint去代替Point,那么getLocations和getLocation就发布了一个非线程安全的可变的引用,破坏了封装性,其实就是暴露了对被封装对象的一部分访问,也就是没有封装好,也就是实例限制没做到位。
也可以将线程安全委托到多个状态变量上。例如:
/**
* VisualComponent
*
* Delegating thread safety to multiple underlying state variables
*
* @author Brian Goetz and Tim Peierls
*/
public class VisualComponent {
private final List keyListeners
= new CopyOnWriteArrayList();
private final List mouseListeners
= new CopyOnWriteArrayList();
public void addKeyListener(KeyListener listener) {
keyListeners.add(listener);
}
public void addMouseListener(MouseListener listener) {
mouseListeners.add(listener);
}
public void removeKeyListener(KeyListener listener) {
keyListeners.remove(listener);
}
public void removeMouseListener(MouseListener listener) {
mouseListeners.remove(listener);
}
}
注:在这个类中,2个list都是线程安全的类,而且2个状态之间没有不变约束的耦合,所以该类的委托行为是线程安全的,该类是线程安全的。然而如下例:
/**
* NumberRange
*
* Number range class that does not sufficiently protect its invariants
*
* @author Brian Goetz and Tim Peierls
*/
public class NumberRange {
// INVARIANT: lower <= upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) {
// Warning -- unsafe check-then-act
if (i > upper.get())
throw new IllegalArgumentException("can't set lower to " + i + " > upper");
lower.set(i);
}
public void setUpper(int i) {
// Warning -- unsafe check-then-act
if (i < lower.get())
throw new IllegalArgumentException("can't set upper to " + i + " < lower");
upper.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
假设当前值域是(0,5),一个线程setLower(5),另一个线程setUpper(4),在一些偶发的时段里,这两个方法都通过了检查,并且使值域转换成了(5,4)这些无效的状态。所以,虽然AtomicInteger是线程安全的,但组合起来不是安全的,因为lower和upper之间有不变约束的耦合,所以可以通过枷锁来维护状态变量之间的不变约束。
总结一句话,如果一个类由多个彼此独立的线程安全的状态变量组成,并且类的方法不包括任何无效状态的转换,则该类可以线程安全地委托给这些状态变量。
另外,如果一个状态变量是线程安全的,且没有任何的不变约束限制它,那么它可以安全地发布。例如,VisualComponent中的2个keyListeners列表。
重用能够降低开发的难度、风险(因为已有的组件已经经过测试)和维护成本。而一个类有时候只支持我们的大部分操作,没法满足我们需要的全部操作,这时候,我们要在不破坏线程安全的前提下,向他添加一个操作。
例如我们向Vector添加一个操作:
/**
* BetterVector
*
* Extending Vector to have a put-if-absent method
*
* @author Brian Goetz and Tim Peierls
*/
@ThreadSafe
public class BetterVector extends Vector {
// When extending a serializable class, you should redefine serialVersionUID
static final long serialVersionUID = -3963416950630760754L;
public synchronized boolean putIfAbsent(E x) {
boolean absent = !contains(x);
if (absent)
add(x);
return absent;
}
}
有时候我们添加的安全操作,会出现同步幻象,例如:
/**
* ListHelder
*
* Examples of thread-safe and non-thread-safe implementations of
* put-if-absent helper methods for List
*
* @author Brian Goetz and Tim Peierls
*/
@NotThreadSafe
class BadLsitHelper {
public List list = Collections.synchronizedList(new ArrayList());
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
注:putIfAbsent操作和list的操作使用了不同的锁,所以也没法保证线程安全。然而如下:
@ThreadSafe
class GoodListHelper {
public List list = Collections.synchronizedList(new ArrayList());
public boolean putIfAbsent(E x) {
synchronized (list) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
}
总结,扩展安全方法和客户端加锁(GoodListHelper)他们所得类的行为与基类(Vector和SynchronizedList)的实现之间都存在耦合,实现时一定要小心谨慎。而且,扩展会破坏基类的封装性,客户端加锁会破坏同步策略的封装性(实例限制)。
然而为了更好地添加一个安全操作,我们引入如下的组合模式的实现:
/**
* ImprovedList
*
* Implementing put-if-absent using composition
*
* @author Brian Goetz and Tim Peierls
*/
@ThreadSafe
public class ImprovedList implements List {
private final List list;
/**
* PRE: list argument is thread-safe.
*/
public ImprovedList(List list) { this.list = list; }
public synchronized boolean putIfAbsent(T x) {
boolean contains = list.contains(x);
if (contains)
list.add(x);
return !contains;
}
// Plain vanilla delegation for List methods.
// Mutative methods must be synchronized to ensure atomicity of putIfAbsent.
public int size() {
return list.size();
}
public boolean isEmpty() {
return list.isEmpty();
}
public boolean contains(Object o) {
return list.contains(o);
}
public Iterator iterator() {
return list.iterator();
}
public Object[] toArray() {
return list.toArray();
}
public T[] toArray(T[] a) {
return list.toArray(a);
}
public synchronized boolean add(T e) {
return list.add(e);
}
public synchronized boolean remove(Object o) {
return list.remove(o);
}
public boolean containsAll(Collection> c) {
return list.containsAll(c);
}
public synchronized boolean addAll(Collection extends T> c) {
return list.addAll(c);
}
public synchronized boolean addAll(int index, Collection extends T> c) {
return list.addAll(index, c);
}
public synchronized boolean removeAll(Collection> c) {
return list.removeAll(c);
}
public synchronized boolean retainAll(Collection> c) {
return list.retainAll(c);
}
public boolean equals(Object o) {
return list.equals(o);
}
public int hashCode() {
return list.hashCode();
}
public T get(int index) {
return list.get(index);
}
public T set(int index, T element) {
return list.set(index, element);
}
public void add(int index, T element) {
list.add(index, element);
}
public T remove(int index) {
return list.remove(index);
}
public int indexOf(Object o) {
return list.indexOf(o);
}
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
public ListIterator listIterator() {
return list.listIterator();
}
public ListIterator listIterator(int index) {
return list.listIterator(index);
}
public List subList(int fromIndex, int toIndex) {
return list.subList(fromIndex, toIndex);
}
public synchronized void clear() { list.clear(); }
}
ImprovedList使用自己的内部锁,引入了一个新的锁层,它并不关心被封装的list是否线程安全,即使额外的一层锁会带来一些微弱的性能损失,但相比于客户端加锁去模拟另一个对象的锁策略而言,ImprovedList并不那么脆弱。只要使用监视器模式封装一个已有的list,并且唯一持有list的引用,那么就能保证线程安全。
转载笔记请注明出处。