文档章节

如何扩展线程安全的类

摆渡者
 摆渡者
发布于 2016/10/24 10:47
字数 1850
阅读 69
收藏 0

Java类库包含许多有用的“基础模块”类。通常,我们应该优先选择重用这些现有的类而不是创建新的类:重用能降低开发工作量、开发风险(因为现有的类都已经通过测试)以及维护成本。有时候,某个现有的线程安全类能支持我们需要的所有操作,但更多时候,现有的类智能支持大部分的操作,此时就需要在不破坏线程安全性的情况下添加一个新的操作。

例如,假设需要一个线程安全的链表,它需要提供一个原子的“若没有则添加(putIfAbsent)”的操作。同步的List类已经实现了大部分的功能,我们可以根据它提供的contains方法和add方法来构造一个“若没有则添加”。由于这个类必须是线程安全的,因此就隐含的增加了另一个需求,即“若没有则添加”这个操作必须是原子操作。这意味着如果在链表中没有包含对象X,那么在执行两次“若没有则添加”X后,在容器中只能包含一个X对象。然而,如果“若没有则添加”操作不是也uanzi操作,那么在某些执行情况下,有两个线程都将看到X不在容器中,并且都执行了添加X的操作,从而使容器包含两个相同的对象。

  • 修改类

要添加一个新的原子操作,最安全的方法是修改原始的类,但这通常无法做到,因为你可能无法访问或修改类的源代码。要想修改原始的类,就需要理解代码中的同步策略,这样增加的功能才能与原有的设计保持一致。如果直接将新方法添加到类中,那么意味着实现同步策略的所有代码任然处于一个源代码文件中,从而更容易理解与维护。

  • 扩展类

另一种方法是扩展这个类(假定在设计这个类时考虑了可扩展性,即类不是final修饰的)。下面的代码中的BetterVector对Vector类进行了扩展,并添加了一个新方法putIfAbsent。扩展Vector很简单,但并非所有的类都像Vector那样将状态向子类公开,因此也就不适合采用这种方法。

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

“扩展”方法比直接将代码添加到类中更加脆弱,因为现在的同步策略实现被分不到多个单独维护的源代码文件中。如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类会被破坏,因为在同步策略改变后它无法再使用正确的锁来控制对基类状态的并发访问。(在Vector的规范中定义了它的同步策略,因此BetterVector不存在这个问题)

  • 客户端加锁

对于由Collections.synchronizedList封装的ArrayList,上述两种方法都行不通,因为客户端代码并不知道在同步封装器工厂中返回的List对象的类型。第三种策略是扩展类的功能,但并不是扩展类本身,而是将扩展代码放入一个“辅助类”中。下面的代码实现了一个包含“若没有则添加”操作的辅助类,用于对线程安全的List进行操作看,但其中的代码是错误的

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已经声明为synchronized类型的方法,对不对?问题在于在错误的锁上进行了同步。无论List使用哪一个锁来保护它自己的状态,可以确定的是,这个锁并不是ListHelper上的锁。ListHelper只是带来了同步的假象,尽管所有的链表操作都被声明为synchronized,但却使用了不同的锁,这意味着putIfAbsent相对于List的其他操作来说并不是原子的,因此就无法确保当putIfAbsent执行时,另一个线程不会修改链表

要想使这个方法能正确执行,必须使List在客户端实现加锁或外部加锁时使用的是同一个锁。客户端加锁是指:对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户代码。要使用客户端加锁,你必须知道对象X使用的是哪一个锁。

在Vector和同步封装器类(Collections.synchronized[List/Map/Set])的文档中指出,它们通过使用Vector或封装容器内置锁来支持客户端加锁。

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

通过添加一个原子操作来扩展类是脆弱的,因为它将类的加锁代码分布到多个类中。然而,客户端加锁却更加脆弱,因为它将类C的加锁代码放到与C完全无关的其他类中。当在那些并不承诺遵循加锁策略的类上使用客户端加锁时,要特别小心。

客户端加锁机制与扩展类机制有许多共同点,二者都是将派生类的行为与积累的实现耦合在一起。和扩展会破坏实现的封装性一样,客户端加锁同样会破坏同步策略的封装性

  • 组合(推荐)

