我们不希望对每一次的内存访问都进行分析以确保程序是线程安全的,而是希望将一些现有的线程安全组件组合为更大规模的组件或者程序,这里介绍一些组合模式,这些组合模式能够使一个类更容易成为线程安全的,并且在维护这些类时不会无意中破坏类的安全性保证。

1、设计线程安全的类

  在设计线程安全类的过程中,需要包含以下三个基本要素:

  (1)、找出构成对象状态的所有变量。

  (2)、找出约束状态变量的不变性条件。

  (3)、建立对象状态的并发访问管理策略。

  对象的状态:如果对象中所得的域都是基本类型的变量,那么这些域将构成对象的全部状态;如果在对象的域中引用了其他对象,那么该对象的状态包括被引用对象的域。

1.1 收集同步需求

  对象与变量都有 一个状态空间,即所有的可能值。状态空间越小,就越容易判断线程状态。final类型的域使用得越多,就越能简化对象可能状态的分析过程。

  在许多类中都定义了一些不可变调条件(某个域的状态范围),用于判断状态是有效的还是无效的。

  同样,在操作中还包含一些后验条件判断状态迁移是否是有效的。如果counter当前状态是17,那么下一个状态只能是18.,当下一个状态需要依赖上一个状态时,这个操作必须是复合操作。

  由于不变性条件以及后验条件在状态以及 状态转换上施加了各种约束,因此就需要额外的同步和封装。

  如果不了解对象的不变性条件和后验条件,那么就不能确保线程安全性。要满足在状态变量的有效值和转换上的各种约束条件,就需要借助于原子性和封装性。

1.2 依赖状态操作

  在某些对象的方法中还包含一些基于状态的先验条件,例如不能从空队列中删除一个元素,在删除元素之前,必须得先判断该队列非空。

1.3 状态的所有权

  所有权在Java中只是一个设计中的要素,在语言层面没有明显的变现。所有权意味着控制权,如果发布了某个可变对象的引用,则意味着共享控制权。在定义哪些变量构成对象的状态时,只考虑对象拥有的数据。

2、实例封闭

  将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。

  被封闭的作用域可以是:

  (1)、一个实例中:作为一个私有成员

  (2)、某个作用域中:作为局部变量

  (3)、线程里:将对象从一个方法传递到另一个方法

2.1、Java监视器模式

  从线程封闭原子以及逻辑推论可以得到Java监视器模式。遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象的内置锁来保护。其可变状态都是私有的,并且涉企到该状态的方法都有一个内置锁来保护,而Java的内置锁也称为监视器锁或者监视器。在许多类中都使用了Java监视器模式,例如Vector和Hashtable。

3、线程安全性委托、独立的状态变量

  当一个对象有多个状态变量时,即多个域,并且每个状态变量没有耦合性,或者说不相互影响,我们就讲是独立的状态变量。当一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效的状态转换,那么可以将线程安全委托给底层的状态变量,只要每个独立的状态变量是线程安全的,那么整个类就是线程安全的。

  假如类的多个状态变量是相互影响的,即使每个状态变量都是线程安全的,那么整个类也有可能不是线程安全的。比如下面的代码:

 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("不能设置lower > upper");
}
lower.set(i);
} public void setUpper(int i){
if(i < lower.get()){
throw new IllegalArgumentException("不能设置upper < lower");
}
upper.set(i);
} }

  NumerRangle不是线程安全的,没有维持对下界和上界进行约束的不变性条件,setLower和setUpper等方法都尝试维护不变性条件,但无法做到setLower和setUpper都是"先检查后执行"的操作,也没有使用加锁机制来维护这些操作的原子性。假如一个线程调用setLower(5),另一个线程调用setUpper(1),那么在一些错误的执行顺序中,两个设置都通过验证了,并且设置成功,结果得到的取值范围就是(5, 1),这是一个无效的状态。虽然分开来讲lower和upper都是线程安全的,但是组合在一起,却不是线程安全的,主要是他们是相互影响的而不是独立的状态变量。因此NumberRangle不能线程安全委托给它的线程安全状态变量。在setLower和setUpper方法中必须加上锁。

4、在现有的线程安全类上添加新的功能

  比如我们要给一个链表添加一个新的功能:“若没有则添加”操作,有两个方法,一是客户端加锁机制,二是组合,下面分别介绍这两种方法

4.1 客户端加锁机制

  客户端加锁机制主要是将扩展代码放入一个"辅助类"中,如下面代码:

 public class ListHelper<E> {
public List<E> list = (List<E>) Collections.synchronizedCollection(new ArrayList<E>()); public synchronized boolean putIfAbsent(E x){
boolean absent = !list.contains(x);
if(absent){
list.add(x);
}
return absent; }
}

  这中方式仍然不是线程安全的,虽然putIfAbsent方法已经声明为synchronized,但是这个锁和list上的锁是不一样的,不是同一个锁,假如有一个线程正在调用putIfAbsent方法,其他的线程仍然可以对list进行操作,这意味着putIfAbsent方法相对于list的其他操作来说并不是原子性的。

  要想putIfAbsent方法能正确执行,必须使List在实现客户端加锁或外部加锁时使用同一个锁,我们对上面的代码进行修改:

 public class ListHelper<E> {
public List<E> list = (List<E>) Collections.synchronizedCollection(new ArrayList<E>()); public boolean putIfAbsent(E x){
synchronized(list){
boolean absent = !list.contains(x);
if(absent){
list.add(x);
}
return absent;
} }
}

  客户端加锁机制是将扩展的类和基类的实现耦合在一起,正如扩展会破坏实现的封装性,客户端加锁同样破坏了同步策略的封装性。

