并发编程学习笔记7—— 并发设计模式


一、不变性(Immutability)模式

不变性模式是让共享变量只有读操作,而没有写操作,来解决并发问题的。

1. 创建具备不变性的类

将一个类所有的属性都设置成 final 的,并且只允许存在只读方法,那么这个类基本上就具备不可变性了。更严格的做法是这个类本身也是 final 的,也就是不允许继承。因为子类可以覆盖父类的方法,有可能改变不可变性。

比如 String 这个类以及它的属性 value[]都是 final 的;而 replace() 方法的实现,表面上似乎是写操作,但没有修改 value[],而是将替换后的字符串作为返回值返回了。

public final class String {
  private final char value[];
  // 字符替换
  String replace(char oldChar, 
      char newChar) {
    //无需替换,直接返回this  
    if (oldChar == newChar){
      return this;
    }

    int len = value.length;
    int i = -1;
    /* avoid getfield opcode */
    char[] val = value; 
    //定位到需要替换的字符位置
    while (++i < len) {
      if (val[i] == oldChar) {
        break;
      }
    }
    //未找到oldChar,无需替换
    if (i >= len) {
      return this;
    } 
    //创建一个buf[],这是关键
    //用来保存替换后的字符串
    char buf[] = new char[len];
    for (int j = 0; j < i; j++) {
      buf[j] = val[j];
    }
    while (i < len) {
      char c = val[i];
      buf[i] = (c == oldChar) ? 
        newChar : c;
      i++;
    }
    //创建一个新的字符串返回
    //原字符串不会发生任何变化
    return new String(buf, true);
  }
}

具备不可变性的类,如果要提供类似修改的功能,可以创建一个新的不可变对象。缺点是浪费内存。

2. 利用享元模式避免创建重复对象

Long、Integer、Short、Byte 等这些基本数据类型的包装类都用到了享元模式。

享元模式本质上其实就是一个对象池,利用享元模式创建对象的逻辑也很简单:创建之前,首先去对象池里看看是不是存在;如果已经存在,就利用对象池里的对象;如果不存在,就会新创建一个对象,并且把这个新创建出来的对象放进对象池里。

Long 这个类并没有照搬享元模式,Long 内部维护了一个静态的对象池,缓存了[-128,127]之间的数字,这个对象池在 JVM 启动的时候就创建好了。

public static Long valueOf(long l) {
        final int offset = 128;
        if (l >= -128 && l <= 127) { // will cache
            return LongCache.cache[(int)l + offset];
        }
        return new Long(l);
    }

private static class LongCache {
        private LongCache(){}

        static final Long cache[] = new Long[-(-128) + 127 + 1];

        static {
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Long(i - 128);
        }
    }

3. 使用 Immutability 模式的注意事项

  • 对象的所有属性都是 final 的,并不能保证不可变性;如果属性是一个对象,那这个对象的属性可能会改变。在使用 Immutability 模式的时候一定要确认保持不变性的边界在哪里,是否要求属性对象也具备不可变性。
  • 不可变对象虽然是线程安全的,但是并不意味着引用这些不可变对象的对象就是线程安全的。如果仅需要保持可见性,那么可以声明为 volatile 变量。如果需要保证原子性,那么可以通过原子类来实现。
public class SafeWM {
  class WMRange{
    final int upper;
    final int lower;
    WMRange(int upper,int lower){
    //省略构造函数实现
    }
  }
  final AtomicReference<WMRange>
    rf = new AtomicReference<>(
      new WMRange(0,0)
    );
  // 设置库存上限
  void setUpper(int v){
    while(true){
      WMRange or = rf.get();
      // 检查参数合法性
      if(v < or.lower){
        throw new IllegalArgumentException();
      }
      WMRange nr = new
          WMRange(v, or.lower);
      if(rf.compareAndSet(or, nr)){
        return;
      }
    }
  }
}

4. 总结

具备不变性的对象,只有一种状态,这个状态由对象内部所有的不变属性共同决定。还有一种更简单的不变性对象,那就是无状态。无状态对象内部没有属性,只有方法。除了无状态的对象,还有无状态的服务、无状态的协议等等。

无状态有很多好处,最核心的一点就是性能。在多线程领域,无状态对象没有线程安全问题,无需同步处理,自然性能很好;在分布式领域,无状态意味着可以无限地水平扩展,所以分布式领域里面性能的瓶颈一定不是出在无状态的服务节点上。


二、Copy-on-Write 模式

String 这个类在实现 replace() 方法的时候,本质上是一种 Copy-on-Write 方法。不可变对象的写操作往往都是使用 Copy-on-Write 方法解决的。

CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个容器,通过 Copy-on-Write 可以使读操作是无锁的。

CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个 Copy-on-Write 容器在修改的时候会复制整个数组,所以如果容器经常被修改或者这个数组本身就非常大的时候,是不建议使用的。反之,如果是修改非常少、数组数量也不大,并且对读性能要求苛刻的场景,使用 Copy-on-Write 容器效果就非常好了。


三、Guarded Suspension,等待唤醒机制的规范实现


