[...续上文]
3. 对象的组合
前面我们已经介绍了关于线程安全和同步的一些基础知识。然而,我们并不希望每次内存访问都进行分析以确保程序是线程安全的,而是希望将一些现有的
线程安全组件组合为更大规模的组件或程序。下面介绍的一些组合模式,能够使一个类更容易成为线程安全的,并且在维护这些类时不会无意中破坏类的安全性保证。
3.1 设计线程安全的类
在线程安全的程序中,虽然可以将程序的所有状态都保存在共有的静态域中,但与将状态封装起来的程序比,还是后者的安全性更容易得到验证,并且前者在修改时更难以始终确保线程安全性。通过使用
封装技术,可以使得在不对整个程序进行分析的情况下就可以判断一个类是否是线程安全的。
在设计线程安全类的过程中,需要同时包含三个基本要素:
1. 找出构成
对象状态的所有变量。
2. 找出约束状态变量的
不变性条件。
3. 建立对象状态的
并发访问管理策略。
3.2 实例封闭
如果对象不是线程安全的,可以通过各种技术使其在多线程程序中安全地使用。可以确保该对象只能由单个线程访问
(线程封闭),或者通过一个锁来保护对该对象的所有访问。
(可以对比上一篇2.3节的线程封闭)。
封装简化了线程安全类的实现过程,它提供了一种
实例封闭机制(Instance Confinement)。当一个对象被封装在另一个对象中时,能够访问被封装对象的所有代码路径都是已知的。与对象可以由整个程序访问的情况相比,更易于对代码进行分析。通过将封闭机制与合适的加锁策略结合起来,可以确保以线程安全的方式来使用非线程安全的对象。
示例:
public class PersonSet
{
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);
}
}
这里是把Person对象封装在PersonSet中,保证对PersonSet的访问是线程安全的,不管Person类是否线程安全。但如果要线程安全的访问Person对象时,就需要额外的同步,或者将Person类也变成线程安全的类。比如自己在创建自己使用的简单缓存类时就可以选择这种方式。
实例封闭是构成线程安全类的一个最简单方式,它还使得在锁策略的选择上有更多的灵活性。
在java平台的类库中还有很多线程封闭的示例,其中有些类的唯一用途就是将非线程安全的类转化为线程安全的类,如Collections.synchronizedList及其类似方法。这些工厂方法通过“装饰器”模式将容器类封装在一个同步的包装器对象中。
3.2.1 java监视器模式
java监视器模式仅仅是一种编写代码的约定,对任何一种锁对象,只要自始至终都使用该锁对象,都可以用来保护对象的状态。如:
public class PrivateLock
{
private final Object myLock = new Object();
Widget widget;
void someMethod()
{
synchronized(myLock)
{
//访问或修改Widget的状态
}
}
}
使用
私有的锁对象而不是对象的内置锁(或任何其他可通过共有方式访问的锁)有许多优点,私有的锁对象可以将锁封装起来,使客户代码无法得到锁。
3.3 线程安全性的委托
大多数对象都是
组合对象。当从头开始创建一个类,或将多个非线程安全的类组合成一个类时,java监视器模式非常有用。但是,如果类中的各个组件都已经是线程安全的,会是什么情况呢?是否需要增加一个额外的线程安全层?答案是“视情况而定”。
前面<进阶-1>篇1.2.2节的例子,CountingFactorizer类中,我们在一个无状态的类中增加了一个AtomicLong的域,并且得到的组合对象是线程安全的。由于CountingFactorizer的状态就是AtomicLong的状态,而AtomicLong是线程安全的,因此CountingFactorizer不会对counter的状态施加额外的有效性约束,所以很容易知道CountingFactorizer是线程安全的。我们可以说CountingFactorizer将它的线程安全性委托给AtomicLong来保证。
3.3.1 独立的状态变量
前面的委托示例仅仅委托给了
单个线程安全的状态变量。我们还可以将线程安全性委托给
多个状态变量,只要这些变量是彼此独立的,即组合而成的类并不会再其包含的多个状态变量上增加任何不变性条件。
3.3.2 委托失效时
大多数组合对象都不会像上面那么简单,在他们的状态变量之间会存在某些不变性条件,比如两个值一个必须比另一个小,等,这些就存在
先检查后执行的复合操作。这时,类本身必须提供自己的加锁机制以确保这些复合操作都是原子操作。
3.3.3 发布底层的状态变量
当把线程安全性委托给某个对象的底层状态变量时,要发布这些变量必须谨慎分析。如果一个状态变量是线程安全的,并且没有任何不变性条件约束他的值,在变量的操作上也不存在任何不允许的转换,那么就可以安全地发布这个变量。
3.4 在现有的线程安全类中添加功能
Java类库中包含许多有用的“基础模块”类。通常,我们应该优先选择重用这些现有的类而不是创建新的类。但有时候某个现有的线程安全类只能支撑我们大部分的操作,此时就需要在不破坏线程安全性的情况下添加一个新的操作。
假如我们需要一个线程安全的链表,提供一个原子的“若没有则添加(put-if-absent)”的操作。同步的list类已经实现了大部分功能,但没有putIfAbsent(一些并发包里的集合是有这个操作的)。
一,要添加一个新的原子操作,最安全的方法是修改原始的类,但这通常无法做到,可能无法修改类的源码。
二,另一种方法是扩展这个类,假定可以继承这个类。排除一些类无法被继承,这种方法仍然比修改源码更加脆弱。因为现在的同步策略实现被分布在多个源码文件中,如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类会被破坏。
3.4.1 客户端加锁机制
拿Collections.synchronizedList封装的ArrayList为例,上面的两种方法都不可行。因为客户代码并不知道在同步封装器工厂方法中返回的list对象的类型。这时可用第三种策略,通过
“辅助类”扩展类的功能。
正确的做法:
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;
}
}
}
容易误导的错误做法:
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本身需要的锁。
客户端加锁是脆弱的,它将对原始类的加锁代码放到与类完全无关的其他类(辅助类或使用类)中。客户端加锁机制与继承类机制有许多共同点,二者都将派生类的行为与基类的实现耦合在一起。
3.4.2 组合
当为现有的类添加一个原子操作时,一个更好的方法:
组合。
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;
}
public synchronized void clear()
{
list.clear();
}
}
看完之后我们发现,其实设计模式里也是推崇组合代替继承,比如代理模式,装饰模式等。
分享到:
相关推荐
而`watch`对象则用于监听数据变化,当指定数据发生变化时执行回调函数。 6. **生命周期钩子**:Vue实例在其生命周期中有多个特定的时刻,每个时刻都有对应的钩子函数,如`beforeCreate`、`created`、`beforeMount`...
《Python进阶》是针对已经有一定Python基础的学习者的一本指南,旨在帮助读者提升Python编程技巧和理解Python的高级特性。书中的内容涵盖了多个关键主题,包括函数参数、调试、生成器、数据结构、装饰器、异常处理...
例如,当你定义了一个包含`username`和`pwd`字段的LoginForm类,Django会自动创建相应的`<input>`标签,这些标签可以用于在前端展示并接收用户输入。通过使用模板语言,我们可以轻松地将这些表单元素插入到HTML页面...
在Android开发中,Drawable是用于绘制图形对象的重要类,它能帮助我们实现各种自定义的UI元素,包括图标。本教程将深入讲解如何利用Drawable来创建一个自定义的电池图标,以此来展示Drawable的灵活性和实用性。 ...
此外,`AnimationSet`允许组合多个动画,实现更复杂的特效。 然而,视图动画的局限在于它无法对视图的内在属性进行改变,例如,一个旋转动画并不会真正改变按钮的方向。为了解决这个问题,Android 3.0引入了属性...
公司开始注重核心业务和核心能力,简化规模和层级,倾向于网络化结构,与供应商和分销商建立更为紧密的合作关系,将他们视为信息流的一部分,而非简单的交易对象。 营销组织的演变反映了这一趋势。从最初的简单销售...
- `git pull <remote> <branch>`:从远程仓库获取最新的更改并合并到本地分支。 #### 五、Git原理概述 - **Hash算法**:Git使用SHA-1哈希算法来唯一标识文件和提交记录,确保数据的一致性和完整性。 - **版本管理...
3. **填充形状**:使用`FillRectangle()`方法填充矩形,需要`Brush`对象: ```csharp SolidBrush brush = new SolidBrush(Color.Red); g.FillRectangle(brush, 50, 50, 100, 100); ``` 4. **绘制圆和椭圆**:`...
1. **JSP指令**:例如`<%@ page %>`, `<%@ include %>`, `<jsp:include>`等,用于设置页面属性、引入其他文件或实现页面组合。 2. **JSP表达式**:`<%= expression %>`用于将Java表达式的值输出到HTML页面。 3. **...
视图是UI的基本构建块,如Text、Image、Button等,它们可以通过各种方式组合来创建复杂的界面。状态则用来管理UI的数据,当状态变化时,SwiftUI会自动更新对应的视图。环境对象允许不同视图之间共享数据,尤其在处理...
<title>dcharts-widget示例</title> <!-- 引入dcharts-widget库 --> <script src="https://cdn.example.com/dcharts-widget.min.js"></script> </head> <body> <div id="chart-container"></div> <script> // ...
- 使用<jsp:include>进行页面组合 - <jsp:forward>和<jsp:redirect>处理页面跳转 - <jsp:useBean>创建和操作JavaBean - <jsp:setProperty>和<jsp:getProperty>设置和获取Bean属性 2. **JSTL(JavaServer Pages ...
XML文件中,通过<animation-list>标签定义帧动画,每帧由<item>标签表示,指定对应的图片资源和持续时间。 3. **启动动画**:在Activity的onCreate()方法中,通过ImageView的getDrawable().mutate()方法将背景转换...
然后,课程可能会涉及**JSP动作标签**,如`<jsp:include>`, `<jsp:forward>`, `<jsp:param>`等,它们简化了页面的组合和控制流程。特别是,`<jsp:useBean>`和`<jsp:setProperty>`用于实例化和操作Java Bean,这是JSP...
template<> struct Factorial<0> { static const int value = 1; }; ``` 在这个例子中,我们定义了一个名为`Factorial`的模板结构体,它用于计算任何正整数的阶乘。当N等于0时,阶乘的结果为1(这是阶乘的定义),...
<version>4.13.2</version> <!-- 最新版本号可能会有所不同 --> <scope>test</scope> </dependency> </dependencies> ``` - Gradle: ```groovy testImplementation 'junit:junit:4.13.2' // 最新版本号可能...
3. **JSP指令**:比如`<%@ page %>`, `<%@ include %>`, 和`<%@ taglib %>`。`<%@ page %>`用来设置JSP页面的属性,如编码、错误页等;`<%@ include %>`用于静态或动态地组合多个文件;`<%@ taglib %>`导入自定义...
- **logic标签**:处理条件和循环,如<logic:equal>、<logic:notEqual>等,简化了JSP中的条件判断。 以上就是北大青鸟ACCP5.0课程中关于SSH中的Struts框架的总结,通过理解Struts的工作原理和应用开发步骤,开发者...