likes
comments
collection
share

你真的懂Java延迟初始化吗?

作者站长头像
站长
· 阅读数 63

引言

有时候,我们需要推迟一些高开销的对象初始化操作,并且只有当使用这些对象时才进行初始化。这让我立马反应到DCL的写法,但很快就被打脸,《Java并发编程实战》中表示不太推荐使用DCL。本文记录安全初始化的几个方式。

你真的懂Java延迟初始化吗?

附上仓库链接:github.com/WeiXiao-Hyy…

线程安全的延迟初始化

public class SafeLazyInitialization {
    private static Resource resource;
    
    public synchronized static Resource getInstance() {
        if (resource == null) {
            resource = new Resource();
        }
        return resource;
    }
}

getInstance的代码路径很短,因此如果getInstance没有被多个线程频繁调用,那么SafeLazyInitialization上不会存在激烈的竞争,从而能提供令人满意的性能。

静态初始化的对象都不需要显式的同步

静态代码块和静态变量初始化

静态代码块和静态变量初始化在类加载后并且被线程使用之前。并且JVM将在初始化期间获得一个锁,这个锁用于确保类的初始化在多线程环境下是安全的。每个线程至少获取一次锁,可能有多个线程同时尝试使用同一个类,而JVM需要确保类的初始化只被执行一次,在初始化完成之前,其他线程需要等待。

因此无论是在被构造期间还是被引用,静态初始化的对象都不需要显式的同步。

提前初始化

通过使用提前初始化,避免了在每次调用SafeLazyInitialization中的getInstance时所产生的同步开销。

public class EagerInitialzation {
    private static Resource resource = new Resource();
    
    public static Resource getResource() {
        return resource;
    }
}

延长初始化占位类模式

首先明确静态内部类的加载过程

静态内部类的加载不需要依附外部类,在使用时才会加载。同时在加载静态内部类的过程中也会加载外部类。

通过以上理论可以形成一种延迟初始化技术,从而在常见的代码路径并不需要同步。

public class ResourceFactory {
    private static class ResourceHolder {
        private static Resource resource = new Resource();
    }
    
    public static Resource getResource() {
        return ResourceHolder.resource;
    }
}

双重检查加锁 (DCL)

相信每个面过试的人很快就能写出以下代码。

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

然而上述代码是有问题的,理由是new Singleton()并不是原子操作,有可能会被编译器进行指令重排操作。

memory = allocate(); //1:分配对象的内存空间 
ctorInstance(memory);//2:初始化对象 
instance = memory;   //3:设置instance指向刚分配的内存地址

上面操作2依赖于操作1,但操作3并不依赖操作2,所有可能出现如下执行顺序:

memory = allocate();   //1:分配对象的内存空间 
instance = memory;     //3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory);  //2:初始化对象

导致程序直接使用这个未初始化的值时,便会出现错误。为了解决上述问题应该在写操作前后都会插入内存屏障,避免指令重排序, 在instance上添加volatile关键字即可。

DCL举例

Nacos的双重检查锁

private final Map<String, ConcurrentHashSet<EventListener>> listenerMap = new ConcurrentHashMap<String, ConcurrentHashSet<EventListener>>();

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);
    ConcurrentHashSet<EventListener> eventListeners = listenerMap.get(key);
    if (eventListeners == null) {
        synchronized (lock) {
            eventListeners = listenerMap.get(key);
            if (eventListeners == null) {
                eventListeners = new ConcurrentHashSet<EventListener>();
                listenerMap.put(key, eventListeners);
            }
        }
    }
    eventListeners.add(listener);
}

当然上述主要是为了防止"先查询后执行"这种方式导致的并发错误。也可以使用map.putIfAbsent()来代替双重检查锁的写法。

private final Map<String, ConcurrentHashSet<EventListener>> listenerMap = new ConcurrentHashMap<String, ConcurrentHashSet<EventListener>>();

public void registerListener(String groupName, String serviceName, String clusters, EventListener listener) {
    String key = ServiceInfo.getKey(NamingUtils.getGroupedName(serviceName, groupName), clusters);
    
    ConcurrentHashSet<EventListener> eventListeners = listenerMap.get(key);
    if (eventListeners == null) {
        ConcurrentHashSet<EventListener> newEventListeners = new ConcurrentHashSet<>();
        eventListeners = listenerMap.putIfAbsent(key, newEventListeners);
        if (eventListeners == null) {
            eventListeners = newEventListeners;
        }
    }
    eventListeners.add(listener);
}

享元模式思考

参考资料

转载自:https://juejin.cn/post/7377191678862508069
评论
请登录