模板网站建设+百度,开封网站网站建设,太原论坛建站模板,傻瓜使用模板建网站最近在看Nacos的源代码时#xff0c;发现多处都使用了“双重检查锁”的机制#xff0c;算是非常好的实践案例。这篇文章就着案例来分析一下双重检查锁的使用以及优势所在#xff0c;目的就是让你的代码格调更加高一个层次。同时#xff0c;基于单例模式#xff0c;讲解一下… 最近在看Nacos的源代码时发现多处都使用了“双重检查锁”的机制算是非常好的实践案例。这篇文章就着案例来分析一下双重检查锁的使用以及优势所在目的就是让你的代码格调更加高一个层次。同时基于单例模式讲解一下双重检查锁的演变过程。Nacos中的双重检查锁 在Nacos的InstancesChangeNotifier类中有这样一个方法private final MapString, ConcurrentHashSetEventListener listenerMap new ConcurrentHashMapString, ConcurrentHashSetEventListener();private final Object lock new Object();public void registerListener(String groupName, String serviceName, String clusters, EventListener listener) {String key ServiceInfo.getKey(NamingUtils.getGroupedName(serviceName, groupName), clusters);ConcurrentHashSetEventListener eventListeners listenerMap.get(key);if (eventListeners null) {synchronized (lock) {eventListeners listenerMap.get(key);if (eventListeners null) {eventListeners new ConcurrentHashSetEventListener();listenerMap.put(key, eventListeners);}}}eventListeners.add(listener);
}
该方法的主要功能就是对监听器事件进行注册。其中注册的事件都存在成员变量listenerMap当中。listenerMap的数据结构是key为Stringvalue为ConcurrentHashSet的Map。也就是说一个key对应一个集合。针对这种数据结构在多线程的情况下Nacos处理流程如下通过key获取value值判断value是否为null如果value值不为null则直接将值添加到Set当中如果为null就需要创建一个ConcurrentHashSet在多线程时有可能会创建多个因此要使用锁。通过synchronized锁定一个Object对象在锁内再获取一次value值如果依然是null则进行创建。进行后续操作。上述过程在锁定前和锁定之后做了两次判断因此称作”双重检查锁“。使用锁的目的就是避免创建多个ConcurrentHashSet。Nacos中的实例稍微复杂一下下面以单例模式中的双重检查锁的演变过程。未加锁的单例 这里直接演示单例模式的懒汉模式实现public class Singleton {private static Singleton instance;private Singleton() {}public Singleton getInstance() {if (instance null) {instance new Singleton();}return instance;}
}
这是一个最简单的单例模式在单线程下运转良好。但在多线程下会出现明显的问题可能会创建多个实例。以两个线程为例可以看到当两个线程同时执行时是有可能会创建多个实例的这很明显不符合单例的要求。加锁单例 针对上述代码的问题很直观的想到是进行加锁处理实现代码如下public class Singleton {private static Singleton instance;private Singleton() {}public synchronized Singleton getInstance() {if (instance null) {instance new Singleton();}return instance;}
}
与第一个示例唯一的区别是在方法上添加了synchronized关键字。这时当多个线程进入该方法时需要先获得锁才能进行执行。通过在方法上添加synchronized关键字看似完美的解决了多线程的问题但却带了性能问题。我们知道使用锁会导致额外的性能开销对于上面的单例模式只有第一次创建时需要锁防止创建多个实例但查询时是不需要锁的。如果针对方法进行加锁每次查询也要承担加锁的性能损耗。双重检查锁 针对上面的问题就有了双重检查锁示例如下public class Singleton {private static Singleton instance;private Singleton() {}public Singleton getInstance() {if (instance null) {synchronized (Singleton.class) {if (instance null) {instance new Singleton();}}}return instance;}
}
第一将锁的范围缩小的方法内第二锁之前先判断一下是不是null如果不为null说明已经实例化了直接返回没必要进行创建第三如果为null进行加锁然后再次判断是否为null。为什么要再次判断因为一个线程判断为null之后另外一个线程可能已经创建了对象所以在锁定之后需要再次核实一下真的为null则进行对象创建。改进之后既保证了线程的安全性又避免了锁导致的性能损失。问题到此结束了吗并没有继续往下看。JVM的指令重排 在某些JVM当中编译器为了性能问题会进行指令重排。在上述代码中new Singleton()并不是原子操作有可能会被编译器进行重排操作。创建对象可抽象为三步memory allocate(); //1分配对象的内存空间
ctorInstance(memory); //2初始化对象
instance memory; //3设置instance指向刚分配的内存地址
上面操作中操作2依赖于操作1但操作3并不依赖于操作2。因此JVM是可以进行指令重排优化的可能会出现如下的执行顺序memory allocate(); //1分配对象的内存空间
instance memory; //3instance指向刚分配的内存地址此时对象还未初始化
ctorInstance(memory); //2初始化对象
指令重排之后将操作3的赋值操作放在了前面那就会出现一个问题当线程A执行完步骤赋值操作但还未执行对象初始化。此时线程B进来了在第一层判断时发现Instance已经有值了实际上还未初始化直接返回对应的值。那么程序在使用这个未初始化的值时便会出现错误。针对此问题可在instance上添加volatile关键字使得instance在读、写操作前后都会插入内存屏障避免重排序。最终单例模式实现如下public class Singleton {private static volatile Singleton instance;private Singleton() {}public Singleton getInstance() {if (instance null) {synchronized (Singleton.class) {if (instance null) {instance new Singleton();}}}return instance;}
}
至此一个完善的单例模式实现了。此时你是否有一个疑问为什么Nacos中的双重检查锁没有使用volatile关键字呢答案很简单上面单例模式如果出现指令重排会导致单例实例被使用。那么再看Nacos的代码由于创建ConcurrentHashSet并不会影响到查询而真正影响查询的是listenerMap.put方法而ConcurrentHashSet本身是线程安全的。因此也就不会出现线程安全问题不用使用volatile关键字了。小结 阅读源码最有意思的一个地方就是可以看到很多经典知识的实践如果能够深入思考拓展一下会获得意想不到的收获。再回顾一下本文的重点阅读Nacos源码发现双重检查锁的使用未加锁单例模式使用会创建多个对象方法上加锁导致性能下降代码内局部加锁双重判断既满足线程安全又满足性能需求单例模式特例创建对象分多步会出现指令重排现象采用volatile进行避免指令重排最后想学习更多类似干货关注一下吧持续输出。
往期推荐
ReentrantLock 中的 4 个坑synchronized 中的 4 个优化你知道几个synchronized 加锁 this 和 class 的区别