当为现有的类添加一个原子操作时,还有一种更好的方法:组合(Composition)。下面的程序中的ImprovedList通过将List对象的操作委托给底层List实力来实现,同时还添加了一个原子的putIfAbsent方法(与Collections.synchronizedList和其他容器封装器一样,ImprovedList假设把某个链表对象传给构造函数以后,客户代码不会再直接使用这个对象,而只能通过ImprovedList来访问它)。

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 absent = !list.contains(x);
		if (absent) {
			list.add(x);
		}
		return absent;
	}
	//...按照类似的方式将ImprovedList的其他方法实现委托给List
}

ImprovedList通过自身的内置锁增加了一层额外的锁。它并不关心底层的List是否是线程安全的,及时List不是线程安全的或者在其后续版本中修改了它的加锁实现,ImprovedList也会提供一直的加锁机制来实现线程安全性。虽然额外的同步层可能导致轻微的性能损失,但与模拟另一个对象的加锁策略相比,ImprovedList更为健壮。

© 著作权归作者所有

摆渡者
粉丝 340
博文 171
码字总数 206342
作品 0
浦东
程序员
私信 提问
[Java 并发编程实战] 设计线程安全的类的三个方式(含代码)

发奋忘食,乐以忘优,不知老之将至。———《论语》 前面几篇已经介绍了关于线程安全和同步的相关知识,那么有了这些概念,我们就可以开始着手设计线程安全的类。本文将介绍构建线程安全类的...

seaicelin
2018/05/23
0
0
java并发编程(三): 对象的组合

对象的组合: 如何将现有的线程安全组件,组合成我们想要的更大规模的程序。 设计线程安全的类: 设计线程安全类的三个要素: 1.找出构成对象状态的所有变量; 2.找出约束状态变量的不变性条...

ihaolin
2014/03/24
0
0
设计模式系列1:单例模式

单例模式,指某个类只能产生一个且仅仅产生一个实例。 技术上如何实现:将构造函数声明为private. 然后通过它的一个方法返回一个实例,这样的话 程序中只要使用到这个类的实例肯定就是同一个...

强子哥哥
2014/01/28
197
0
C# ConcurrentBag的实现原理

目录 一、前言 二、ConcurrentBag类 三、 ConcurrentBag线程安全实现原理 1. ConcurrentBag的私有字段 2. 用于数据存储的TrehadLocalList类 3. ConcurrentBag实现新增元素 4. ConcurrentBag...

InCerry
2018/08/18
0
0
如何线程安全的使用HashMap

为什么HashMap是线程不安全的 总说HashMap是线程不安全的,不安全的,不安全的,那么到底为什么它是线程不安全的呢?要回答这个问题就要先来简单了解一下HashMap源码中的使用的存储结构(这里...

Edwyn王
2016/09/02
78
0

没有更多内容

加载失败,请刷新页面

加载更多

Jenkins系列_插件安装及报错处理

进入Jenkins之后我们可以进行插件的安装,插件管理位于以下模块: 发现上面报了一堆错误,是因为插件的依赖没有安装好,那么这一节,就先把这些错误解决掉吧。解决完成后,也就基本会使用插件...

shzwork
今天
2
0
mysql mysql的所有查询语句和聚合函数(整理一下,忘记了可以随时看看)

查询所有字段 select * from 表名; 查询自定字段 select 字段名 from 表名; 查询指定数据 select * from 表名 where 条件; 带关键字IN的查询 select * from 表名 where 条件 [not] in(元素...

edison_kwok
昨天
9
0
多线程同时加载缓存实现

import com.google.common.cache.Cache;import com.google.common.cache.CacheBuilder;import java.util.concurrent.ExecutionException;import java.util.concurrent.ExecutorServi......

暗中观察
昨天
3
0
利用VisualVM 内存查看

准备工作,建几个测试类。等下就是要查看这几个类里面的属性 package visualvm;public class MultiObject { private String str; private int i; MultiObject(String str...

冷基
昨天
2
0
组装一台工作游戏两用机

一、配置清单如下: 分类 项目 价格(元) 主板 华硕(ASUS)TUF Z370-PLUS GAMING II 电竞特工 Z370二代 支持9代CPU 1049 CPU 英特尔(Intel) i7 8700K 酷睿六核 盒装CPU处理器 2640 风扇 九...

mbzhong
昨天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部