文档章节

JUC系列三:对象的委托与组合

那位先生_
 那位先生_
发布于 2015/08/05 23:53
字数 2294
阅读 115
收藏 3

在讲之前,我们先看一个Java监视器模式示例,这个示例是用于调度车辆的车辆追踪器,首先使用监视器模式来构建车辆追踪器,然后再尝试放宽某些封装性需求同时又保持线程安全性。每台车都由一个String对象来标识,并且拥有一个相应的位置坐标。在MonitorVehicleTracker类中封装了所有车辆的标识和位置,且这个类将被一个读取线程和多个更新线程所共享,所以这个类一定要是线程安全的。

import java.util.*;
/**
	基于监视器模式的线程安全的车辆追踪
	MutablePoint没有被发布出去,发布出去的只是一个数据与MutablePoint相同的新对象,所以修改MutablePoint的值只能通过setLocation来实现
*/
public class MonitorVehicleTracker{
	private final Map<String,MutablePoint> locations;
	
	/**
		构造时采用的是深拷贝,如果不采用深拷贝,那么在构造完后,locations就被发布出去了,这样里面的数据就可能被不安全的修改
		比如
		
		Map<String,MutablePoint> locations=...
		MonitorVehicleTracker m=new MonitorVehicleTracker(locations);
		...

		构造完后就可以在这以后的代码往locations中添加和删除车辆,这是不安全的
	*/
	public MonitorVehicleTracker(Map<String,MutablePoint> locations){
		this.locations=deepCopy(locations);
	}
	public synchronized Map<String,MutablePoint> getLocations(){
		return deepCopy(locations);
	}
	/**
		防止MutablePoint被发布出去,返回一个数据相同的新的对象,
	*/
	public synchronized MutablePoint getLocation(String id){
		MutablePoint point=locations.get(id);
		return point==null?null:new MutablePoint(point);
	}
	public synchronized void setLocation(String id,int x,int y){
		MutablePoint point=locations.get(id);
		if(point==null){
			throw new IllegalArgumentException("No Such Id: "+id);
		}
		point.x=x;
		point.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)));//防止locations中的MutablePoint被发布出去,返回一个数据相同的新对象
		}
		return Collections.unmodifiableMap(result);//返回一个不能被修改的map
	}
}
/**
	非线程安全
*/
class MutablePoint{
	public int x,y;
	public MutablePoint(){
		x=0;y=0;
	}
	public MutablePoint(MutablePoint p){
		this.x=p.x;
		this.y=p.y;
	}
}

在这里,虽然MutablePoint不是线程安全的,但由于MonitorVehicleTracker是线程安全的,其共有方法都使用了内置锁进行同步,并且它所包含的Map对象和可变的MutablePoint都未曾发布。当需要返回车辆的位置时,通过MutablePoint拷贝函数或者deepCopy方法复制正确的值,从而生成一个键值对都与原Map对象都相同的新的Map对象。 但这也会产生一个性能上的问题,当车辆较多时,复制数据会占用较多的时间。而且,当复制完成后,如果车辆的位置发生了变化,并不会即使反应到读取线程中去,这是好是坏就依情况而定了。为了解决这些问题,接下来我们就使用其它的方式来实现这个线程安全类

###委托

所谓委托,就是使用一个原本就线程安全的类来管理类的某个或某些状态,在上面的例子中,线程安全主要体现在locations状态上,所以,我们现在将locations委托给线程安全的ConcurrentHashMap。

import java.util.*;
import java.util.concurrent.*;
/**
	不使用deepCopy
	基于委托的车辆追踪器
	虽然Point被发布出去了,但是unmodifiableMap添加或删除替换里面的车辆,而Point又是不可变的,所以跟没发布出去一样
	只能通过MonitorVehicleTracker1的setLocation方法来修改Point
*/
public class MonitorVehicleTracker1{
	private final ConcurrentMap<String,Point> locations;
	private final Map<String,Point> unmodifiableMap;

