【读书笔记】《Java并发编程实战》第四章 对象的组合

设计线程安全的类

要确保类的线程安全性,就需要确保它的不变性条件不会再并发访问的情况下被破坏。

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

  • 找出构成对象状态的所有变量
  • 找出约束状态变量的不变性条件
  • 建立对象状态的并发访问管理策略

概念介绍:
同步策略:定义了如何在不违背对象不变条件后验条件的情况下对其状态的访问操作进行协同。

不变性条件:不变式表达了对状态的约束,这些状态是应该符合这个约束的值的组合。
 在一些类的它的一些变量的变化是有一定规则的,比如类中定义一个属性表示苹果卖出了多少斤,卖出就增加,退货就减少,但是它肯定不会是一个负值,不为负值这就是这个变量的不变性条件。在比如类中定义了最大值与最小值,那么在这两个变量只有有一个不变性条件就是最大值要大于等于最小值。

后验条件:在操作中会用后验条件来判断状态转换是否是有效的。

1.找出构成对象状态的所有变量

 如果对象中所有的域都是基本类型的变量,那么这些域将构成对象的全部状态。
 如果在对象的域中引用了其他对象,那么该对象的状态将包含被引用对象的域。

2.找出约束状态变量的不变性条件

 由于不变性条件以及后验条件在状态及状态转换上施加了各种约束,因此就需要额外的同步与封装。如果某些状态是无效的,那么必须对底层的状态变量进行封装。否则客户代码可能会使对象处于无效状态。如果在某个操作中存在无效的状态转换,那么该操作必须是原子的。另外,如果在类中没有施加这种约束,那么就可以放宽封装性或序列化等需求,以便获得更高的灵活性或性能。
 如果在一个不变性条件中包含多个变量,那么在执行任何访问相关变量的操作时,都必须持有保护这些变量的锁。

依赖状态的操作
 先验条件->操作->后验条件
先验条件:只有满足该条件,操作才能执行。
后验条件:某个域的下一个状态依赖于前一个状态,例如今天周一,那么下一个状态只能是周二,如果取值周三,则不满足后验条件。

3. 建立对象状态的并发访问管理策略

 根据上面两步找出的变量进行并发访问控制保证不变性条件的约束,对比较独立的属性直接进行并发访问,但是对有关联的那就必须要更多的机制保证这个约束。

实例封闭

对于已有的对象如何保证线程安全?
 如果某个对象不是线程安全的,那么可以通过多种技术使其在多线程程序中安全地使用。你可以确保该对象只能由单个线程访问(线程封闭),或者通过一个锁来保护对该对象的所有访问。

将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。

通过监视器模式实现,把对象封装到一个新对象里面,所有对这个对象的访问都通过新对象的方法访问,然后保证新对象的方法是线程安全的就行了。

线程安全性委托

已有对象保证线程安全,并不一定需要自己实现线程安全,可以通过线程安全性委托。

 但是有时候我们并不是一定要设计成线程安全的类,如果已经存在一些线程安全的类可以保证我们需要的线程安全,还是要尽量用现有的,比如上面提到过苹果卖出的重量,就可以利用AtomicInteger来保证安全,再比如一些缓存也可以用ConcurrentMap来保证线程安全,把我们需要保证的线程安全委托给一些线程安全的类

 但是委托并不是一定有用的,比如前面的最大、最小值例子,如果定义成现AtomicInteger也无法保证它们的不可变条件的约束!这种可能就只能加锁了,但是如果他们两个并没有不可变条件约束是两个无关的共享变量,还是可以把多个无关的状态变量委托给线程安全的类

拓展线程安全的类

 如果一个线程安全的类功能不满足我们的需求,需要扩展一些功能,可是又不能修改这个类,那么就必须要对这个类进行扩展,扩展分两种方法继承、客户端扩展。继承实现比较简单,继承的类只要在保证新增的方法是线程安全的,那么它整个都是线程安全的,不过客户端扩展可能情况复杂一点,一个错误的例子如下图:

@NotThreadSafe
public class ListHelper<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对象加锁。

正确的例子如下:

@ThreadSafe
public class ListHelper<E> {
	public List<E> list = 
		Collections.synchronizedList(new ArrayList<E>());
	...
	public boolean putIfAbsent(E x) {
		synchronized (list) {  //注意这里使用的锁的区别
			boolean absent = !list.contains(x);
			if (absent) 
				list.add(x);
			return absent;
			}
		}
}

或者可以通过组合的方法将List接口重写,通过自身的内置锁增加一层额外的锁。如下图例子:

@ThreadSafe
public class ImprovedList<T> implements List<T> {
	private final List<T> list;

	public ImprovedList(List<T> list) {
		this.list = list;
	}

	public synchronized boolean putIfAbsent(T x) {
		boolean contains= list.contains(x);
		if (contains) 
			list.add(x);
		return !contains;
	}

	public synchronized void clear() {
		list.clear();
	}
	//...按照这样的方式添加内置锁重写List接口所有方法
}

文章参考:https://www.sohu.com/a/402289976_120591934?trans=000014_bdss_dkmwzacjP3p:CP=

猜你喜欢

转载自blog.csdn.net/Handsome_Le_le/article/details/107406666