《java并发编程实战》 第四章 对象如何组合

第四章 对象的组合

为什么出现对象的组合?在前三章中,我们并不希望对每一次内存访问都进行确保程序是线程安全的,而是希望将一些现有的线程安全组件组合成更为规模大的组件。对象的组合模式能将一个类更容易成为线程安全的。书中原话,通过使用封装技术,可以使得在不对整个程序进行分析情况下,就可以判断一个类是否线程安全。(这么强??)

如何设计线程安全的类----思想层面

设计线程安全类的过程中,需要包含以下三个基本要素:
1、找出构成对象状态的所有变量,如果对象中所有的域都是基本类型的变量,那么这些域(基本变量)将构成对象的全部状态。final类型的域使用越多,就越能简化对象可能状态分析过程。
2、考虑约束状态变量的不变性条件 (不变性条件用于判断变量状态有效还是无效)、先验条件、后验条件
  由于不变性条件以及后验条件在状态及状态转换上施加了各种约束,因此需要额外的同步与封装。如果某些状态是无效的,那么必须对底层的状态变量进行封装,否则客户代码可能使对象处于无效状态。如果某个操作中存在着无效的状态转换,那么该操作必须是原子的。例如:在一个表示数值范围的类,可以包含两个状态变量,分别表示范围的上界和下界。两个变量必须遵循的约束是:下界小于等于上界值。类似这种包含多个变量的不变性条件将带来原子性需求:这些相关变量必须在单个原子操作中进行读取或更新。不能首先更新一个变量,然后再更新其他的变量。因为释放锁后,可能会使对象处于无效状态。如果一个不变性条件中包含多个变量,那么在执行任何访问相关变量的操作时,都必须持有保护这些变量的锁。
如果某个操作中包含有基于状态的先验条件,那么这个操作被称为依赖状态的操作。例如:不能从空队列中移除一个元素,在删除元素前,队列必须处于“非空的”状态。在java中,很多先验条件(包括等待某个条件为真的各种内置机制)与内置加锁机制紧密关联,一种更简单的方法是通过现有库中的类(例如堵塞队列Blocking Queue)或者信号量Semaphore来实现依赖状态的行为。
  考虑状态的所有权问题:在面向对象编程中,所有权是属于类设计的一个要素。在C++中,把一个对象传递给某个方法时,必须认真考虑这种操作是否传递对象的所有权。java的垃圾回收机制替我们处理了所有权的问题。具体原理待补充。容器类通常表现出一种“所有权分离”形式,其中容器类拥有其自身的状态,而客户代码则拥有容器各个对象的的状态。而容器类拥有的自身状态指容器类里的内部类的状态。以Servlet框架中ServletContext就是其中一个例子。(由于一个WEB应用中的所有Servlet共享同一个ServletContext对象,它是一个全局的储存信息的空间,因此Servlet对象之间可以通过ServletContext对象来实现通讯。)ServletContext为Servlet提供了类似Map形式的对象容器服务,在ServletContext中可以通过setAttribute或getAttribute应用程序对象,而一个ServletContext是被多个线程同时访问,所以有由Servlet容器实现的ServletContext对象必须是线程安全的。保存在ServletContext中的对象由应用程序拥有,Servlet容器替应用程序保管它们,这些对象要么是事实不可变对象、要么是线程安全的对象、要么是由锁保护的对象。
3、建立对象状态的并发访问管理策略,synchronized 、volatile或者任何一个线程安全类都要对应于某种同步策略,用于在并发时确保数据的完整性。同时要将同步策略文档化,很多正式的java技术规范,例如Servlet和JDBC,也没有在它们的文档中给出线程安全性的保证与需求。

如何封装非线程安全对象

