《java并发编程实践》读书笔记第四章

关键知识
  1. 设计线程安全的类
设计线程安全的类的过程应该包括下面3个基本要素:
1.确保对象状态是由哪些变量构成的
2.确定限制状态变量的不变约束。
3.制定一个管理并发访问对象状态的策略。
  1. 对象的状态首先要从它的域说起:
一个对象有n个基本域,它的状态就是域值组成的n元组。如果一个对象的域引用了其他对象,那么它的状态同时也包含了被引用对象的域。
  1. 同步策略定义了对象如何协调对其状态的访问。并且不会违反它的不变约束和后验条件。
  2. 状态空间:
定义:对象与状态可能处于的状态的范围称为状态空间。
状态空间越小,越容易判断他们。尽量使用final修饰就可以简化我们对对象的可能状态进行分析。
  1. 理解不变约束和后验条件
不变约束:例如long类型的变量的状态空间是Long.MIN_VALUE到Long.MAX_VALUE,不允许其值为负就可以称为不变约束。
后验条件:用来检验状态转换是否非法。对于++操作,下次值应该是当前值加1,如果不是则说明同步失败。
  1. 多变量的不变约束需要原子性。必须在单一的原子操作中获取或者更新相互关联的变量 。不能先更新一个变量,然后释放锁,再重获锁,在更新其他的变量。因为释放了锁后可能会使对象处于无效状态。
  2. 状态依赖的操作
定义:若一个操作存在基于状态的先验条件,则称他是状态依赖的。例如你无法从空队列里删除一个条目,
在你删除元素时,队列必须处于非空状态。
  1. 在单线程程序中,操作如果无法满足先验条件,必然失败,别无他选。但是在并发程序中,原本为假的先验条件可能会由于其他线程的活动而变成真。
  2. 实例限制
定义:一个对象被另一个对象封装。
好处:当一个对象被另一个对象封装时,所有访问被封装对象的代码路径就是全部可知的,这相比与
让对象被整个系统访问来说,更容易对代码路径进行分析。
  1. 在Java平台类库中有很多线程限制的实例,包括一些类,它的存在就是为了把非线程安全的类转化成线程安全的。ArrayList和HashMap这样的容器类时非线程安全的,但是类库提供了包装器工厂方法(Collections.synchronizedList及其同族的方法),使这些非线程安全的类可以安全地用于多线程环境中。这些工厂方法利用Decorator模式(装饰器)使用一个同步的包装器对象包装容器。
  2. Java监视器模式,下面是书中的示例代码
public class PrivateLock{
	private final Object myLock = new Object();
	Widget widget;
	void someMethod(){
		synchronized(myLock){
			//访问或修改Widget的状态
		}
	}
}

这段代码中使用了Java监视器模式但是与一般使用对象内部锁不同,这里使用了私有锁myLock。使用私有锁对象可以封装锁,这样客户代码无法得到它。
12. 来看一下书中提供的一个使用监视器实现的线程安全的例子,代码如下:

