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

举报
JavaEdge 发表于 2021/06/04 00:01:51 2021/06/04
【摘要】 “多线程版本的if”来理解Guarded Suspension模式,不同于单线程中的if,这个“多线程版本的if”是需要等待的,而且还很执着,必须要等到条件为真。但很显然这个世界,不是所有场景都需要这么执着,有时候我们还需要快速放弃。 需要快速放弃的一个最常见的例子是各种编辑器提供的自动保存功能。自动保存功能的实现逻辑一般都是隔一定时间自动执行存盘操作,存盘操作的前提...

“多线程版本的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;
  }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

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

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

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

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

发现,示例中的共享变量是一个状态变量,业务逻辑依赖于这个状态变量的状态:当状态满足某条件时,执行某个业务逻辑,其本质就是个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;
  }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

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;
  }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

说说其他应用场景?

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

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

总结

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();
  }
}  

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

文章来源: javaedge.blog.csdn.net,作者:JavaEdge.,版权归原作者所有,如需转载,请联系作者。

原文链接:javaedge.blog.csdn.net/article/details/116212252

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。