书中用的标题叫实例封闭,此处的实例封闭机制不同于第三章的栈封闭(线程封闭,确保对象只能由单个线程访问),而是一种类的封装。而是将一个对象a(或者数据)封装到另一个对象b中,b中能访问a对象(或者数据)的所有代码路径都是已知),在通过合适的加锁策略对这些访问a对象(或者数据的)路径进行限制,从而确保以线程安全的方式来使用非线程安全的对象b。
  例如:PersonSet说明了如何通过封闭和加锁机制使PersonSet成为线程安全的,即使PersonSet的状态变量并不是线程安全的。PersonSet的状态由HashSet来管理,而HashSet并非线程安全的,但是mySet是私有不会逸出,并且能访问mySet的代码路径已经被上锁。所以PersonSet是一个线程安全的类。

	@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类库中还有很多线程封闭的实例,其中有些类用途就像PersonSet将非线程安全的类转化为线程安全的类。例如一些基本的容器类ArrayList、 HashMap等并非线程安全,但类库中提供了包装器工厂方法Collections.synchronizedList等方法。(这些工厂方法将容器类封装在一个同步的包装器对象中,而包装器能将对象中的每个方法都实现微同步方法,并将调用请求转发到底层的容器对象中。只要包装器对象拥有对底层容器的唯一引用,那么它就是线程安全的,在javadoc也指出,对底层容器对象的访问必须通过包装器)。包装器对象就类似上面的PersonSet,包装对象即Person。

JAVA监视器模式----内置锁

java的内置锁也被称为监视器锁或监视器。遵循java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。**内置锁获得锁和释放锁是隐式的,进入synchronized修饰的代码就获得锁,走出相应的代码就释放锁。**内置锁可以是指定当前对象、当前类的Class对象、指定任意对象。内置锁最大优点是简洁易用不同于显示锁,显示锁最大优点是功能丰富,所以能用内置锁就用内置锁,在内置锁功能不能满足之时在考虑显示锁。

synchronized(list){ //获得锁
    list.append();    
    list.count();
    ......
}//释放锁

注意:内置锁不同于私有锁(在类内部声明一个私有属性如private Object lock,在需要加锁的代码段synchronized(locka) )。
  书中举的例子:客户端代码还是可以通过公有方法访问私有锁,若错误获得了另外一个对象的锁,可能会产生活跃性问题。要想验证某个公有访问的锁在程序中是否正确的使用,则需要检查整个程序,而不是单个类。

public class PrivateLock{
	private final Object myLock = new Object();/私有锁
	@GuardedBy("myLock") Widget  widget;
	void someMethode(){
		synchronzied(myLock){
			......//访问或者修改Widget状态
		}
	}
}

内置锁被如何使用查看上面如何封装非线程安全对象。书中还举了个例子:
  首先有一个表示车辆坐标的类MultablePoint,这个类线程不安全。

class MultablePoint{
    public int x,y;
    public MultablePoint() {
    	x = 0;y = 0;
    }
    public MultablePoint(MultablePoint p) {
        this.x = p.x;
        this.y = p.y;
    }
}

但是,追踪器类是线程安全的,它所包含的Map对象和可变的Point对象都未曾发布.当需要返回车辆的位置时,通过MutablePoint拷贝或者deepCopy方法类复制正确的值,从而生成一个新的Map对象。补充:java.util.Collections类中提供了UnmodifiableMap方法,UnmodifiableMap方法会映射一个新的、只读的、不可修改的Map对象,当你调用此map的put方法时会抛错。这个不可修改的Map指的是Map中的对象地址不可修改,里面的对象若支持修改的话,其实也还是可以修改的。

@ThreadSafe
public class MonitorVehicleTracker {
	@GuardedBy("this")
    private Map<String,MultablePoint> locations;
    public MonitorVehicleTracker(HashMap<String,MultablePoint> locations) {
        this.locations = deepCopy(locations);
    }
    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);
        }
    }
    public synchronized Map<String,MultablePoint> getLocations() {
        return deepCopy(locations);
    }
    public synchronized MultablePoint getLocation(String id){
        MultablePoint mPoint = locations.get(id);
        return mPoint==null?null:new MultablePoint(mPoint);
    }
    private static Map<String, MultablePoint> deepCopy(Map<String, MultablePoint> 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);
    }
}

