Java 并发编程(三)为现有的线程安全类中添加新的原子操作

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

假设我们需要一个线程安全的链表,他需要提供一个原子的”若没有则添加(Put-If-Absent)“的操作。同步的 List 类已经实现了大部分的功能,我们可以根据它提供的 contains 方法和 add 方法构造一个实现。

可以有四种方法来实现这个原子操作。

第一种方法,也是最安全的方法,便是修改原始类。

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

第二种方法,可以扩展(继承)这个类—-假如原始类在设计的时候时考虑到了它的可扩展性。

例如,我们可以设计一个 BetterVector 对 Vector 进行扩展,并添加了一个新方法 putIfAbsent。

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

扩展 Vector 很简单,但并非所有的类都像 Vector 那样将状态向子类公开,因此也就不适合采用这种方法。

”扩展“方法比较脆弱,主要原因是 同步策略的实现被分离到了多个源代码文件中,如果底层类改变了同步策略,更改了不同的锁来保护状态,那么子类便会被破坏。

第三种方法,使用辅助类,实现客户端加锁机制。

对于某些类,比如 Collections.synchronizedList 封装的 ArrayList , 前两种方法都行不通,因为客户代码不知道在同步封装器工厂方法中返回的 List 对象的类型。这时候采用客户端加锁的方式,将扩展代码放到一个”辅助类“中。

于是我们很自然的就写出 ListHelper 辅助类。

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 ,但是它却是在 ListHelper 上加锁,而 List 却是用自己或内部对象的锁。 ListHelper 只是带来了同步的假象,

在 Vector 和同步封装器类的文档中指出,他们是通过 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;}
	}
}

通过添加一个原子操作来扩展类是脆弱的,因为它将类的加锁代码分布到多个类中。然而,客户端加锁却更加脆弱,因为它将类的加锁代码放到与其完全无关的其他类中。

第四种方法,使用组合(Composition)的方式。

public  class ImprovedList<T> implements List<T> {
	public 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;
	}

	//...按照类似的方式委托List的其他方法
}

ImprovedList 通过自身的内置锁增加了一层额外的加锁。它并不关心底层的 List 是否是线程安全的,即使 List 不是线程安全的或者修改了它的枷锁方式,Improved 也会提供一致的加锁机制来实现线程安全性。虽然额外的同步层可能导致轻微的性能损失,但与模拟另一个对象的加锁策略相比,ImprovedList 更为健壮。事实上,我们使用了 Java 监视器模式来封装现有 List ,并且只要在类中拥有指向底层 List 的为意外不引用,就能确保线程安全性。

时间: 2024-10-14 14:05:56

Java 并发编程(三)为现有的线程安全类中添加新的原子操作的相关文章

Java 并发编程(三)为线程安全类中加入新的原子操作

Java 类库中包括很多实用的"基础模块"类.通常,我们应该优先选择重用这些现有的类而不是创建新的类.:重用能减少开发工作量.开发风险(由于现有类都已经通过測试)以及维护成本.有时候,某个线程安全类能支持我们须要的全部操作,但很多其它的时候,现有的类仅仅能支持大部分的操作,此时就须要在不破坏线程安全的情况下加入一个新的操作. 如果我们须要一个线程安全的链表,他须要提供一个原子的"若没有则加入(Put-If-Absent)"的操作.同步的 List 类已经实现了大部分

《Java并发编程实战》第八章 线程池的使用 读书笔记

一.在任务与执行策略之间的隐性解耦 有些类型的任务需要明确地指定执行策略,包括: . 依赖性任务.依赖关系对执行策略造成约束,需要注意活跃性问题.要求线程池足够大,确保任务都能放入. . 使用线程封闭机制的任务.需要串行执行. . 对响应时间敏感的任务. . 使用ThreadLocal的任务. 1. 线程饥饿死锁 线程池中如果所有正在执行任务的线程都由于等待其他仍处于工作队列中的任务而阻塞,这种现象称为线程饥饿死锁. 2. 运行时间较长的任务 Java提供了限时版本与无限时版本.例如Thread

