Java多线程——对象组合
我们不希望对每一次的内存访问都进行分析以确保程序是线程安全的,而是希望将一些现有的线程安全组件组合为更大规模的组件或者程序,这里介绍一些组合模式,这些组合模式能够使一个类更容易成为线程安全的,并且在维护这些类时不会无意中破坏类的安全性保证。
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多线程——对象组合的更多相关文章
- Java多线程——对象及变量的并发访问
Java多线系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多线 ...
- (转)java 多线程 对象锁&类锁
转自:http://blog.csdn.net/u013142781/article/details/51697672 最近工作有用到一些多线程的东西,之前吧,有用到synchronized同步块,不 ...
- Java多线程编程核心技术---对象及变量的并发访问(二)
数据类型String的常量池特性 在JVM中具有String常量池缓存的功能. public class Service { public static void print(String str){ ...
- java 多线程访问同一个对象数据保护的问题
java 多线程同时访问统一个数据的时候,会引起一些错误,后面的线程会修改数据,而前面的线程还在使用修改前的内容, 使用 synchronized 关键字,保证代码块只能有一个线程来访问 public ...
- java多线程中注入Spring对象问题
web应用中java多线程并发处理业务时,容易抛出NullPointerException. 原因: 线程中的Spring Bean没有被注入.web容器在启动时,没有提前将线程中的bean注入,在线 ...
- Java多线程编程核心技术(二)对象及变量的并发访问
本文主要介绍Java多线程中的同步,也就是如何在Java语言中写出线程安全的程序,如何在Java语言中解决非线程安全的相关问题.阅读本文应该着重掌握如下技术点: synchronized对象监视器为O ...
- Java多线程6:Synchronized锁代码块(this和任意对象)
一.Synchronized(this)锁代码块 用关键字synchronized修饰方法在有些情况下是有弊端的,若是执行该方法所需的时间比较长,线程1执行该方法的时候,线程2就必须等待.这种情况下就 ...
- Java多线程对同一个对象进行操作
示例: 三个窗口同时出售20张票. 程序分析: 1.票数要使用一个静态的值. 2.为保证不会出现卖出同一张票,要使用同步锁. 3.设计思路:创建一个站台类Station,继承THread,重写run方 ...
- java多线程系列(二)---对象变量并发访问
对象变量的并发访问 前言:本系列将从零开始讲解java多线程相关的技术,内容参考于<java多线程核心技术>与<java并发编程实战>等相关资料,希望站在巨人的肩膀上,再通过我 ...
随机推荐
- ERROR 2002 (HY000): Can’t connect to local MySQL server through socket ‘/var/lib/mysql/mysql.sock’ (2)
ERROR 2002 (HY000): Can’t connect to local MySQL server through socket ‘/var mysql 启动不了 ERROR 2002 ( ...
- [Training Video - 2] [Groovy Introduction]
Building test suites, Test cases and Test steps in SOAP UI Levels : test step level test case level ...
- Kali linux切换语言为中文
echo LANG="zh_CN.UTF-8" > /etc/default/locale
- 二)quartz.properties
The Properties File Quartz uses a properties file called (kudos on the originality) quartz.propertie ...
- 与table有关的布局
当IE8发布时,它将支持很多新的CSS display属性值,包括与表格相关的属性值:table.table-row和table-cell,它也是最后一款支持这些属性值的主流浏览器.它标志着复杂CSS ...
- Ubuntu的常识使用了解3
打包与压缩
- [记]Centos下流量统计使用记录
因为最近要进行centos流量统计,需求是想针对tomcat进行针对性的上下行流量时段统计及汇总,找了很多资料及命令,要么是可以针对进程的但是没有汇总,要么是有汇总但是不针对进程. 所以只能混合几个命 ...
- .NET框架源码解读之SSCLI的调试支持
阅读源码一个比较快的手段就是在调试器里阅读,这样可以在实际运行SSCLI的过程中,通过堆栈跟踪的方式查看完整的程序执行路径. 当在SSCLI环境里执行一个托管程序的时候,堆栈上通常有托管和非托管代码同 ...
- #测试框架推荐# test4j,数据库测试
# 背景 后端都是操作DB的,这块的自动化测试校验的话,是需要数据库操作的,当然可以直接封装方法来操作数据,那么有没有开源框架支持数据操作,让我们关注写sql语句?或者帮我们做mysql的断言呢? # ...
- ajax 与 form 提交的区别
有如下几种区别: 1. Ajax在提交.请求.接收时,都是异步进行的,网页不需要刷新:Form提交则是新建一个页面,哪怕是提交给自己本身的页面,也是需要刷新的: 2. A在提交时,是在后台新建一个请求 ...