unmodifiableMap的用法stackoverflow上有大哥探讨过这个问题,给了个很好的例子:

public class SeeminglyUnmodifiable {
	private Map<String, Point> startingLocations = new HashMap<>(3);
	   public SeeminglyUnmodifiable(){
	      startingLocations.put("LeftRook", new Point(1, 1));
	      startingLocations.put("LeftKnight", new Point(1, 2));
	      startingLocations.put("LeftCamel", new Point(1, 3));
	      //..more locations..
	   }
	   public Map<String, Point> getStartingLocations(){
	      return Collections.unmodifiableMap(startingLocations);
	   }
	   public static void main(String [] args){
	     SeeminglyUnmodifiable  pieceLocations = new SeeminglyUnmodifiable();
	     Map<String, Point> locations = pieceLocations.getStartingLocations();
	     Point camelLoc = locations.get("LeftCamel");
	     System.out.println("The LeftCamel's start is at [ " + camelLoc.getX() +  ", " + camelLoc.getY() + " ]");
	     //Try 1. update elicits Exception
	     try{
	        locations.put("LeftCamel", new Point(0,0));  
	     } catch (java.lang.UnsupportedOperationException e){
	        System.out.println("Try 1 - Could not update the map!");
	     }
	     //Try 2. Now let's try changing the contents of the object from the unmodifiable map!
	     camelLoc.setLocation(5,5);
	     //Now see whether we were able to update the actual map
	     Point newCamelLoc = pieceLocations.getStartingLocations().get("LeftCamel");
	     System.out.println("Try 2 - Map updated! The LeftCamel's start is now at [ " + newCamelLoc.getX() +  ", " + newCamelLoc.getY() + " ]");       }
	}

locations.put(“LeftCamel”, new Point(0,0)); 操作中,locations是经过unmodifiableMap得到的私有变量startingLocations 的映射的Map,这个Map中的对象地址,即LeftCamel对应的Point地址不可更改,因此不能用put操作更改Map指向另一个Point,但是可以修改原Point中的内容。

委托

线程安全性的委托和java监视器模式一样也是一种对象组合的方式,不同的是委托要求组合的对象是线程安全的。委托是指将线程的安全性委托给现有的线程安全类。
  当基于java监视模式的车辆追踪器中的Point变成不可变后,可以采用委托的方式。DelegatingVehicleTracker 类中getLocation、setLocation方法没有用synchronized方法,思考委托如何做到线程安全的
本类中所有的状态(locations)ConcurrentMap类型均由ConcurrentHashMap来管理,补充知识(HashMap是线程不安全的,Hashtable 本身比较低效,因为它的实现基本就是将 put、get、size 等各种方法加上“synchronized”。简单来说,这就导致了所有并发操作都要竞争同一把锁,一个线程在进行同步操作时,其他线程只能等待,大大降低了并发操作的效率。)
  ConcurrentHashMap采用分段锁技术:ConcurrentHashMap的源代码涉及到散列算法和链表数据结构,ConcurrentHashMap由Segment数组和HashEntry数组组成。Segment是一种可重入锁,在ConcurrentHashMap里扮演锁的角色; HashEntry则用于存储键值对数据。 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素。当对HashEntry数组的数据进行修改时, 必须首先获得与它对应的Segment锁。HashEntry 类的 value 域被声明为 Volatile 型,当一个写线程修改了某个 HashEntry 的 value 域后,另一个读线程读这个值域,Java 内存模型能够保证读线程读取的一定是更新后的值。
   因此ConcurrentHashMap支持多个并发同时修改,ConcurrentMap<String,Point> locations是线程安全的。
