换了个markdown的编辑器,感受挺方便的,可是手机端的格式显示不正确,若是读者是手机端用户,点击右上角做者主页查看,就能够了html
前文回顾
经过博主以前发布的两篇博客从零开始学多线程之线程安全(一)和从零开始学多线程之共享对象(二)讲解的知识点,咱们如今已经能够构建线程安全的类了,本篇将为您介绍构建类的模式,这些模式让类更容易成为线程安全的,而且不会让程序意外破坏这些类的线程安全性.java
本篇博客将要讲解的知识点
- 构建线程安全类要关注那些因素.
- 使用实例限制+锁的模式,使非线程安全的对象,能够被并发的访问。
- 扩展一个线程安全的类的四种方式
构建线程安全的类
咱们已经知道多线程操纵的类必须是线程安全的,不然会引起种种问题,那么如何设计线程安全的类呢?咱们能够从如下三个方面考虑:设计模式
- 肯定对象状态是由哪些变量构成的;
- 肯定限制状态变量的不变约束;
- 制定一个管理并发访问对象状态的策略
当咱们想要建立一个线程安全的类的时候,首先要关注的就是这个类的成员变量是否会被发布,若是被发布,那么就要根据对象的可变性(可变对象、不可变对象、高效不可变对象)去决定如何发布这个对象(若是不明白安全发布的概念,请移驾从零开始学多线程之共享对象(二))安全
而后再看状态是否依靠外部的引用实例化:若是一个对象的域引用了其余对象,那么它的状态也同时包含了被引用对象的域.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监视器模式)的方式,安全的访问它们.
咱们还学会了如何扩展一个线程安全的的类:扩展有四法,组合是最佳.
下一篇博客,我会为介绍几种经常使用的线程安全容器和同步工具.来构建线程安全的类.
好了本篇博客就分享到这里,咱们下篇再见.