图书馆网站建设研究,怎么上传wordpress,自助服务器网站建设,身边的网络营销案例上一篇文章中#xff0c;我们提到可以用“多线程版本的 if”来理解 Guarded Suspension 模式#xff0c;不同于单线程中的 if#xff0c;这个“多线程版本的 if”是需要等待的#xff0c;而且还很执着#xff0c;必须要等到条件为真。但很显然这个世界#xff0c;不是所有…上一篇文章中我们提到可以用“多线程版本的 if”来理解 Guarded Suspension 模式不同于单线程中的 if这个“多线程版本的 if”是需要等待的而且还很执着必须要等到条件为真。但很显然这个世界不是所有场景都需要这么执着有时候我们还需要快速放弃。
需要快速放弃的一个最常见的例子是各种编辑器提供的自动保存功能。自动保存功能的实现逻辑一般都是隔一定时间自动执行存盘操作存盘操作的前提是文件做过修改如果文件没有执行过修改操作就需要快速放弃存盘操作。下面的示例代码将自动保存功能代码化了很显然 AutoSaveEditor 这个类不是线程安全的因为对共享变量 changed 的读写没有使用同步那如何保证 AutoSaveEditor 的线程安全性呢
class AutoSaveEditor{//文件是否被修改过boolean changedfalse;//定时任务线程池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 模式的经典实现
Balking 模式本质上是一种规范化地解决“多线程版本的 if”的方案对于上面自动保存的例子使用 Balking 模式规范化之后的写法如下所示你会发现仅仅是将 edit() 方法中对共享变量 changed 的赋值操作抽取到了 change() 中这样的好处是将并发处理逻辑和业务逻辑分开。
boolean changedfalse;
//自动存盘操作
void autoSave(){synchronized(this){if (!changed) {return;}changed false;}//执行存盘操作//省略且实现this.execSave();
}
//编辑操作
void edit(){//省略编辑逻辑......change();
}
//改变状态
void change(){synchronized(this){changed true;}
}用 volatile 实现 Balking 模式
前面我们用 synchronized 实现了 Balking 模式这种实现方式最为稳妥建议你实际工作中也使用这个方案。不过在某些特定场景下也可以使用 volatile 来实现但使用 volatile 的前提是对原子性没有要求。 在《Copy-on-Write 模式》中有一个 RPC 框架路由表的案例在 RPC 框架中本地路由表是要和注册中心进行信息同步的应用启动的时候会将应用依赖服务的路由表从注册中心同步到本地路由表中如果应用重启的时候注册中心宕机那么会导致该应用依赖的服务均不可用因为找不到依赖服务的路由表。为了防止这种极端情况出现RPC 框架可以将本地路由表自动保存到本地文件中如果重启的时候注册中心宕机那么就从本地文件中恢复重启前的路由表。这其实也是一种降级的方案。 自动保存路由表和前面介绍的编辑器自动保存原理是一样的也可以用 Balking 模式实现不过我们这里采用 volatile 来实现实现的代码如下所示。之所以可以采用 volatile 来实现是因为对共享变量 changed 和 rt 的写操作不存在原子性的要求而且采用 scheduleWithFixedDelay() 这种调度方式能保证同一时刻只有一个线程执行 autoSave() 方法。
//路由表信息
public class RouterTable {//Key:接口名//Value:路由集合ConcurrentHashMapString, CopyOnWriteArraySetRouter rt new ConcurrentHashMap(); //路由表是否发生变化volatile boolean changed;//将路由表写入本地文件的线程池ScheduledExecutorService sesExecutors.newSingleThreadScheduledExecutor();//启动定时任务//将变更后的路由表写入本地文件public void startLocalSaver(){ses.scheduleWithFixedDelay(()-{autoSave();}, 1, 1, MINUTES);}//保存路由表到本地文件void autoSave() {if (!changed) {return;}changed false;//将路由表写入本地文件//省略其方法实现this.save2Local();}//删除路由public void remove(Router router) {SetRouter setrt.get(router.iface);if (set ! null) {set.remove(router);//路由表已发生变化changed true;}}//增加路由public void add(Router router) {SetRouter set rt.computeIfAbsent(route.iface, r - new CopyOnWriteArraySet());set.add(router);//路由表已发生变化changed true;}
}Balking 模式有一个非常典型的应用场景就是单次初始化下面的示例代码是它的实现。这个实现方案中我们将 init() 声明为一个同步方法这样同一个时刻就只有一个线程能够执行 init() 方法init() 方法在第一次执行完时会将 inited 设置为 true这样后续执行 init() 方法的线程就不会再执行 doInit() 了。
class InitTest{boolean inited false;synchronized void init(){if(inited){return;}//省略doInit的实现doInit();initedtrue;}
}线程安全的单例模式本质上其实也是单次初始化所以可以用 Balking 模式来实现线程安全的单例模式下面的示例代码是其实现。这个实现虽然功能上没有问题但是性能却很差因为互斥锁 synchronized 将 getInstance() 方法串行化了那有没有办法可以优化一下它的性能呢
class Singleton{private static Singleton singleton;//构造方法私有化 private Singleton() {}//获取实例单例public synchronized static Singleton getInstance(){if(singleton null){singletonnew Singleton();}return singleton;}
}办法当然是有的那就是经典的双重检查Double Check方案下面的示例代码是其详细实现。在双重检查方案中一旦 Singleton 对象被成功创建之后就不会执行 synchronized(Singleton.class){}相关的代码也就是说此时 getInstance() 方法的执行路径是无锁的从而解决了性能问题。不过需要你注意的是这个方案中使用了 volatile 来禁止编译优化其原因你可以参考《01 | 可见性、原子性和有序性问题并发编程 Bug 的源头》中相关的内容。至于获取锁后的二次检查则是出于对安全性负责。
class Singleton{private static volatile Singleton singleton;//构造方法私有化 private Singleton() {}//获取实例单例public static Singleton getInstance() {//第一次检查if(singletonnull){synchronize{Singleton.class){//获取锁后二次检查if(singletonnull){singletonnew Singleton();}}}return singleton;}
}总结
Balking 模式和 Guarded Suspension 模式从实现上看似乎没有多大的关系Balking 模式只需要用互斥锁就能解决而 Guarded Suspension 模式则要用到管程这种高级的并发原语但是从应用的角度来看它们解决的都是“线程安全的 if”语义不同之处在于Guarded Suspension 模式会等待 if 条件为真而 Balking 模式不会等待。 Balking 模式的经典实现是使用互斥锁你可以使用 Java 语言内置 synchronized也可以使用 SDK 提供 Lock如果你对互斥锁的性能不满意可以尝试采用 volatile 方案不过使用 volatile 方案需要你更加谨慎。 当然你也可以尝试使用双重检查方案来优化性能双重检查中的第一次检查完全是出于对性能的考量避免执行加锁操作因为加锁操作很耗时。而加锁之后的二次检查则是出于对安全性负责。双重检查方案在优化加锁性能方面经常用到例如《17 | ReadWriteLock如何快速实现一个完备的缓存》中实现缓存按需加载功能时也用到了双重检查方案。