	public MonitorVehicleTracker1(Map<String,Point> map){
		locations=new ConcurrentHashMap<String,Point>(map);
		unmodifiableMap=Collections.unmodifiableMap(locations);
	}
	/**
		MonitorVehicleTracker类中返回的是快照,而在这里返回的是不可修改但却实时的车辆位置视图,这就意味着如果返回后车辆的位置发生了变化,对于视图线程来说,是可见的。
	*/
	public Map<String,Point> getLocations(){
		return unmodifiableMap;
		//return Collections.unmodifiableMap(new HashMap<String,Point>(locations));
	}
	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);
		}
	}
}
/**
	一个不可变的Point类
	Point必须是不可变的,因为getLocations会发布一个含有可变状态的Point引用
*/
class Point{
	....
}

我们可以看到,当使用线程安全的类去管理locations时,对于locations的get和replace都将是线程安全的,所以不需要像第一个例子一样,对getLocation和setLocation进行同步,而对于getLocations方法来说,返回的是一个不可修改的Map,所以也不用担心Map中的对象被更新或者是删除。但是,尽管如此,对于Map中的Point对象,我们还是可以修改的里面的属性值的(Point对象被发布出去了),而且getLocation方法也发布了一个Point对象,所以,这也是不安全的,因此,我们要么将Point对象设置成不可变的,要么不发布Point,在这里,我们选择前者。

/**
	一个不可变的Point类
	Point必须是不可变的,因为getLocations会发布一个含有可变状态的Point引用
*/
class Point{
	public final int x,y;
	public Point(int x,int y){
		this.x=x;
		this.y=y;
	}
}

在调用getLocations时,返回给读线程的是一个Collections.unmodifiableMap(locations)对象,所以如果写线程使用了setLocation来更新车辆的信息,读线程都能及时的看到。

对于Point来说,它要么是不可变的,要么不会被发布出去,但是,当我们需要发布一个可变的Point时,我们就需要创建一个线程安全的可变Point,这样,即使是被发布出去,也保证了线程安全性。

public class SafePoint{
	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;
	}
}
public class MonitorVehicleTracker2{
	private final Map<String,SafePoint> locations;
	private final Map<String,SafePoint> unmodifiableMap;
	public MonitorVehicleTracker2(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);//set是线程安全的
	}
}

当然,上面只是个很简单的例子,对于复杂的共享类来说,多个状态之间可能会存在不变性,这时,如果只是使用线程安全类来委托的话,并不能解决线程安全问题。像下面的例子

public class NumberRange{
	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't set lower to "+i+" > upper");
		}
		lower.set(i);
	}

	public void setUpper(int i){
		if(i<lower.get()){
			throw new IllegalArgumentException("can't set upper to "+i+" < lower");
		}
		upper.set(i);
	}

	public boolean isInRange(int i){
		return (i>=lower.get()&&i<=upper.get());
	}
}

在上面的例子中,存在lower<upper的不变性约束,尽管setXXX方法使用了先检查后执行对这个不变性进行了维持,但并没有使用足够的加锁机制来保证这些操作的原子性,试想如果两个方法同时被两个线程分别调用,那么就可能破坏这种不变性。所以仅靠委托是不够的,还必须对这些操作进行加锁同步。

###组合 当我们要为一个线程安全的类添加一个新的操作时,我们可以选择多种方式,像修改原始的类,虽然这似乎并不怎么理想(因为你不一定能获取到原始类的源码),也可以扩展这个类。比如我们要给Vector添加一个若没有则添加的操作。

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

我们还可以在使用时进行加锁

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操作加锁时和putIfAbsent操作加锁时使用的是同一个锁,否则就是非线程安全的。

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;
	}
}

很明显,putIfAbsent操作的加锁对象是ListHelper对象,而list的操作的加锁对象是list,加锁对象不同,就不能保证线程的安全性。

当为现有的类添加一个操作时,为了保证原有类的线程安全,我们还可以使用更好的方式,那就是组合。我们可以将List对象的操作委托给底层的List实例来实现List的操作,同时还添加一个原子的putIfAbsent方法,这样,List实例就不能直接被访问,而是要通过封装类来实现。

