专栏首页DDD仅且仅创建一次对象

仅且仅创建一次对象

此篇算是对《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

本文分享自微信公众号 - 码农戏码(coder-game),作者:朱兴生

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-07-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Spark Streamming+Kafka提交offset实现有且仅有一次

    董可伦
  • Clay: 创建和使用深层次对象图

    Clay 是 CodePlex 上的一个开源项目,帮助我们创建轻松创建对象,就 JavaScript 或其它动态语言一样简单。Clay 项目的网址是 http:...

    张善友
  • 仅需60秒,使用k3s创建一个多节点K8S集群!

    最近,我一直在Kubernetes上进行各种测试和部署。因此,我不得不一次又一次创建和销毁Kubernetes集群,有的时候甚至在一个小时内执行好几次。但由于我...

    k3s中文社区
  • kafka怎么保证数据消费一次且仅消费一次?使用消息队列如何保证幂等性?

    精确一次处理语义(exactly onece semantic–EOS),Kafka的EOS主要体现在3个方面:

    一个会写诗的程序员
  • 仅使用CSS,带你创建一个漂亮的动画加载页面

    利用伪元素、关键帧动画,你将具有强大的创造力,本文就是一个例子。本例中,利用两者,就可以构建一个加载动画,无需任何JS代码和图片。

    疯狂的技术宅
  • Java中的lambda每次执行都会创建一个新对象吗

    之前写过一篇文章 Java中的Lambda是如何实现的,该篇文章中讲到,在lambda表达式执行时,jvm会先为该lambda生成一个java类,然后再创建一个...

    KINGYT
  • 如何创建一个“纯净”的对象

    假设 Object 的原型中有一个自定义的 log 属性,我们用字面量语法定义 obj 对象,那么使用 for-in 遍历方法就会遍历到这个 log 对象,为了...

    JS菌
  • 第一章 对象导言 第二章 对象的创建与使用

    1 抽象过程 2 对象接口 3 实现隐藏 4 重用 5 继承:重用接口 6 多态性:互换对象 7 创建销毁对象 8 异常处理:应对错误 9 分析...

    用户1154259
  • 如何在Hybris commerce里创建一个media对象

    Jerry Wang
  • 如何在Hybris commerce里创建一个media对象

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

    Jerry Wang
  • [android] 采用layoutInflater打气筒创建一个view对象

    上一节知道了ListView的工作原理,数据也展示出来了,但是TextView显示的非常难看,如果想美化一下,就先创建好一个布局出来,这个布局采用了两层Line...

    陶士涵
  • Java中如何使某个类的对象唯一性,只能创建一个对象

    private static Student s = new Student();

    用户7886150
  • 深入理解JavaScript面向对象的程序设计(一)——对象的创建

    类似Java等面向对象语言中创建对象的语法,在 JavaScript中可以通过执行 new操作符后跟要创建的对象类型的名称来创建。JavaScript中通过如下...

    CherishTheYouth
  • JVM系列一(Java内存区域和对象创建).

    线程共享,JVM中最大的一块内存,此内存的唯一目的就是存放对象实例,Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”(Garbage Co...

    JMCui
  • 面试官:new Object[5] 一共创建了几个对象?

    Java 数组的本质是一个Java类,它是通过new语句来实例化,但是这个new语句却不会实例化数组中的对象,我们对它的理解有些偏差。

    Java技术栈
  • 指定一个创建对象的方法,如果对象存在则直接返回

    单例模式确保一个类只有一个实例,只能自己的内部实现实例化,当他人再次实例化时返回第一次实例化的对象。

    用户8983410
  • 一篇文章带教会你使用JavaScript 创建对象

    就像许多其他编程语言一样,可以将JavaScript中的对象与现实生活中的对象进行比较。

    前端进阶者
  • 在fragment里面创建一个popwindow对象无法弹出的问题

    一般是个控件,点击之后就弹出 popwindow 但是如果你出现popwindow无法弹出的问题  

    wust小吴
  • python学习笔记(10)python面向对象(一)类的创建实战

    在python中使用class语句来创建一个类,class之后为类的名称并以冒号结尾,例子如下:

    大数据小禅

扫码关注云+社区

领取腾讯云代金券