并发编程学习笔记之组合对象(三)

换了个markdown的编辑器,感受挺方便的,可是手机端的格式显示不正确,若是读者是手机端用户,点击右上角做者主页查看,就能够了html

前文回顾

经过博主以前发布的两篇博客从零开始学多线程之线程安全(一)从零开始学多线程之共享对象(二)讲解的知识点,咱们如今已经能够构建线程安全的类了,本篇将为您介绍构建类的模式,这些模式让类更容易成为线程安全的,而且不会让程序意外破坏这些类的线程安全性.java

本篇博客将要讲解的知识点

  1. 构建线程安全类要关注那些因素.
  2. 使用实例限制+锁的模式,使非线程安全的对象,能够被并发的访问。
  3. 扩展一个线程安全的类的四种方式

构建线程安全的类

咱们已经知道多线程操纵的类必须是线程安全的,不然会引起种种问题,那么如何设计线程安全的类呢?咱们能够从如下三个方面考虑:设计模式

  1. 肯定对象状态是由哪些变量构成的;
  2. 肯定限制状态变量的不变约束;
  3. 制定一个管理并发访问对象状态的策略

当咱们想要建立一个线程安全的类的时候,首先要关注的就是这个类的成员变量是否会被发布,若是被发布,那么就要根据对象的可变性(可变对象、不可变对象、高效不可变对象)去决定如何发布这个对象(若是不明白安全发布的概念,请移驾从零开始学多线程之共享对象(二))安全

而后再看状态是否依靠外部的引用实例化:若是一个对象的域引用了其余对象,那么它的状态也同时包含了被引用对象的域.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. 扩展功能而,不是扩展类自己(客户端加锁,在调用这个对象的地方,使用对象的锁确保线程安全)
  4. 组合

咱们一个一个来分析以上方法的利弊.

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彻底无关的类中.在那些不关注锁策略的类中使用客户端加锁时,必定要当心

客户端加锁与扩展类有不少共同之处--所得类的行为与基类的实现之间都存在耦合.正如扩展会破坏封装性同样,客户端加锁会破坏同步策略的封装性.

  1. 组合对象:
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也能保证线程安全.

优势:相比以前的方法,这种方式提供了更健壮的代码.

缺点:额外的同步带来一些微弱的性能损失.

总结

本篇博客咱们讲解了,要设计线程安全的类要从三个方面考虑:

  1. 肯定对象状态是由哪些变量构成的;
  2. 肯定限制状态变量的不变约束;
  3. 制定一个管理并发访问对象状态的策略

对于非线程安全的对象,咱们能够考虑使用锁+实例限制(Java监视器模式)的方式,安全的访问它们.

咱们还学会了如何扩展一个线程安全的的类:扩展有四法,组合是最佳.

下一篇博客,我会为介绍几种经常使用的线程安全容器同步工具.来构建线程安全的类.

好了本篇博客就分享到这里,咱们下篇再见.

相关文章
相关标签/搜索