读书笔记-----Java并发编程实战(一)线程安全性

线程安全类:在线程安全类中封装了必要的同步机制,客户端无须进一步采取同步措施 示例:一个无状态的Servlet 1 @ThreadSafe 2 public class StatelessFactorizer implements Servlet{ 3 public void service(ServletRequest req,ServletResponse resp){ 4 BigInteger i = extractFromRequest(req); 5 BigInteger[] fact

《Java并发编程实战》第二章 线程安全性 读书笔记

一.什么是线程安全性 编写线程安全的代码 核心在于要对状态访问操作进行管理. 共享,可变的状态的访问 - 前者表示多个线程访问, 后者声明周期内发生改变. 线程安全性 核心概念是正确性.某个类的行为与其规范完全一致. 多个线程同时操作共享的变量,造成线程安全性问题. * 编写线程安全性代码的三种方法: 不在线程之间共享该状态变量 将状态变量修改为不可变的变量 在访问状态变量时使用同步 Java同步机制工具: synchronized volatile类型变量 显示锁(Explicit Lock

Java并发编程之set集合的线程安全类你知道吗

Java并发编程之-set集合的线程安全类 Java中set集合怎么保证线程安全,这种方式你知道吗? 在Java中set集合是 本篇是<凯哥(凯哥Java:kagejava)并发编程学习>系列之<并发集合系列>教程的第二篇: 本文主要内容:Set集合子类底层分别是什么?基于底层为什么set的子类可以存放一个数据?怎么解决set线程安全问题? 一:Set集合子类 Set的三个子类分别是:HaseSet.TreeSet.LinkedHashSet.这三个都是线程不安全的.那么这三个子类

Java并发编程(8):多线程环境中安全使用集合API(含代码)

Java并发编程(8):多线程环境中安全使用集合API(含代码)JAVA大数据中高级架构 2018-11-09 14:44:47在集合API中,最初设计的Vector和Hashtable是多线程安全的.例如:对于Vector来说,用来添加和删除元素的方法是同步的.如果只有一个线程与Vector的实例交互,那么,要求获取和释放对象锁便是一种浪费,另外在不必要的时候如果滥用同步化,也有可能会带来死锁.因此,对于更改集合内容的方法,没有一个是同步化的.集合本质上是非多线程安全的,当多个线程与集合交互时

Java并发编程三个性质:原子性、可见性、有序性

并发编程 并发程序要正确地执行,必须要保证其具备原子性.可见性以及有序性:只要有一个没有被保证,就有可能会导致程序运行不正确 线程不安全在编译.测试甚至上线使用时,并不一定能发现,因为受到当时的CPU调度顺序,线程个数.指令重排的影响,偶然触发 线程安全的定义 比如说一个类,不论通过怎样的调度执行顺序,并且调用处不用对其进行同步操作,其都能表现出正确的行为,则这个类就是线程安全的 并发编程三个概念 原子性: 一个操作或多个操作要么全部执行且执行过程不被中断,要么不执行 可见性: 多个线程修改同一

java并发编程实战-第2章-线程安全性

2. 线程安全性 2.1 什么是线程安全性 线程安全类:当一个类被多个线程访问时,不管运行环境中如何调度,这些线程如何交替执行,并且在调用的代码部分不需要额为的同步或者协同.这个类为线程安全类 Thread-safe classes encapsulate any needed synchronization so that clients need not provide their own. 2.1.1. Example: A Stateless Servlet Stateless obje

深入浅出Java并发编程(一):线程池的使用

我们在使用线程的时候就去建立一个线程,这样实现起来非常简便,但是会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间段很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率. 那么如何解决此类问题呢? 在Java中可以通过线程池来解决这样的效果.前面有文章简单提到过线程池的使用.今天我们来详细讲解下Java的线程池,由易而难,循序渐进,步骤如下: 首先我们从最核心的ThreadPoolExecutor类的方法讲起 然后讲述它的实现原理 接着给出了相应的示例 最后我们讨论下如何