使用监视器模式的MonitorVehicleTracker只能返回的是“车辆位置的快照”(原话),个人觉得,监视模式中所有公有方法都使用了synchronized,同步明明是可以保证内存可见性,所以返回的也是最新更改的位置。监视模式给的例子getLocation会发布一个指向可变状态的引用,而且这个引用不是线程安全的,这才是其缺点。而使用委托的车辆追踪器DelegatingVehicleTracker返回的是一个不可修改Point,但是ConcurrentHashMap可以保证可见性可,因此可以获得实时的、不可修改的车辆位置视图。如果不需要委托的实时位置,可以采用浅拷贝。由于Map的内容是不可变的,因此只复制Map的结构,不用复制它的内容。
ConcurrentHashMap实现高并发的原理:参考 http://www.importnew.com/16147.html
   将线程安全委托给ConcurrentHashMap:

@Immutable
public class Point{
	public final int x,y;
	public Point(int x,int y)
	{
		this.x = x;
		this.y = y;
	}
}
@ThreadSafe
public class DelegatingVehicleTracker {
	private final ConcurrentMap<String,Point> locations;
	private final Map<String,Point> unmodifiableMap;
	
	public DelegatingVehicleTracker(Map<String,Point> Points){
	locations = new ConcurrentHashMap<String,Point>(points);
	unmodifiableMap = Collections.unmodifiableMap(locations);
	}
	public Map<String,Point> getLocation(){ //返回实时的车辆位置
		return unmodifiableMap;
	}
	public Point getLocation(String id){
		return locations.get(id);
	}
	public void setLocation(String id,int x, int y){
		if(locations.replace(id,new Point(x,y)) == null) 
		   throw new IllegalArgumentException("invalid vehicle name :"+id);
	}
}

//shallow copy
public Map<String,Point> getLocation(){
	return Collections.unmodifiableMap(
		new HashMap<String,Point>(locations));
}

将线程安全性委托给多个状态变量: VisualComponent使用CopyOnWriteArrayList来保存各个监听器列表。每个链表都是线程安全的,此外,由于各个状态之间不存在耦合关系,鼠标事件监听器和键盘事件监听器之间不存在任何关联,因此VisualComponent可以将它线程安全性委托给keyListeners 和mouseListeners 两个状态对象。

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

当多个状态变量线程安全,但是之间存在着某些不变性条件,可能会出现委托失效。例如:NumberRange,setLower、setUpper都是“先检查后执行”操作,但是它们没有使用足够的加锁机制来保证这些操作的原子性。例如一个线程调用setLower(5),setUpper(4),两个调用都能通过检查,最终得到的取值范围就是(5,4);这是一个无效的状态,虽然AtomicInteger是线程安全的,但状态变量lower和upper不是彼此独立的,因此NumberRange不能讲线程安全性委托给他的线程安全状态变量。

public class NumberRange{
	//不变性条件:lower<=upper
	private final AtomicInteger lower = new AtomicInteger(0);
	private final AtomicInteger upper = new AtomicInteger(0);
	public void setLower(int i)
	{
		if(i > upper.get())//特别容易犯的并发下不安全的“先检查后执行”
		{
			throw new IllegalArgumentException("can not set lower to"+ i+" > upper");
		}
		lower.set(i);
	}
	public void setUpper(int i){
	//
	if(i < lower.get())
		throw new IllegalArgumentException("can not set upper to "+ i+" > lower");
	upper.set(i);
	}
	public boolean isInRange(int i)
	{
		return (i >= lower.get() && i <= upper.get())
	}
}

委托基础上如何发布状态状态变量

对象的组合,以车辆追踪器为例:第一个版本中通过内置锁封装内部状态中非线性安全的对象MutablePoint,第二个版本线程安全的对象Immutable Point通过委托给ConcurrentHashMap,或者委托给多个独立的状态变量。第一版第二版都通过Collections.unmodifiableMap()操作返回车辆的不可变副本,调用者不能增加或者删除车辆,并且监视器模式下返回的位置不是线程安全的Point,委托模式返回线程安全的Point但是不可修改。

此时我如果想使用委托给ConcurrentHashMap的优势可见性,又想可以修改,只需要将第二个版本中不可变对象Point更改为一个线程安全且可变的Point类,然后发布Point状态即可。

