美文网首页
仅且仅创建一次对象

仅且仅创建一次对象

作者: 码农戏码 | 来源:发表于2018-07-06 10:29 被阅读0次

    此篇算是对《voliatile,synchronized,cas》理论的一种实践

    全局引用场景

    单例模式

    不用讲,这是首先想到的方式。

    饿汉式 static final field

    public class Singleton{
        //类加载时就初始化
        private static final Singleton instance = new Singleton();
        
        private Singleton(){}
        public static Singleton getInstance(){
            return instance;
        }
    }
    

    这是最简单又安全的方式。但也有缺点:

    1. 它不是一种懒加载模式(lazy initialization)
    2. 一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

    静态内部类

    public class Singleton {  
        private static class SingletonHolder {  
            private static final Singleton INSTANCE = new Singleton();  
        }  
        private Singleton (){}  
        public static final Singleton getInstance() {  
            return SingletonHolder.INSTANCE; 
        }  
    }
    

    这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本

    双重检验锁

    public class Singleton {
        private volatile static Singleton instance; //声明成 volatile
        private Singleton (){}
        public static Singleton getSingleton() {
            if (instance == null) {                         
                synchronized (Singleton.class) {
                    if (instance == null) {       
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
       
    }
    

    这个写法得注意到volatile

    主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

    1. 给 instance 分配内存
    2. 调用 Singleton 的构造函数来初始化成员变量
    3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
      但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

    声明为volatile,使用其一个特性:禁止指令重排序优化。

    也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

    volatile的更多特性,可以看一下上篇文章《voliatile,synchronized,cas》

    间接被引用情景

    需要创建一次的对象不是直接被全局的引用所引用,而是间接地被引用。经常有这种情况,全局维护一个并发的ConcurrentMap, Map的每个Key对应一个对象,这个对象需要只创建一次

    CAS

    private final ConcurrentMap<String, InstanceObject> cache
            = new ConcurrentHashMap<>();
    
        public InstanceObject get(String key) {
            InstanceObject single = cache.get(key);
            if (single == null) {
                InstanceObject instanceObject = new InstanceObject(key);
                single = cache.putIfAbsent(key, instanceObject);
                if (single == null) {
                    single = instanceObject;
                }
            }
            return single;
        }
    

    使用这个很可能会产生多个InstanceObject对象,但最终只有一个InstanceObject有用

    但并不没有达到仅创建一个的目标

    如果创建InstanceObject的成本不高,那也不用太讲究

    但一旦是大对象缓存,那么这很可能就是问题了,因为缓存中的对象获取成本一般都比较高,而且通常缓存都会经常失效,那么避免重复创建对象就有价值了

    影子类

    private final ConcurrentMap<String, Future<InstanceObject>> cache1 = new ConcurrentHashMap<>();
    
        public InstanceObject get1(final String key) {
            Future<InstanceObject> future = cache1.get(key);
            if (future == null) {
                Callable<InstanceObject> callable = new Callable() {
                    @Override
                    public InstanceObject call() throws Exception {
                        return new InstanceObject(key);
                    }
                };
                FutureTask<InstanceObject> task = new FutureTask<>(callable);
    
                future = cache1.putIfAbsent(key, task);
                if (future == null) {
                    future = task;
                    task.run();
                }
            }
    
            try {
                return future.get();
            } catch (Exception e) {
                cache.remove(key);
                throw new RuntimeException(e);
            }
        }
    

    这儿使用Future来代替真实的对象,多次创建Future代价比创建缓存大对象小得多

    自旋锁

    觉得Future对象还是重了,那就使用更轻的AtomicBoolean,那其实主要使用的还是volatile的特性

     private final ConcurrentMap<String, AtomicBoolean> spinCache = new ConcurrentHashMap<>();
    
        public InstanceObject getAtomic(final String key)  {
            InstanceObject single = cache.get(key);
            if (single == null) {
                AtomicBoolean newBoolean = new AtomicBoolean(false);
                AtomicBoolean oldBoolean = spinCache.putIfAbsent(key, newBoolean);
                if (oldBoolean == null) {
                    cache.put(key, new InstanceObject(key));
                    newBoolean.set(true);
                } else {
                    //其他线程在自旋状态上自旋,等等被释放
                    while (!oldBoolean.get()) {}
                }
                single = cache.get(key);
            }
            return  single;
        }
    

    总结

    保守写法可以使用synchronized,lock,他们的性能也不低;但为了性能极致,可以使用上面的方式。

    完整的测试代码:https://github.com/zhuxingsheng/javastudy/blob/master/src/main/java/com/jack/createonlyone/CreateOnlyOneMain.java

    相关文章

      网友评论

          本文标题:仅且仅创建一次对象

          本文链接:https://www.haomeiwen.com/subject/fyjvuftx.html