目录
- 并发工具优于 wait 和 notify
- 文档应包含线程安全属性
- 明智审慎的使用延迟初始化
- 不要依赖线程调度器
- 优先选择 Java 序列化的替代方案
- 非常谨慎地实现 Serializable
- 考虑使用自定义的序列化形式
- 保护性的编写 readObject 方法
- 对于实例控制,枚举类型优于 readResolve
- 考虑用序列化代理代替序列化实例
81. 并发工具优于 wait 和 notify
正确地使用 wait 和 notify 比较困难,就应该用更高级的并发工具来代替。
wait 的标准用法:
// The standard idiom for using the wait method
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
// (Releases lock, and reacquires on wakeup)
... // Perform action appropriate to condition
}
用if跟wait方法不是原子操作,所以用while确保正确判断。
notify会随机唤醒一个线程,notifyAll则会唤醒所有线程,保守方法是用notifyAll方法,因为notify只唤醒一个,有可能其它恶意方法或者不小心的操作使唤醒的线程再度进入wait,从而无限等待程序卡住。
82. 文档应包含线程安全属性
线程安全级别:
不可变的
这个类的实例看起来是常量。不需要外部同步。示例包括 String、Long 和 BigInteger(详见第 17
条)。
无条件线程安全
该类的实例是可变的,但是该类具有足够的内部同步,因此无需任何外部同步即可并发地使
用该类的实例。例如 AtomicLong 和 ConcurrentHashMap。
有条件的线程安全
与无条件线程安全类似,只是有些方法需要外部同步才能安全并发使用。示例包括Collections.synchronized 包装器返回的集合,其迭代器需要外部同步。
非线程安全
该类的实例是可变的。要并发地使用它们,客户端必须使用外部同步来包围每个方法调用(或调用序列)。这样的例子包括通用的集合实现,例如 ArrayList 和 HashMap。
线程对立
即使每个方法调用都被外部同步包围,该类对于并发使用也是不安全的。线程对立通常是由于在不
同步的情况下修改静态数据而导致的。没有人故意编写线程对立类;此类通常是由于没有考虑并发性而导致的。当发现类或方法与线程不相容时,通常将其修复或弃用。第 78 条中的generateSerialNumber 方法在没有内部同步的情况下是线程对立的,如第 322 页所述。
83. 明智审慎的使用延迟初始化
延迟初始化是延迟字段的初始化,直到需要它的值。如果不需要该值,则不会初始化字段。
如果您使用延迟初始化,请使用同步访问器
// Lazy initialization of instance field - synchronized accessor
private FieldType field;
private synchronized FieldType getField() {
if (field == null)
field = computeFieldValue();
return field;
}
如果需要在静态字段上使用延迟初始化来提高性能,使用 lazy initialization holder class 模式
// Lazy initialization holder class idiom for static fields
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
private static FieldType getField() { return FieldHolder.field; }
如果需要使用延迟初始化来提高实例字段的性能,请使用双重检查模式,必须注意属性需要用volatile关键字,否则可能出现指令重排序。
// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null) { // First check (no locking)
synchronized(this) {
if (field == null) // Second check (with locking)
field = result = computeFieldValue();
}
}
return result;
}
84. 不要依赖线程调度器
不要依赖线程调度器来判断程序的正确性。生成的程序既不健壮也不可移植。因此,不要依赖
Thread.yield 或线程优先级。这些工具只是对调度器的提示。线程优先级可以少量地用于提高已经工作的程序的服务质量,但绝不应该用于「修复」几乎不能工作的程序。
线程不应该处于忙等待状态(busy-wait),而应该反复检查一个共享对象,等待它的状态发生变化。错误示范:
public void await() {
while (true) {
synchronized(this) {
if (count == 0)
return;
}
}
}
85. 优先选择 Java 序列化的替代方案
序列化是危险的,应该避免。可以考虑json或者protobuf等方案来替代java自带的序列化。
static byte[] bomb() {
Set<Object> root = new HashSet<>();
Set<Object> s1 = root;
Set<Object> s2 = new HashSet<>();
for (int i = 0; i < 100; i++) {
Set<Object> t1 = new HashSet<>();
Set<Object> t2 = new HashSet<>();
t1.add("foo"); // Make t1 unequal to t2
s1.add(t1); s1.add(t2);
s2.add(t1); s2.add(t2);
s1 = t1;
s2 = t2;
}
return serialize(root); // Method omitted for brevity
}
这个方法调用100次,每次都会增加t1的"foo"跟s1和s2中的hashset,反序列化时会计算哈希码,消耗cpu资源,影响整个应用的性能。
86. 非常谨慎地实现 Serializable
实现 Serializable 接口的一个主要代价是,一旦类的实现被发布,它就会降低更改该类实现的灵活性。
1、默认的序列化形式,类中私有的包以及私有实例字段将成为其导出 API 的一部分(私有可见性变成可见)。
2、接受默认的序列化形式,然后更改了类的内部实现,则会导致与序列化形式不兼容,可以实现 ObjectOutputStream.putFields 或 ObjectInputStream.readFields方法,但是会增加开发的初始成本。
3、如果你没有通过声明一个名为 serialVersionUID 的静态 final long 字段来指定这个标识符,那么系统将在运行时对类应用加密散列函数(SHA-1)自动生成它,添加,删除方法也会改变这个值,破坏了UID,造成运行时InvalidClassException异常。
增加了出现 bug 和安全漏洞的可能性(第85项)
序列化提供了创建实例的另外一种方式,反序列化提供除构造器之外的另一种初始化值的入口。
实现 Serializable 接口的第三个代价是,它增加了与发布类的新版本相关的测试负担。
可序列化的类修改时,需要测试反序列化是否也能成功。
实现 Serializable 接口并不是一个轻松的决定。
需要权衡是否可序列化,一般线程类不会设计为可序列化。
为继承而设计的类(详见第 19 条)很少情况适合实现 Serializable 接口,接口也很少情况适合扩展它
子类也变成可序列化了
内部类(详见第 24 条)不应该实现 Serializable。
87. 考虑使用自定义的序列化形式
在没有考虑默认序列化形式是否合适之前,不要接受它。
后续版本需要一直支持序列化
如果对象的物理表示与其逻辑内容相同,则默认的序列化形式可能是合适的。
如果对象描述的是简单类型,例如人的名称等,则是合适的
// Good candidate for default serialized form
public class Name implements Serializable {
/**
* Last name. Must be non-null.
* @serial
*/
private final String lastName;
/**
* First name. Must be non-null.
* @serial
*/
private final String firstName;
/**
* Middle name, or null if there is none.
* @serial
*/
private final String middleName;
... // Remainder omitted
}
即使你认为默认的序列化形式是合适的,你通常也必须提供 readObject 方法来确保不变性和安全性。
如下代码不适合用默认序列化
// Awful candidate for default serialized form
public final class StringList implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry implements Serializable {
String data;
Entry next;
Entry previous;
} ... // Remainder omitted
}
这个类有如下缺点:
1、它将导出的 API 永久地绑定到当前的内部实现
以后只能是链表这种数据结构实现。
2、它会占用过多的空间。
链表中的对象可能过大
3、它会消耗过多的时间
遍历开销大
4、它可能导致堆栈溢出
/
/ StringList with a reasonable custom serialized form
public final class StringList implements Serializable {
private transient int size = 0;
private transient Entry head = null;
// No longer Serializable!
private static class Entry {
String data;
Entry next;
Entry previous;
}
// Appends the specified string to the list
public final void add(String s) { ... }
/**
* Serialize this {@code StringList} instance.
* *
@serialData The size of the list (the number of strings
* it contains) is emitted ({@code int}), followed by all of
* its elements (each a {@code String}), in the proper
* sequence.
*/
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeInt(size);
// Write out all elements in the proper order.
for (Entry e = head; e != null; e = e.next)
s.writeObject(e.data);
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
int numElements = s.readInt();
// Read in all elements and insert them in list
for (int i = 0; i < numElements; i++)
add((String) s.readObject());
} ... // Remainder omitted
}
88. 保护性的编写 readObject 方法
readObject相当于一个构造器,需要对所有属性进行验证。
条目50的Period类
// Immutable class that uses defensive copying
public final class Period {
private final Date start;
private final Date end;
/**
* @param start the beginning of the period
* @param end the end of the period; must not precede start
* @throws IllegalArgumentException if start is after end
* @throws NullPointerException if start or end is null
*/
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(start + " after " + end);
}
public Date start () {
return new Date(start.getTime());
}
public Date end () {
return new Date(end.getTime());
}
public String toString() {
return start + " - " + end;
} .
.. // Remainder omitted
}
// readObject method with defensive copying and validity checking
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
// Defensively copy our mutable components
start = new Date(start.getTime());
end = new Date(end.getTime());
// Check that our invariants are satisfied
if (start.compareTo(end) > 0)
throw new InvalidObjectException(start +" after "+ end);
}
需要对readObject方法进行保护性拷贝,才能防止伪造字节流,对类攻击。
序列化类的类建议
1、类中的对象引用字段必须保持为私有属性,要保护性的拷贝这些字段中的每个对象。不可变类中的可变组件就属于这一类别。
2、对于任何约束条件,如果检查失败就抛出一个 InvalidObjectException 异常。这些检查动作应该跟在所有的保护性拷贝之后。
3、如果整个对象图在被反序列化之后必须进行验证,就应该使用 ObjectInputValidation 接口(本书没有讨论)。无论是直接方法还是间接方法,都不要调用类中任何可被覆盖的方法
89. 对于实例控制,枚举类型优于 readResolve
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
这个类实现序列化接口之后,有两种方法可以防止攻击
1、INSTANCE 声明为transient
2、改为枚举类
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
}
90. 考虑用序列化代理代替序列化实例
序列化代理模式:
// Serialization proxy for Period class
private static class SerializationProxy implements Serializable {
private final Date start;
private final Date end;
SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
private static final long serialVersionUID =
234098243823485285L; // Any number will do (Item 87)
}
在Period类添加方法,方便创建代理
// writeReplace method for the serialization proxy pattern
private Object writeReplace() {
return new SerializationProxy(this);
}
为了防止攻击者获取代理对象,在Period类中再添加方法
/
/ readObject method for the serialization proxy pattern
private void readObject(ObjectInputStream stream)throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
正如保护性拷贝方法一样(详见 88 条),序列化代理方式可以阻止伪字节流的攻击(详见 88 条)以及内部字段的盗用攻击(详见 88 条)。
当你发现必须在一个不能被客户端拓展的类上面编写 readObject 或者 writeObject 方法时,就应该考虑使用序列化代理模式。
此模式需要额外的开销,因为多了个代理类。