@ThreadSafe
public class SafePoint{
	@GuardedBy("this") private int x,y;
	private SafePoint(int[] a){this(a[0],a[1]);}
	public SafePoint(SafePoint p){this(p.get());}
	public SafePoint(int x, int y){
		this.x = x;
		this.y = y;
	}
	public synchronized int[] get(){
	return new int[] { x, y};
}
	public synchronized void set(int x, int y){
	this.x = x;
	this.y = y;
	}
}
@ThreadSafe
public class PublishingVehicleTracker{
	private final Map<String,SafePoint> locations;
	private final Map<String,SafePoint> unmodifiableMap;
	public PublishVehicleTracker(Map<String,SafePoint> locations){
		this.locations = new ConcurrentHashMap<String,SafePoint>(locations);
		this.unmodifiableMap = Collections.unmodifiableMap(this.locations);
	}
	public Map<String,SafePoint> getLocations(){ return unmodifiableMap;}
	public SafePoint getLocation(String id){
		return locations.get(id);
	}
	public void setLocation(String id,int x,int y)
	{
		if(!locations.containsKey(id))
			throw new IllegalArgumentException("invalid vehicle name:"+id);
			locations.get(id).set(x,y);
	}
}

如何在现有的线程安全类中添加功能

要添加一个新的原子操作,最安全的方法就是直接修改原始的类,但是有时无法访问或者修改类的源代码。若修改原始的类,需要理解原始代码中同步策略,这样增加的功能才和原有的设计保持一致。
例如像在Vector中增加一个“若没有则添加”的方法,"扩展"方法比直接在源代码中修改更加脆弱,因为现在的同步策略实现被分布到多个单独维护的源代码文件中。选择“扩展”的方式要选择原始类一致的同步策略,否则会出错。BetterVector的实现方法参考了Vector规范中的同步策略。

@ThreadSafe
public class BetterVector<E> extends Vector<E>
{
	public synchronized boolean putIfAbsent(E e){
		boolean absent = !contains(x);
		if(absent)
			add(x);
		return absent;
	}
}

客户端加锁机制

非线程安全的“若没有则添加”,虽然putIfAbsent已经声明为synchronzied类型的变量,但ListHelper只是带来了假象,问题在于在错误的锁上进行了同步,尽管所有的链表操作都被声明为synchronized,但却使用了不同的锁,意味着putIfAbsent相对于List的其他操作都不是原子。所以,putIfAbsent并不是原子操作。假设在那么在某一时刻,满足if(!absent)不变约束的同时准备add()某个对象的时候,已经有另一个线程通过ListHelper .list.add()过这个对象了,所以还是会出现add()两个相同对象的情况。

@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在实现客户端加锁或者外部加锁时使用同一个锁。客户端加锁是指,对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户代码。修改后的ListHelper,要增加原子性操作的对象是list,使用list本身作为客户端代码的锁。要使用客户端加锁,你必须知道对象X使用的是哪一个锁。客户端加锁机制更加脆弱,因为它将类X的加锁代码放到与X完全无关的其他类中,此处List类的加锁代码不是放在List中,而是放在新建的ListHelper中。客户端加锁,就是在调用这个对象的地方,使用对象的锁确保线程安全

public class ListHelper <E>{
    private final 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;
        }
    }
}

组合方式

通过ImprovedList对象来操作传进来的list对象,用的都是Improved的锁。即使传进来的list不是线程安全的,ImprovedList也能保证线程安全。Improved通过自身的内置锁增加一一层额外的加锁,它不用关心底层的List是否线程安全,虽然额外的同步导致轻微的性能损失但是相比客户端加锁方式更加健壮。

public class ImprovedList<T> implements List<T> {
    private final List<T> list;
    public ImprovedList(List<T> list) {
        this.list = list;
    }
    public synchronized boolean putIfAbsent(Object obj){
        boolean absent = list.contains(obj);
        if(absent){
            list.add((T) obj);
        }
        return absent;
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_41262453/article/details/86544965