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

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. zabbix监控阅读目录

    一.zabbix安装 点击查看:http://www.cnblogs.com/hwlong/p/5820321.html 二.解决乱码问题 点击查看:http://www.cnblogs.com/hw ...

  2. Ninject在.NET WebForm和MVC中的使用

    1.建立项目:Models/BLL/DAL/IBLL/IDAL/WebSite 2.WebSite要引用其余几个项目 3.NuGet中搜索Ninject,安装 Ninject.Ninject.Web. ...

  3. hack vba password, en useful...

    Unbelivibale, but I found a very simple way that really works! Do the follwoing: 1. Create a new sim ...

  4. sqlserver 开窗函数Over()的使用

    利用over(),将统计信息计算出来,然后直接筛选结果集 declare @t table(ProductID int,ProductName varchar(20),ProductType varc ...

  5. 理解数据库中的undo日志、redo日志、检查点

    数据库存放数据的文件,本文称其为data file. 数据库的内容在内存里是有缓存的,这里命名为db buffer.某次操作,我们取了数据库某表格中的数据,这个数据会在内存中缓存一些时间.对这个数据的 ...

  6. 如何规范移动应用交互设计?UI/UX设计师须知的11个小技巧

    以下内容由Mockplus团队翻译整理,仅供学习交流,Mockplus是更快更简单的原型设计工具. 十年前,手机的使用只是为了沟通. 而近几年,情况发生了很大变化,我们很难找到不使用手机的人.手机在极 ...

  7. C# Redis Server分布式缓存编程(二)(转)

    出处;http://www.cnblogs.com/davidgu/p/3263485.html 在Redis编程中, 实体和集合类型则更加有趣和实用 namespace Zeus.Cache.Red ...

  8. windows下Apache的虚拟主机配置

    1.Apache虚拟主机: 在Apache上有关于虚拟主机的具体说明,具体可以参考Apache手册,这里简单的说一下虚拟主机主要分为两种: 1.基于主机名的虚拟主机(一个IP地址,多个网站) 2.基于 ...

  9. Linux 基础教程 43-su和sudo命令

        在使用Linux系统中,有时候还需要做身份切换,这是为什么? 使用普通账号:系统日常操作的好习惯   虽然使用root对系统进行各种操作不受权限等方面的限制,但却存在重大的安全隐患,假如有人不 ...

  10. linux网络管理员

    1. 查看当前开启的所有网络服务 命令:netstat -a (all)显示所有选项,默认不显示LISTEN相关-t (tcp)仅显示tcp相关选项-u (udp)仅显示udp相关选项-n 拒绝显示别 ...