Java中七种单利模式与原子性和指令重排

方式一

public class Single {
    private Single(){}

    private static Single single = null;

    public static Single getInstance(){
        if(single==null){
            single =new Single();
        }
        return single;
    }
}
  • 优点:简单明了
  • 缺点:线程不安全,在多线程中会重复创建对象
  • 适用场景 单线程情况下

既然方式一无法保证在多线程的单一性,我们可以通过加锁的方式经行改造一下

方式二

public class Single {
    private Single(){}

    private static Single single = null;

    public static Single getInstance(){
        synchronized (Single.class){
            if(single==null){
                single =new Single();
            }
        }
        return single;
    }
}

通过加一个同步锁,就保证了多线程的唯一性

  • 缺点:锁本身是一种非常耗时的操作,虽然保证了唯一性,但是在多个线程的情况下,无疑会浪费资源。
  • 适用场景:线程较少的地方比较适用

3、方式三 双重检查锁

public class Single {
    private Single(){}

    private static Single single = null;

    public static Single getInstance(){
        if(single == null){//1
            synchronized (Single.class){
                if(single==null){
                    single =new Single();//2
                }
            }
        }
        return single;
    }
}
  • 优点:只有实例为null的情况下,才执行枷锁操作,避免无意义的同步锁,相比于方式2 方式3 双重判空时间效率更高。
  • 缺点:两层if判断更容易出错,代码相对较多,同时虽然一定程度上解决了多线程的问题,但是还有一个重大的隐患,会出现重排序,导致程序获取到的实例为空。

这里需要具体说一下方式3
这里需要引入两个概念:

  • 原子操作

  • 指令重排

原子操作

原子操作就是不可分割的操作,不能被线程打断的操作就是原子操作
比如说,赋值

 private int a = 0;//非原子操作
 private void setA(){
    a = 10;//原子操作
 } 

第一行,int a =0; 这个不是原子操作,是因为他至少有两个操作:

  • 一是声明变量a,让a在内存中开辟一个空间。
  • 二是给a赋值
    正是因为有这两个操作,所以在多线程中,就会充满了不确定性,因为他有一个中间状态,变量a已经声明,但是没有赋值的状态,这样你无法确定这块内存空间的a 就是上面线程内存空间的a。
    而第三行,只有一个操作,给a赋值,对于这个操作,你并没有一个中间状态,要么成功变成10,要么不成功还是0,即使是在多线程并发的情况下也是如此。
    所以原子操作,就是指不能被线程打断的不可分割的操作。
指令重排

指令队列在CPU执行时不是串行的, 当某条指令执行时消耗较多时间时, 并不会一直等待, 而是开启下一个指令去执行,当然是有条件的, 即两条指令不存在相关性,如下例子。

int a =0;//指令1
int b =0;//指令2
int c = a +b;//指令3

这段代码在单线程情况下指令顺序可能是

  • 指令1—>指令2—>指令3
  • 指令2—>指令1—>指令3
    因为指令1和指令2并没有相关性,所以先执行指令1和指令2对代码的运行结果完全没区别,但是指令3则不是,必须放在指令1和指令2之后。
    在单线程情况下,指令重排不会对我们的代码逻辑造成影响,但是多线程情况下,就不一样了,代码如下
    //这里必须要添加volatile
    private volatile boolean initialized = false;
    private static String result;
        //线程A 往文件夹里写完内容 然后线程B去读取
        new Thread(new Runnable() {
            @Override
            public void run() {
                writeAsset("test.json");//指令1
                initialized =  true;//指令2
            }
        }).start();

        //线程B
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (!initialized){
                    sleep();
                }
                //从文件夹里读取
               result =  readAsset("test.json");
            }
        }).start();

这里面的代码,就可能因为指令重排的问题,导致下面读取的result为null,A线程可能会出现指令2 initialized = true先于指令1 writeAsset执行。这就会导致线程B立即执行readAsset,所以需要把变量initialized 添加volatile关键字,阻止写操作指令重排,保证有序性。

回到刚刚的问题上来,为什么这个双重检查锁会出现问题?

single =new Single();//2

首先这句代码并不是一个原子操作,它的内部实现可以简化成如下代码

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

上面的代码,2依赖于操作1,但是操作3并不依赖于操作2,所以可以经行指令重排,变成

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

在单线程中,这样的重排肯定是没有任何问题的,但是在多线程中,这个时候一个线程在判断

if(single == null)//1

刚好碰到上一个线程走到,指令重排之后的instance =memory; 这个时候Single有了内存地址,引用不为null,但是却没有初始化对象,这个时候就出现问题了。

方式四 终极双重检查锁

public class Single {
    private Single(){}
    private volatile  static Single single = null;
    public static Single getInstance(){
        if(single == null){//1
            synchronized (Single.class){
                if(single==null){
                    single =new Single();//2
                }
            }
        }
        return single;
    }
}
  • 优点:解决了方式三中潜在的隐患,适用于各种环境
  • 缺点:代码少为有点多,双层if+synchronized +volatile 写起来麻烦

方式五 静态属性单利

 private Single(){}
    
    private static Single single = new Single();
    
    public static Single getInstance(){
        return single;
    }
  • 优点:线程安全,作为静态属性,在内存中只有一份(在Java中,一个类在一个ClassLoader中只会被初始化一次,这是JVM本身的特点,而静态资源会随着类的加载而加载)
  • 缺点:不能控制加载时机,静态属性会随着类的加载而加载,并不是我们调用 getInstance()方法才去创建对象,所以资源利用率不高。
    适用场景:内存消耗小的对象、程序一运行就需要加载到内存的对象。

当然方式五也可以改为静态代码块来实现

 private Single(){}
    static {
        Single single = new Single();
    }
    public static Single getInstance(){
        return single;
    }

方式六 静态内部类实现单利

方式五实现虽然优雅,但是有一些缺点,所以我们需要再次改进一下,一样是通过Java的特性来实现

加载外部类或者实例化外部类,都不会加载内部类 或静态内部类

 private Single(){}

   private static class SingleClass{
        private Single sInstance = new Single();
    }
    public static Single getInstance(){
        return SingleClass.sInstance ;
    }

既保证了唯一性,又能控制加载时机。
强烈推荐

方式七 枚举单利

public class EnumSingleton {
    public static EnumSingleton getInstance() {
        return Single.INSTANCE.getInstance();
    }

    private enum Single{
        INSTANCE;
        private EnumSingleton singleton;
        Single(){
            singleton = new EnumSingleton();
        }
        private EnumSingleton getInstance() {
            return singleton;
        }
    }
}

单例的枚举实现在《Effective Java》中有提到,因为其功能完整、使用简洁、无偿地提供了序列化机制、在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化等优点,单元素的枚举类型被作者认为是实现Singleton的最佳方法。

关于枚举序列化单利的问题

Java规范中规定,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。
也就是说,以下面枚举为例,序列化的时候只将 DATASOURCE 这个名称输出,反序列化的时候再通过这个名称,查找对于的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

private enum Single{
INSTANCE;
}

由此可知,枚举天生保证序列化单例。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容