Effective Java 3rd (九)

目录

  1. 并发工具优于 wait 和 notify
  2. 文档应包含线程安全属性
  3. 明智审慎的使用延迟初始化
  4. 不要依赖线程调度器
  5. 优先选择 Java 序列化的替代方案
  6. 非常谨慎地实现 Serializable
  7. 考虑使用自定义的序列化形式
  8. 保护性的编写 readObject 方法
  9. 对于实例控制,枚举类型优于 readResolve
  10. 考虑用序列化代理代替序列化实例

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 方法时,就应该考虑使用序列化代理模式。

此模式需要额外的开销,因为多了个代理类。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值