并发编程学习笔记之组合对象(三)
换了个markdown的编辑器,感觉挺方便的,但是手机端的格式显示不正确,如果读者是手机端用户,点击右上角作者主页查看,就可以了
前文回顾
通过博主之前发布的两篇博客从零开始学多线程之线程安全(一)和从零开始学多线程之共享对象(二)讲解的知识点,我们现在已经可以构建线程安全的类了,本篇将为您介绍构建类的模式,这些模式让类更容易成为线程安全的,并且不会让程序意外破坏这些类的线程安全性.
本篇博客将要讲解的知识点
- 构建线程安全类要关注那些因素.
- 使用实例限制+锁的模式,使非线程安全的对象,可以被并发的访问。
- 扩展一个线程安全的类的四种方式
构建线程安全的类
我们已经知道多线程操纵的类必须是线程安全的,否则会引发种种问题,那么如何设计线程安全的类呢?我们可以从以下三个方面考虑:
- 确定对象状态是由哪些变量构成的;
- 确定限制状态变量的不变约束;
- 制定一个管理并发访问对象状态的策略
当我们想要创建一个线程安全的类的时候,首先要关注的就是这个类的成员变量是否会被发布,如果被发布,那么就要根据对象的可变性(可变对象、不可变对象、高效不可变对象)去决定如何发布这个对象(如果不明白安全发布的概念,请移驾从零开始学多线程之共享对象(二))
然后再看状态是否依靠外部的引用实例化:如果一个对象的域引用了其他对象,那么它的状态也同时包含了被引用对象的域.
public class Domain {
private Object obj;
public Domain(Object obj) {
this.obj = obj;
}
}
这时候就要保证传入的obj对象的线程安全性.否则obj对象在外部被改变,除修改线程以外的线程,不一定能感知到对象已经被改变,就会出现过期数据的问题.
我们应该尽量使用final修饰的域,这样可以简化我们对对象的可能状态进行分析(起码保证只能指向一块内存地址空间).
然后我们再看类的状态变量是否涉及不变约束,并要保护类的不变约束
public class Minitor {
private long value = 0;
public synchronized long getValue(){
return value;
}
public synchronized long increment(){
if(value == Long.MAX_VALUE){
throw new IllegalStateException(" counter overflow");
}
return ++value;
}
}
我们通过封装使状态value没有被发布出去,这样就杜绝了客户端代码将状态置于非法的状况,保护了不变约束if(value == Long.MAX_VALUE).
维护类的线程安全性意味着要确保在并发访问的情况下,保护它的不变约束;这需要对其状态进行判断.
increment()方法,是让value++进行一次自增操作,如果value的当前值是17,那么下一个合法值是18,如果下一状态源于当前状态,那么操作必须是原子操作.
这里涉及到线程安全的可见性与原子性问题,如果您对此有疑问请移驾从零开始学多线程之线程安全(一)
实例限制
一个非线程安全的对象,通过实例限制+锁,可以让我们安全的访问它.
实例限制:把非线程安全的对象包装到自定义的对象中,通过自定义的对象去访问非线程安全的对象.
public class ProxySet {
private Set<String> set = new HashSet<>();
public synchronized void add(String value){
set.add(value);
}
public synchronized boolean contains(String value){
return set.contains(value);
}
}
HashSet是非线程安全的,我们把它包装进自定义的ProxySet类,只能通过ProxySet加锁的方法操作集合,这样HashSet又是线程安全的了.
如果我们把访问修饰符改为public的,那么这个集合还是线程安全的吗?
public Set<String> set = new HashSet<>();
这时候其它线程就可以获取到这个set集合调用add(),那么Proxyset的锁就无法起到作用了.所以他又是非线程安全的了.所以我们一定不能让实例限制的对象逸出.
将数据封装在对象内部,把对数据的访问限制在对象的方法上,更易确保线程在访问数据时总能获得正确的锁
实例限制使用的是监视器模式,监视器模式的对象封装了所有的可变状态,并由自己的内部锁保护.(完成多线程的博客后,博主就会更新关于设计模式的博客).
扩展一个线程安全的类
我们使用Java类库提供的方法可以解决我们的大部分问题,但是有时候我们也需要扩展java提供的类没有的方法.
现在假设我们要给同步的list集合,扩展一个缺少即加入的方法(必须保证这个方法是线程安全的,否则可能某一时刻会出现加入两个一样的值).
我们有四种方法可以实现这个功能:
- 修改原始的类
- 扩展这个类(继承)
- 扩展功能而,不是扩展类本身(客户端加锁,在调用这个对象的地方,使用对象的锁确保线程安全)
- 组合
我们一个一个来分析以上方法的利弊.
1.修改原始的类:
优点: 最安全的方法,所有实现类同步策略的代码仍然包含在要给源代码文件中,因此便于理解与维护.
缺点:可能无法访问源代码或没有修改的自由.
2.扩展这个类:
优点:方法相当简单直观.
缺点:并非所有类都给子类暴露了足够多的状态,以支持这种方案,还有就是同步策略的
实现会被分布到多个独立维护的源代码文件中,所以扩展一个类比直接在类中加入代码更脆弱.如果底层的类选择了
不同的锁保护它的状态变量,从而会改变它的同步策略,子类就在不知不觉中被破坏,
因为他不能再用正确的锁控制对基类状态的并发访问.
3.扩展功能而,不是扩展类本身:
public class Lock {
public List<String> list = Collections.synchronizedList(new ArrayList<String>());
public synchronized boolean putIfAbsent(String value){
boolean absent = !list.contains(value);
if(!absent){
list.add(value);
}
return absent;
}
}
这个方法是错的.使用synchronized关键字虽然同步了缺少即加入方法, 而且使用list也是线程安全的,但是他们用的不是同一个锁,list由于pulic修饰符,任意的线程都可以调用它.那么在某一时刻,满足if(!absent)不变约束的同时准备add()这个对象的时候,已经有另一个线程通过lock.list.add()过这个对象了,所以还是会出现add()两个相同对象的情况.
正确的代码,要确保他们使用的是同一个锁:
public class Lock {
public List<String> list = Collections.synchronizedList(new ArrayList<String>());
public boolean putIfAbsent(String value){
synchronized(list){
boolean absent = !list.contains(value);
if(!absent){
list.add(value);
}
return absent;
}
}
}
现在都使用的是list对象的锁,所以也就不会出现之前的情况了.
这种方式叫客户端加锁.
优点: 比较简单.
缺点:
如果说为了添加另一个原子操作而去扩展一个类容易出问题,是因为它将加锁的代码分布到对象继承体系中的多个类中.然而客户端加锁其实是更加脆弱的,因为他必须将类C中的加锁代码(locking code)置入与C完全无关的类中.在那些不关注锁策略的类中使用客户端加锁时,一定要小心
客户端加锁与扩展类有很多共同之处--所得类的行为与基类的实现之间都存在耦合.正如扩展会破坏封装性一样,客户端加锁会破坏同步策略的封装性.
- 组合对象:
public class ImprovedList<T> implements List<T> {
private final List<T> list;
public ImprovedList(List<T> list) {
this.list = list;
}
public synchronized boolean putIfAbsent(Object obj){
boolean absent = list.contains(obj);
if(absent){
list.add((T) obj);
}
return absent;
}
}
通过ImprovedList对象来操作传进来的list对象,用的都是Improved的锁.即使传进来的list不是线程安全的,ImprovedList也能保证线程安全.
优点:相比之前的方法,这种方式提供了更健壮的代码.
缺点:额外的同步带来一些微弱的性能损失.
总结
本篇博客我们讲解了,要设计线程安全的类要从三个方面考虑:
- 确定对象状态是由哪些变量构成的;
- 确定限制状态变量的不变约束;
- 制定一个管理并发访问对象状态的策略
对于非线程安全的对象,我们可以考虑使用锁+实例限制(Java监视器模式)的方式,安全的访问它们.
我们还学会了如何扩展一个线程安全的的类:扩展有四法,组合是最佳.
下一篇博客,我会为介绍几种常用的线程安全容器和同步工具.来构建线程安全的类.
好了本篇博客就分享到这里,我们下篇再见.
喜欢我的博客就请点赞+【关注】一波