public class MyList<E> implements List<T>{
	private final List<T> list;
	public MyList<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 add(T x){...}
	// ... 使用内置锁实现 list的其它方法
}

所以,不管list是不是线程安全的,MyList也都提供了一致的加锁机制来实现线程安全性。但这样的实现,会产生一些性能上的损失,但实现更加健壮。

© 著作权归作者所有

那位先生_

那位先生_

粉丝 131
博文 109
码字总数 242433
作品 0
深圳
后端工程师
私信 提问
加载中

评论(0)

Java 多线程系列目录(共43篇)

Java多线程系列目录(共43篇) 最近,在研究Java多线程的内容目录,将其内容逐步整理并发布。 (一) 基础篇 01. Java多线程系列--“基础篇”01之 基本概念 02. Java多线程系列--“基础篇”02之 ...

foxeye
2016/02/29
346
0
2、JUC系列之---线程通信

一、线程通信--示例 需求: input 设置resource , resource为name和sex,output输出resource 代码: 结果: 原因:input 设置了lee male后 又设置了name = 迪,sex还没有设置,output就输出了...

李李李李格尔楞
2018/05/15
20
0
java程序猿技术栈

一、java 基础知识 1.1 java基础集合类 1.2 jdk1.5、1.6、1.7、1.8 特效比较 1.3 java异常处理 1.4 jvm原理及常见问题 1.5 log4j等日志收集 1.6 jdbc驱动 1.7 jdk反射机制使用和原理 1.8 ja...

南寒之星
2016/11/30
17
0
java高并发系列 - 第23天:JUC中原子类,一篇就够了

这是java高并发系列第23篇文章,环境:jdk1.8。 本文主要内容 JUC中的原子类介绍 介绍基本类型原子类 介绍数组类型原子类 介绍引用类型原子类 介绍对象属性修改相关原子类 预备知识 JUC中的原...

路人甲Java
2019/08/07
0
0
使用Kotlin高效地开发Android App(五)完结篇

一. 单例 使用 Java 来编写单例模式的话,可以写出好几种。同样,使用 Kotlin 也可以写出多种单例模式。在这里介绍的是一种使用委托属性的方式来实现单例的写法。 首先,Kotlin 在语法层面上...

Tony沈哲
2018/10/30
0
0

没有更多内容

加载失败,请刷新页面

加载更多

1核2G云服务哪家便宜?

前言: 又到一年续费时,我们来盘点哪些云厂商新手活动给力?有人说我又不是新手,有啥用?你要知道你作为家里唯一一位程序员,有强大的家庭后盾,比如爸爸妈妈爷爷奶奶叔叔阿姨......... 不过...

王念博客
11分钟前
184
0
JavaScript 箭头函数:适用与不适用场景

JavaScript 箭头函数:适用与不适用场景 现代 JavaScript 中最引人注目的功能之一是引入了箭头函数,用 => 来标识。 这种函数有两大优点 – 非常简洁的语法,和更直观的作用域和 this的绑定。...

王囧草
20分钟前
46
0
Docker快速入门

1 几个概念 Docker可以把开发的软件代码以及软件所依赖的所有运行时环境、依赖类库都打包成一个容器镜像,因此使用docker打包软件可以让程序员开发的程序运行在各种不同的计算机硬件环境中。...

即将秃头的Java程序员
22分钟前
68
0
Zookeeper-03-权限管理

Zookeeper-03-权限管理 用的不多,暂时先不整理了

moon888
23分钟前
36
0
渲染学习笔记——GPU应用阶段

1.GPU流水线 注:绿色可编程,橙色可控不可编程,红色完全不可控 2.顶点着色器 顶点着色器计算速度快于片元着色器,所以很多中间数据在顶点着色器计算。 3.裁剪 4.屏幕映射 5.三角形 6.片元着...

myctrd
29分钟前
61
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部