4.2 组合

  当为现有的类添加一个原子操作时,有一个更好的办法:组合(Composition)。如下代码:

 public class ImprovedList<T> implements List<T> {

     private final List<T> list;
public ImprovedList(List<T> list){
this.list = list;
} public synchronized boolean putIfAbsent(E x){
boolean absent = !list.contains(x);
if(absent){
list.add(x);
}
return absent; }
public synchronized boolean add(T arg0) {
return list.add(arg0);
}
// ... 按照类似的方式委托list其他方法 }

  ImprovedList通过自身的内置锁加了一层额外的加锁,并不关系底层的List是否是线程安全的,即使List不是线程安全的或者修改了它的加锁实现,ImprovedList也会提供一致的加锁机制来实现线程安全性。事实上,我们使用了Java监视器模式来封装现有的的List,并且只要在类中拥有指向底层List的唯一外部引用(ImprovedList的构造函数),就能确保线程安全性。

 

Java多线程——对象组合的更多相关文章

  1. Java多线程——对象及变量的并发访问

    Java多线系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多线 ...

  2. (转)java 多线程 对象锁&类锁

    转自:http://blog.csdn.net/u013142781/article/details/51697672 最近工作有用到一些多线程的东西,之前吧,有用到synchronized同步块,不 ...

  3. Java多线程编程核心技术---对象及变量的并发访问(二)

    数据类型String的常量池特性 在JVM中具有String常量池缓存的功能. public class Service { public static void print(String str){ ...

  4. java 多线程访问同一个对象数据保护的问题

    java 多线程同时访问统一个数据的时候,会引起一些错误,后面的线程会修改数据,而前面的线程还在使用修改前的内容, 使用 synchronized 关键字,保证代码块只能有一个线程来访问 public ...

  5. java多线程中注入Spring对象问题

    web应用中java多线程并发处理业务时,容易抛出NullPointerException. 原因: 线程中的Spring Bean没有被注入.web容器在启动时,没有提前将线程中的bean注入,在线 ...

  6. Java多线程编程核心技术(二)对象及变量的并发访问

    本文主要介绍Java多线程中的同步,也就是如何在Java语言中写出线程安全的程序,如何在Java语言中解决非线程安全的相关问题.阅读本文应该着重掌握如下技术点: synchronized对象监视器为O ...

  7. Java多线程6:Synchronized锁代码块(this和任意对象)

    一.Synchronized(this)锁代码块 用关键字synchronized修饰方法在有些情况下是有弊端的,若是执行该方法所需的时间比较长,线程1执行该方法的时候,线程2就必须等待.这种情况下就 ...

  8. Java多线程对同一个对象进行操作

    示例: 三个窗口同时出售20张票. 程序分析: 1.票数要使用一个静态的值. 2.为保证不会出现卖出同一张票,要使用同步锁. 3.设计思路:创建一个站台类Station,继承THread,重写run方 ...

  9. java多线程系列(二)---对象变量并发访问

    对象变量的并发访问 前言:本系列将从零开始讲解java多线程相关的技术,内容参考于<java多线程核心技术>与<java并发编程实战>等相关资料,希望站在巨人的肩膀上,再通过我 ...

随机推荐

  1. 23-吝啬的国度(vector+深搜)

    吝啬的国度 时间限制:1000 ms  |  内存限制:65535 KB 难度:3   描述 在一个吝啬的国度里有N个城市,这N个城市间只有N-1条路把这个N个城市连接起来.现在,Tom在第S号城市, ...

  2. day25 map,filter,reduce 内置函数,作业

    =====================作业一#用map来处理字符串列表啊,把列表中所有人都变成sb,比方alex_sbname=['alex','wupeiqi','yuanhao']###### ...

  3. 让Ubuntu使用阿里云国内源,解决下载速度慢问题。

    阿里云镜像官方地址 http://mirrors.aliyun.com/ 找到最新源地址列表: http://www.linuxdiyf.com/linux/23163.html 软件包管理中心(推荐 ...

  4. ResourceUtils读取properties文件

    注意: properties文件要放在classPath下面,也就是与src下. path.properties(在properties文件中\代表着没有完,下行同本行是一个内容) perfectMa ...

  5. 07Mendel's First Law

    Problem Figure 2. The probability of any outcome (leaf) in a probability tree diagram is given by th ...

  6. Ural 1519 Formula 1 (DP)

    题意:给定一个 n * m 的矩阵,问你能花出多少条回路. #pragma comment(linker, "/STACK:1024000000,1024000000") #inc ...

  7. HDU 6065 RXD, tree and sequence (LCA+DP)

    题意:给定上一棵树和一个排列,然后问你把这个排列分成m个连续的部分,每个部分的大小的是两两相邻的LCA的最小深度,问你最小是多少. 析:首先这个肯定是DP,然后每个部分其实就是里面最小的那个LCA的深 ...

  8. Gym 101201I Postman (贪心)

    题意:有个邮递员,要送信,每次最多带 m 封信,有 n 个地方要去送,每个地方有x 封要送,每次都到信全送完了,再回去,对于每个地方,可以送多次直到送够 x 封为止. 析:一个很简单的贪心,就是先送最 ...

  9. awk基础03-分支和循环语句

        awk既然是一门解释型语言,则就可以支持如分支语句.循环语句等.今天就来学习一下在awk中的分支和循环语句.如果您有过任何一门编程语言的基础,则下面所讲内容也是很好理解的. 分支语句 if-e ...

  10. 团队博客第三周(Running Duck队)

    代码链接:Tetris 码云链接 一.需求&原型改进 1.原型改进 汉字方块每次可生成一个并逐渐下落 可通过触摸左右下滑动实现方块的左右移动和快速下滑 左上角按钮可对汉字进行暂时保存和替换 右 ...