public class MonitorVehicleTracker{
	//书中大量的域对象都使用了final修饰。
	private final Map<String,MutablePoint> locations;
	public MonitorVehicleTracker(Map<String,MutablePoint> locations){
		this.locations=deepCopy(locations);
	}
	public synchronized Map<String,MutablePoint> 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<String,MutablePoint> deepCopy(Map<String,MutablePoint> m){
	Map<String,MutablePoint> result = new HashMap<String,MutablePoint>();
	for(String id:m.keySet()){
		result.put(id,new MutablePoint(m.get(id));
	}
	return Collections.unmodifiableMap(result);		
}
public class MutablePoint{
	public final int x,y;//final修饰一旦赋值不可更改。
	public MutablePoint(MutablePoint p){
		this.x = p.x;
		this.y = p.y;
	}
}

这段代码对应的现实需求就是一个用于调度出租车、警车、货运卡车等机动车的“机动车追踪器”,每一辆机动车都有一个String标识,并有一个与之对应的位置(x,y)。每个VehicleTracker对象都封装了一辆已知机动车的标识和位置。视图(view)和多个更新线程可能会共享数据模型。视图线程会获取机动车的名称和位置。将他们显示在显示器上。

Map<String,Point> locations = vehicles.getLocations();
for(String key:locations.keySet())
	renderVehicle(key,locations.get(key));

这里只是给出了视图线程获取数据的部分代码,系统将所有机动车的位置信息返回给视图线程。类似的更新线程会通过GPS设备上获取的数据或者调度员通过GUI界面手工输入的数据,修改机动车的位置。

void vehicleMoved(VehicleMovedEvent evt){
	Point loc = evt.getNewLocation();
	vehicles.setLocation(evt.getVehicleId(),loc.x,loc.y);
}

代码基本上是自解释的。实时获取机动车的位置并且更新位置。
MutablePoint类中需要关注的就是其含参构造方法。接受一个同类型的对象作为参数,将其域复制过来。因为x、y都是基本数据类型,所以其类似于完全拷贝。再看MonitorVehicleTracker类其保有一个私有的final修饰的map,存储了id为键MutablePoint为值的映射。deepCopy方法在类中运用的很广泛,顾名思义深拷贝。方法内部声明了一个新的map容器,其在线程栈中,其他线程无法访问。在循环遍历中将传入的map中的映射对拷贝到新的容器中,最终返回一个不能被修改的map。Collections.unmodefiableMap(Map m)其作用是产生一个只读的Map,当你调用此map的put方法时会抛错。通过这种限制,确保返回视图线程的map不会被恶意修改。MonitorVehicleTracker的状态空间只由map决定,map容器中存储了MutablePoint,所以MonitorVehicleTracker的状态传导到了MutablePoint。好在后者是不可变对象,当需要数据更新时他会返回一个新的不可变对象。MonitorVehicleTracker中提供了针对map的get和set方法。为了确保视图线程得到的机动车位置是实时更新的,set和get操作必须是原子性的。类中使用了监视器技术来实现同步。
13. 上面的机动车示例程序使用了监视器模式。书中紧接着又提供了另外一种方式。使用委托的方式。在MonitorVehicleTracker中map的状态是可变的。可以通过将非线程安全的类委托给线程安全的类来实现线程安全。具体做法就是使用ConcurrentMap来取代Map来存储映射。当然MutablePoint必须还得是不可变的。
14. 诚然关于并发编程真的有时候非常的繁杂。上面我们说了将非线程安全的状态委托给线程安全的类可以实现线程安全。书中委托示例程序仅仅只是委托了一个单一的线程安全的状态变量。我们也可以将线程安全委托到多个隐含的状态变量上,只要这些变量是彼此独立的。再来看一个书中的示例程序:

public class VisualComponent{
	private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<KeyListener>();
	private final List<MouseListener> mouserListeners = new CopyOnWriteArrayList<MouseListener>();
	public void addKeyListener(KeyListener listener){
		keyListeners.add(listener);
		}
	public void addMouseListener(MouseListener listener){
		MouseListener.add(listener);
		}
	public void removeKeyListener(KeyListener listener){
		keyListeners.remove(listener);
		}
	public void removeMouseListener(MouseListener listener){
		MouseListeners.remove(listener);
		}
}

VisualComponent是一个允许客户注册鼠标键盘事件监听器的图形组件。它为每种类型的时间维护一个已注册监听器的清单。但是键盘监听器和鼠标监听器之间并没有关联,他们彼此独立。程序用来实现这一效果的关键是使用了CopyOnWriteArrayList——CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是,我们可以对CopyOnWrite容器进行并发的读,因为当前容器并没有添加新的值,所以不需要加锁。对于写操作需要获取到锁,避免创造出多个复制容器。所以同一时间只能有一个进行写操作。区别于传统的在set和get方法上都加锁的方式,CopyOnWrite容器实现了读写分离。
15. java类库中包含了许多很有用的“构建块”类。重用这些已有的类要好于创建一个新的:重用能够降低开发的难度、风险(因为已有的组件都经过测试)和维护成本。
16. 来看书中的另外一个示例:

public class ListHelper<E>{
	public List<E> list = Collections.synchronizedList(new ArrayList<E>());
	...
	public synchronized boolean putIfAbsent(E e){
		boolean absent = !list.contains(e);
		if(absent){
			list.add(e);
		}
		return absent;
	}
	...

这段代码是用来自定义实现一个缺少即加入操作的例子。使用了锁,保证了缺少即加入的操作原子性,好像没什么问题。但是这里使用了ListHelper的内置锁,他只能保证putIfAbsent方法的调用是同步的。list是公共可访问的,在putIfAbsent方法执行的同时list的其他方法依然是可以访问的。这会产生意想不到的结果。关键问题是这里的同步行为发生在错误的锁上。正确的做法是:方法所用的锁要与list用户客户端加锁与外部加锁时所用的锁是同一个锁。如下

public class ListHelper<E>{
	public List<E> list = Collections.synchronizedList(new ArrayList<E>());
	...
	public boolean putIfAbsent(E e){
		synchronized(list){
		boolean absent = !list.contains(e);
		if(absent){
			list.add(e);
		}
		return absent;
		}
	}
	...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值