4对象的组合

我们不需要每次设计的类都去考虑是否线程安全,而是希望将一些现有线程安全的组件组合为更大规模的组件或程序。

设计线程安全的类

在设计线程安全类的过程中,需要包含以下三个基本要素:

  • 找出构成对象状态的所有变量
  • 找出约束状态变量的所有不变性条件
  • 建立对象状态并发访问的管理策略
对象中的状态是有基本数据类型或者引用数据类型域构成的。
//使用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;
	}
}
同步策略(Synchronization Policy)定义了如何在不违背对象不变条件或后验条件的情况下对其状态的访问操作进行协同。不变条件,用于判断对象的状态是有效的还是无效的。例如,上面的例子程序中,value是long类型的变量,其状态空间从Long.MIN_VALUE到Long.MAX_VALUE,value值存在一个限制就是不能是负值。这个就是不变条件。后验条件用来判断状态的迁移是否是有效的。如果value的当前值为17,那么下一个有效状态只能是18。当下一个状态要依赖当前状态时,这个操作必须是一个复合操作。(我们将“先检查后执行”以及“读取——修改——写入”等操作统称为复合操作)
如果不了解一个对象的不变性条件与后验条件,那么就不能确保线程安全性。要满足状态变量的有效值或状态转换上的各种约束条件,就需要借助于原子性与封装性。
依赖状态的操作
如果在某个操作中包含有基于状态的先验条件,那么这个操作就称为依赖状态的操作。
状态的所有权
书中的这段话不是很理解同时也觉得书中没有讲明白——如果以某个对象为根节点构造一张对象图,那么该对象的状态将是对象图中所有对象包含的域的一个子集。为什么是一个“子集”?在从对象可以达到的所有域中,需要满足哪些条件才不属于对象状态的一部分?( 如果有理解的大神可以评论指点下,不胜感激)
实例封闭
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
通过封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用非线程安全的对象。
//通过封闭机制来确保现场安全
ThreadSafe
public class  PersonSet{
	@GuardedBy("this")
	private final Set<Person> mySet = new HashSet<Person>();
	
	public synchronized void addPerson(Person p){
		mySet.add(p);
	}
	
	public synchronized boolean containsPerson(Person p){
		return mySet.contains(p);
	}
}
实例封闭是构建线程安全类的一个最简单方式,它还使得在所策略的选择上拥有了更多的灵活性。
封闭机制更易于构建线程安全的类,因为当封闭类的状态时,在分析线程安全性时就无需检查整个程序。
Java的内置锁也称为监视器锁或监视器。在使用私有的锁对象而不是对象内置的锁有许多优点,私有的锁对象可以将锁封装起来,使客户代码无法得到锁,但客户代码可以通过公有的方法来访问锁,以便(正确或者不正确地)参与到它的同步策略中。
线程安全性的委托
@ThreadSafe
public class CountingFActorizer implements Servlet{
	private final AtomicLong count = new AtomicLong(0);
	
	public long getCount(){return count.get();}
	
	public void service(ServletRequest req, ServletResponse resp){
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = factors(i);
		count.incrementAndGet();
		encodeIntoResponse(resp, factors);
	}
}
CountingFActorizer类,有一个AtomicLong类型的域,组合一起是一个线程安全的类。由于CountingFActorizer的状态就是AtomicLong的状态,而AtomicLong是线程安全的,因此CountingFActorizer不会对count的状态施加额外的有效性约束,所以很容易知道CountingFActorizer是线程安全的。 我们可以说CountingFActorizer将它的线程安全性委托给AtomicLong来保证:之所以CountingFActorizer是线程安全的,是因为AtomicLong是线程安全的。(如果count不是final类型,那么要分析CountingFActorizer的线程安全性将变得更加复杂。如果CountingFActorizer将count修改为指向另一个AtomicLong引用,那么必须确保count的更新操作对于所有访问count的线程都是可见的,并且还要确保在count的值上不存在竞态条件。这也是尽可能使用final类型域的另一个原因。
独立的状态变量
对象类中的状态变量是线程安全的并且他们之间互相之间不存在耦合关系,这个就是将线程安全性委托给多个状态变量。
当委托失效时,就是对象类中的状态变量是线程安全的,但是它们相互之间存在不变性条件或者“先检查后执行”的约束性条件,没有维护不变性或者先检查后执行的条件造成线程不是线程安全的。
如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量。
在现有的线程安全类中添加功能
我们应该优先选择重用这些现有的类而不是创建新的类:重用能够降低开发工作量、开发风险以及维护成本。
例如,假设需要一个线程安全的链表,他需要提供一个原子的“若没有则添加(Put-If-Absent)”的操作。同步的List提供了大部分的功能,我们可以根据提供的contains方法和add方法来构造一个“若没有则添加”的操作。
//扩展Vector并增加一个“若没有则添加”方法
@ThreadSafe
public class BetterVector<E> extends Vector<E>{
	public synchronized boolean pubIfAbsent(E x){
		boolean absent = !contains(x);
		if(absent)
			add(x);
		return absent;
	}
}
“扩展”方法比直接将代码添加到类中更加脆弱,因为现在的同步策略实现被分布到多个单独维护的源代码文件中。如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类会被破坏,因为在同步策略改变后它无法在使用正确的锁来控制对基类状态的并发访问。
客户端加锁机制
客户端加锁是指,对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户代码。要使用客户端加锁,必须知道对象X使用的是哪一个锁。
@NotThreadSafe
class BadListHelper <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,它不管使用哪个锁来保护他的状态,可以肯定的是,这个锁并不是ListHelper上的锁。ListHelper只是带来了同步的假象,尽管所有的链表操作都被声明为synchronized,但却使用了不同的锁,这意味着putIfAbsent相对于List的其他操作来说并不是原子的,因此就无法确保当putIfAbsent执行时另一个线程不会修改链表。
//通过客户端加锁来实现“若没有则添加”(不要这么做)
@ThreadSafe
public class ListHelper<E>{
	public List<E> list = Collections.synchronizedList(new ArrayList<E>);
	...
	public boolean pubIfAbsent(E x){
		synchronized(lsit){
			boolean absent = !contains(x);
			if(absent)
				add(x);
			return absent;
		}
	}
}
客户端加锁却更加脆弱,因为它将类C的加锁代码放到与C完全无关的其他类中。客户端加锁机制与扩展类机制有许多共同点。二者都是将派生类的行为与基类的实现耦合在一起。正如扩展会破坏实现的封装性,客户端加锁同样会破坏同步策略的封装性。
组合(Composition)
//通过组合实现“若没有则添加”
@ThreadSafe
public class ImprovedList<E> implements List<E>{
	private final List<T> list;
	public ImprovedList(List<T> list){ this.list = list;}
	public synchronized boolean pubIfAbsent(E x){
		boolean absent = !contains(x);
		if(absent)
			add(x);
		return absent;
	}
	
	public synchronized void clear(){list.clear();}
	//...按照类似的方法委托list其他方法
}
这个例子比其他例子更为健壮。使用了Java监视器模式来封装现有List,并且只要在类中拥有指向底层List的唯一外部引用,就能确保线程安全性。
















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值