相信看过之前几篇自旋锁实现的同学对设计一个自旋锁会有一定的感觉,有几个实现的要点很实用:
1. 使用AtomicBoolean原子变量的getAndSet(true)方法来实现并发情况下,找到第一个成功执行方法的线程。这个技巧经常使用,在并发编程中经常会遇到这种需求
2.经常会使用volatile boolean类型的变量在多个线程之间同步状态,需要注意的是,对volatile变量的修改只具有可见性,不具有原子性(比如++操作)。
3. 使用一个AtomicReference原子变量的getAndSet方法来创建一个虚拟的链表结构,原理也是CAS操作,在队列锁中经常使用
这篇结合一个实例来说说如何灵活地利用这些技巧实现无锁的能力。
在实际开发中会经常遇到“只创建一次”的场景,这里说的“只创建一次”不是说单实例模式。单实例模式也是解决只创建一次的问题,关于单实例模式有很多技巧来实现高并发情况下只创建一个对象的问题,这里不讨论。 单实例模式解决的问题是所有线程都公用一个静态的引用,可以用volatile变量来标示这个静态引用,从而在所有线程之间共享这个静态引用的状态是否已经改变。
这里说的“只创建一次”的场景是这个需要创建一次的对象不是直接被全局的引用所引用,而是间接地被引用。经常有这种情况,全局维护一个并发的ConcurrentMap, Map的每个Key对应一个对象,这个对象需要只创建一次。这个场景和单实例模式不一样,不能用volatile变量来维护这个局部对象的引用。
举个实际的例子,下面这段代码来自阿里的Dubbo框架的服务注册类AbstractRegistryFactory。 它维护了一个全局的ConcurrentHashMap来保存服务注册中心字符串和服务注册中心对象的映射,不同的key对应不同的Registry对象。
为了保证每个key对应的Registry对象只创建一次,Dubbo的实现是使用了可重入锁来保证串行性,每次都要先获取锁,然后get(key)来获得Registry对象,如果为空,那么就执行createRegistry()方法来创建唯一的Registry对象。
可重入锁和synchronzied一样,会阻塞线程,在高并发执行这个方法的时候,所有线程排队执行,性能肯定很差。
// 注册中心集合 Map<RegistryAddress, Registry>
private static final Map<String, Registry> REGISTRIES = new ConcurrentHashMap<String, Registry>();
public Registry getRegistry(URL url) {
url = url.setPath(RegistryService.class.getName())
.addParameter(Constants.INTERFACE_KEY, RegistryService.class.getName())
.removeParameters(Constants.EXPORT_KEY, Constants.REFER_KEY);
String key = url.toServiceString();
// 锁定注册中心获取过程,保证注册中心单一实例
LOCK.lock();
try {
Registry registry = REGISTRIES.get(key);
if (registry != null) {
return registry;
}
registry = createR