1.概念
"双检锁"机制,通常指的是双重检查锁定(Double-Checked Locking),是一种用于多线程编程的同步机制,旨在减少锁竞争的开销,提高性能。这通常用于延迟初始化单例模式。双重检查锁定在Java中通常与volatile关键字结合使用以确保线程之间的可见性。
2.应用场景
2.1 场景1
以下是一个示例的双重检查锁定模式,用于延迟初始化单例:
public class Singleton {
private volatile static Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
-
首先,我们声明了一个volatile修饰的静态变量instance,这是为了确保在多线程环境下,对instance的读取和写入操作都是原子性的。volatile关键字还保证了当一个线程修改instance的值后,其他线程能立即看到这个变化。
-
getInstance方法首先检查instance是否为null,如果是null,才会进入同步块。
-
进入同步块后,再次检查instance是否为null,这是为了避免多个线程在同步块外等待,然后都在同步块内创建实例。只有一个线程能够在同步块内创建实例,其他线程在第二次检查时会发现instance已经不为null,于是它们会直接返回已创建的实例。
这种方式通过双重检查,能够在大多数情况下避免进入同步块,提高了性能。需要注意的是,此方式要求instance变量必须用volatile关键字修饰,以确保线程之间的可见性
这里的volatile关键字确保了instance的可见性,避免了线程之间的缓存不一致问题。在第一次检查之后,如果多个线程进入同步块,只有第一个线程会创建实例,其他线程会等待,从而减少了性能开销。
双检锁机制在单例模式中非常常见,用于确保单例对象的懒加载和线程安全性。
2.2 场景2
多线程环境中延迟初始化一个复杂对象。假如:有一个数据库连接池,只有在需要时才初始化连接池。
public class ConnectionPool {
private volatile static ConnectionPool instance;
private ConnectionPool() {
// 初始化连接池的逻辑
}
public static ConnectionPool getInstance() {
if (instance == null) { // 第一次检查
synchronized (ConnectionPool.class) {
if (instance == null) { // 第二次检查
instance = new ConnectionPool();
}
}
}
return instance;
}
// 其他数据库连接池相关方法
}
在上面的示例中,ConnectionPool类使用双检加锁机制延迟初始化连接池实例。只有在第一次调用getInstance方法时,才会进行实际的初始化。这有助于减少资源的浪费,并提高性能。
2.3 场景3
当需要延迟初始化的对象非常复杂或成本较高时,双检加锁机制也可以派上用场。下面是一个示例,演示如何使用双检加锁机制来延迟初始化一个高成本的对象,比如一个巨大的内存缓存。
public class ComplexObjectCache {
private volatile ComplexObject cachedObject;
public ComplexObject getComplexObject() {
if (cachedObject == null) { // 第一次检查
synchronized (this) {
if (cachedObject == null) { // 第二次检查
// 初始化ComplexObject的逻辑,可能会耗费大量时间和资源
cachedObject = new ComplexObject();
}
}
}
return cachedObject;
}
// 其他操作缓存对象的方法
}
在这个示例中,ComplexObjectCache类用于缓存一个ComplexObject对象,该对象的初始化成本很高。使用双检加锁机制,只有在第一次获取ComplexObject对象时,才会进行实际的初始化,之后的获取都会直接返回已缓存的对象。
2.4 场景4
假设你有一个在线电子商务平台,用户购买商品后,你需要在数据库中记录订单信息,同时将订单信息存储在Redis缓存中以提供快速的查询。当用户提交订单时,你可以采用以下方式:
public class OrderService {
private volatile boolean initialized = false;
private OrderDatabase orderDatabase;
private OrderCache orderCache;
public void placeOrder(Order order) {
if (!initialized) { // 第一次检查
synchronized (this) {
if (!initialized) { // 第二次检查
orderDatabase = new OrderDatabase();
orderCache = new OrderCache();
initialized = true;
}
}
}
// 将订单保存到数据库
orderDatabase.saveOrder(order);
// 将订单保存到缓存
orderCache.saveOrder(order);
}
// 其他方法
}
在这个示例中,OrderService用于处理用户下单操作。在第一次处理订单前,双检加锁机制用于确保orderDatabase和orderCache的初始化只发生一次。之后的订单下单操作会直接使用已初始化的数据库和缓存对象来保持一致性。这种方式可避免多次初始化数据库和缓存对象,提高性能并减少不一致性的风险。
3.总结
尽管双重检查锁定在性能上有一定的优势,但也需要小心使用,因为它在一些早期版本的Java中存在一些问题,如指令重排问题。因此,在现代Java版本中,可以考虑使用其他线程安全的延迟初始化方法,如静态内部类或枚举类型单例,以避免潜在的问题。