这次彻底搞懂并发编程的Balking模式

“多线程版本的if”来理解Guarded Suspension模式,不同于单线程中的if,这个“多线程版本的if”是需要等待的,而且还很执着,必须要等到条件为真。但很显然这个世界,不是所有场景都需要这么执着,有时候我们还需要快速放弃。

需要快速放弃的一个最常见的例子是各种编辑器提供的自动保存功能。自动保存功能的实现逻辑一般都是隔一定时间自动执行存盘操作,存盘操作的前提是文件做过修改,如果文件没有执行过修改操作,就需要快速放弃存盘操作。

下面的示例代码将自动保存功能代码化了,很显然AutoSaveEditor这个类非线程安全,因为对共享变量changed的读写没有使用同步,那如何保证AutoSaveEditor的线程安全性呢?

class AutoSaveEditor {
  // 文件是否被修改过
  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;
  }
}

解决这个问题,最简单直接对读写共享变量changed的方法autoSave()和edit()都加互斥锁。虽然简单,但性能很差,因为锁的范围太大了。
可以缩小锁的粒度,只在读写共享变量changed的地方加锁,如下:

void autoSave(){
  synchronized(this){
    if (!changed) {
      return;
    }
    changed = false;
  }
  // 存盘
  this.execSave();
}

void edit(){
  ...
  synchronized(this){
    changed = true;
  }
}  

发现,示例中的共享变量是一个状态变量,业务逻辑依赖于这个状态变量的状态:当状态满足某条件时,执行某个业务逻辑,其本质就是个if,放到多线程场景里,就是一种“多线程版本的if”。
这种“多线程版本的if”应用场景很多,有人就把它总结成了一种设计模式 - Balking模式。

实现Balking V1.0

Balking实质是规范化解决“多线程版本的if”的方案,使用Balking改造自动保存案例:

boolean changed = false;

void autoSave(){
  synchronized(this){
    if (!changed) {
      return;
    }
    changed = false;
  }
  this.execSave();
}

void edit(){
  ...
  change();
}

// 改变状态
// 将edit()中对changed的赋值放进change(),达到解耦并发处理逻辑和业务逻辑
void change(){
  synchronized(this){
    changed = true;
  }
}

synchronized实现Balking最为稳妥,推荐工作中使用。

实现Balking V2.0

某些特定场景下,也可以使用volatile来实现,前置条件是不要求原子性。

比如RPC框架中,【本地路由表】要和【注册中心】进行信息同步,应用启动时,会将应用依赖服务的路由表从【注册中心】同步到本地路由表中,如果应用重启的时候【注册中心】宕机,则会导致该应用依赖的服务均不可用,因为找不到依赖服务的路由表。

为防止这种极端情况出现,RPC框架可将本地路由表自动保存到本地文件,若重启时,注册中心宕机,那就从本地文件中恢复重启前的路由表。这也是一种降级方案。

自动保存路由表和前面介绍的编辑器自动保存原理是一样,也可用Balking模式,这里采用volatile。

因为对changed和rt的写操作不要求原子性,且采用scheduleWithFixedDelay()这种调度方式,能保证同一时刻只有一个线程执行autoSave()

//路由表信息
public class RouterTable {
  //Key:接口名
  //Value:路由集合
  ConcurrentHashMap<String, CopyOnWriteArraySet<Router>> 
    rt = new ConcurrentHashMap<>();    
  //路由表是否发生变化
  volatile boolean changed;
  //将路由表写入本地文件的线程池
  ScheduledExecutorService ses=
    Executors.newSingleThreadScheduledExecutor();
  //启动定时任务
  //将变更后的路由表写入本地文件
  public void startLocalSaver(){
    ses.scheduleWithFixedDelay(()->{
      autoSave();
    }, 1, 1, MINUTES);
  }
  //保存路由表到本地文件
  void autoSave() {
    if (!changed) {
      return;
    }
    changed = false;
    //将路由表写入本地文件
    //省略其方法实现
    this.save2Local();
  }
  //删除路由
  public void remove(Router router) {
    Set<Router> set=rt.get(router.iface);
    if (set != null) {
      set.remove(router);
      //路由表已发生变化
      changed = true;
    }
  }
  //增加路由
  public void add(Router router) {
    Set<Router> set = rt.computeIfAbsent(
      route.iface, r -> 
        new CopyOnWriteArraySet<>());
    set.add(router);
    //路由表已发生变化
    changed = true;
  }
}

说说其他应用场景?

class InitTest{
  boolean inited = false;
  // 同步方法
  synchronized void init(){
    // 后续执行init()方法的线程就不会再执行doInit()
    if(inited){
      return;
    }
    //省略doInit的实现
    doInit();
    // 第一次执行完时会将inited设置为true
    inited=true;
  }
}

总结

Balking模式和Guarded Suspension模式从实现上看似乎没有多大的关系,Balking模式只需要用互斥锁就能解决,而Guarded Suspension模式则要用到管程这种高级的并发原语;但是从应用的角度来看,它们解决的都是“线程安全的if”语义,不同之处在于,Guarded Suspension模式会等待if条件为真,而Balking模式不会等待。

Balking模式的经典实现是使用互斥锁,你可以使用Java语言内置synchronized,也可以使用SDK提供Lock;如果你对互斥锁的性能不满意,可以尝试采用volatile。

下面init()方法的本意是:仅需计算一次count的值,采用了Balking模式的volatile实现方式,你觉得这个实现是否有问题?

class Test{
  volatile boolean inited = false;
  int count = 0;
  void init(){
    if(inited){
      return;
    }
    inited = true;
    //计算count的值
    count = calc();
  }
}  
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值