从零开始学多线程之组合对象(三)

前文回顾 通过博主之前发布的两篇博客从零开始学多线程之线程安全(一)和从零开始学多线程之共享对象(二)讲解的知识点,我们现在已经可以构建线程安全的类了,本篇将为您介绍构建类的模式,这些模式让类更容易成为线程安全的,并且不会让程序意外破坏这些类的线程安全性. 本篇博客将要讲解的知识点 构建线程安全类要关注那些因素. 使用实例限制+锁的模式,使非线程安全的对象,可以被并发的访问。 扩展一个线程安全的类的四种方式 构建线程安全的类 我们已经知道多线程操纵的类必须是线程安全的,否则会引发种种问题,那么如何设计线程安全的类呢?我们可以从以下三个方面考虑: 确定对象状态是由哪些变量构成的; 确定限制状态变量的不变约束; 制定一个管理并发访问对象状态的策略 当我们想要创建一个线程安全的类的时候,首先要关注的就是这个类的成员变量是否会被发布,如果被发布,那么就要根据对象的可变性(可变对象、不可变对象、高效不可变对象)去决定如何发布这个对象(如果不明白安全发布的概念,请移驾从零开始学多线程之共享对象(二)) 然后再看状态是否依靠外部的引用实例化:如果一个对象的域引用了其他对象,那么它的状态也同时包含了被引用对象的域. 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 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 set = new HashSet<>(); 这时候其它线程就可以获取到这个set集合调用add(),那么Proxyset的锁就无法起到作用了.所以他又是非线程安全的了.所以我们一定不能让实例限制的对象逸出. 将数据封装在对象内部,把对数据的访问限制在对象的方法上,更易确保线程在访问数据时总能获得正确的锁 实例限制使用的是监视器模式,监视器模式的对象封装了所有的可变状态,并由自己的内部锁保护.(完成多线程的博客后,博主就会更新关于设计模式的博客). 扩展一个线程安全的类 我们使用Java类库提供的方法可以解决我们的大部分问题,但是有时候我们也需要扩展java提供的类没有的方法. 现在假设我们要给同步的list集合,扩展一个缺少即加入的方法(必须保证这个方法是线程安全的,否则可能某一时刻会出现加入两个一样的值). 我们有四种方法可以实现这个功能: 修改原始的类 扩展这个类(继承) 扩展功能而,不是扩展类本身(客户端加锁,在调用这个对象的地方,使用对象的锁确保线程安全) 组合 我们一个一个来分析以上方法的利弊. 1.修改原始的类: 优点: 最安全的方法,所有实现类同步策略的代码仍然包含在要给源代码文件中,因此便于理解与维护. 缺点:可能无法访问源代码或没有修改的自由. 2.扩展这个类: 优点:方法相当简单直观. 缺点:并非所有类都给子类暴露了足够多的状态,以支持这种方案,还有就是同步策略的 实现会被分布到多个独立维护的源代码文件中,所以扩展一个类比直接在类中加入代码更脆弱.如果底层的类选择了 不同的锁保护它的状态变量,从而会改变它的同步策略,子类就在不知不觉中被破坏, 因为他不能再用正确的锁控制对基类状态的并发访问. 3.扩展功能而,不是扩展类本身: public class Lock { public List list = Collections.synchronizedList(new ArrayList()); 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 list = Collections.synchronizedList(new ArrayList()); 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 implements List { private final List list; public ImprovedList(List 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监视器模式)的方式,安全的访问它们. 我们还学会了如何扩展一个线程安全的的类:扩展有四法,组合是最佳. 下一篇博客,我会为介绍几种常用的线程安全容器和同步工具.来构建线程安全的类. 好了本篇博客就分享到这里,我们下篇再见.https://www.cnblogs.com/xisuo/p/9772067.html
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信