class GuardedObject<T>{
  //受保护的对象
  T obj;
  final Lock lock = 
    new ReentrantLock();
  final Condition done =
    lock.newCondition();
  final int timeout=1;
  //获取受保护对象  
  T get(Predicate<T> p) {
    lock.lock();
    try {
      //MESA管程推荐写法
      while(!p.test(obj)){
        done.await(timeout, 
          TimeUnit.SECONDS);
      }
    }catch(InterruptedException e){
      throw new RuntimeException(e);
    }finally{
      lock.unlock();
    }
    //返回非空的受保护对象
    return obj;
  }
  //事件通知方法
  void onChanged(T obj) {
    lock.lock();
    try {
      this.obj = obj;
      done.signalAll();
    } finally {
      lock.unlock();
    }
  }
}

四、Balking模式

Balking 模式即多线程版本的 if,业务逻辑依赖于某个状态变量的状态,当状态满足某个条件时,执行某个业务逻辑。

常见的例子是各种编辑器提供的自动保存功能。存盘操作的前提是文件做过修改,如果文件没有执行过修改操作,就需要快速放弃存盘操作。

class AutoSaveEditor{
  //文件是否被修改过
  boolean changed=false;
  //定时任务线程池
  ScheduledExecutorService ses = 
    Executors.newSingleThreadScheduledExecutor();
  //定时执行自动保存
  void startAutoSave(){
    ses.scheduleWithFixedDelay(()->{
      autoSave();
    }, 5, 5, TimeUnit.SECONDS);  
  }
  //自动存盘操作
  void autoSave(){
  synchronized(this){
    if (!changed) {
      return;
    }
    changed = false;
  }
  //执行存盘操作
  //省略且实现
  this.execSave();
}
//编辑操作
void edit(){
  //省略编辑逻辑
  ......
  synchronized(this){
    changed = true;
  }
}  
}

用 synchronized 实现 Balking 模式最为稳妥,建议你实际工作中也使用这个方案。不过在某些特定场景下,也可以使用 volatile 来实现,但使用 volatile 的前提是对原子性没有要求

class AutoSaveEditor{
  //文件是否被修改过
  volatile boolean changed=false;
  //定时任务线程池
  ScheduledExecutorService ses = 
    Executors.newSingleThreadScheduledExecutor();
  //定时执行自动保存
  void startAutoSave(){
    ses.scheduleWithFixedDelay(()->{
      autoSave();
    }, 5, 5, TimeUnit.SECONDS);  
  }
  //自动存盘操作
  void autoSave(){
    if (!changed) {
      return;
    }
    changed = false;
  //执行存盘操作
  //省略且实现
  this.execSave();
}
//编辑操作
void edit(){
  //省略编辑逻辑
  ......
  changed = true;
}  
}

五、worker thread模式

即线程池使用的模式。


六、Reactor in Netty


Netty 中最核心的概念是事件循环(EventLoop),其实也就是 Reactor 模式中的 Reactor,负责监听网络事件并调用事件处理器进行处理。

在 4.x 版本的 Netty 中,网络连接和 EventLoop 是稳定的多对 1 关系,而 EventLoop 和 Java 线程是 1 对 1 关系,这里的稳定指的是关系一旦确定就不再发生变化。

也就是说一个网络连接只会对应唯一的一个 EventLoop,而一个 EventLoop 也只会对应到一个 Java 线程,所以一个网络连接只会对应到一个 Java 线程。最大的好处就是对于一个网络连接的事件处理是单线程的,这样就避免了各种并发问题。

Netty 中还有一个核心概念是 EventLoopGroup。实际使用中,一般都会创建两个 EventLoopGroup,一个称为 bossGroup,一个称为 workerGroup。

这个和 socket 处理网络请求的机制有关,socket 处理 TCP 网络连接请求,是在一个独立的 socket 中,每当有一个 TCP 连接成功建立,都会创建一个新的 socket,之后对 TCP 连接的读写都是由新创建处理的 socket 完成的。也就是说处理 TCP 连接请求和读写请求是通过两个不同的 socket 完成的


思考题

1、下面的示例代码中,Account 的属性是 final 的,并且只有 get 方法,那这个类是不是具备不可变性呢?

public final class Account{
  private final StringBuffer user;
  public Account(String user){
    this.user = new StringBuffer(user);
  }
  
  public StringBuffer getUser(){
    return this.user;
  }
  public String toString(){
    return "user"+user;
  }
}

:这个类不具备不可变性,因为StringBuffer 中的value 是可变的,但是这个类线程安全。


2、Java 提供了 CopyOnWriteArrayList,为什么没有提供 CopyOnWriteLinkedList 呢?
:数组占用连续的内存空间,复制比较容易;而链表的复制比较复杂,在复制每个节点时都要注意静态条件。另外链表的修改比起数组的影响要小很多,配合分段锁等技术也可以实现线程安全,不需要复制整个链表。而链表便于顺序读和写操作的特性,与Copy On Write读多写少违背。


3、guarded suspension模式中用 await 实现等待,可以改成 sleep 吗?

//获取受保护对象  
T get(Predicate<T> p) {
  try {
    while(!p.test(obj)){
      TimeUnit.SECONDS
        .sleep(timeout);
    }
  }catch(InterruptedException e){
    throw new RuntimeException(e);
  }
  //返回非空的受保护对象
  return obj;
}
//事件通知方法
void onChanged(T obj) {
  this.obj = obj;
}

:最好不要。从性能上看,sleep 在时间没到之前无法被唤醒,会影响响应时间,如果sleep时间太短又会反复唤醒,浪费性能。从可见性上,即时 obj 已经被赋值,也未必能马上看到。


参考资料:王宝令----Java并发编程实战

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值