单例模式与多线程

关键

如何使单例模式遇到多线程是安全的。

立即加载/"饿汉模式"

立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法就是直接new实例化。而立即加载在中文语境看来,有"急"的意思,所以也叫"饿汉模式"。

public class MyObject {

    // 立即加载方式==饿汉模式

    /** Field myObject */
    private static MyObject myObject = new MyObject();

    /**
     * Constructs MyObject
     *
     */
    private MyObject() {}

    /**
     * Method getInstance
     *
     *
     * @return
     *
     * 此代码版本为立即加载,此版本代码的缺点是不能有其他实例变量,
     * 因此getinstance()方法没有同步,所以可能出现非线程安全问题。
     */
    public static MyObject getInstance() {
        return myObject;
    }
}


public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}


public class Run {

    /**
     * Method main
     *
     *
     * @param args
     */
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();
    }
}


/*result:
1108767984
1108767984
1108767984
*/

运行结果显示hashCode为同一个值,说明对象时同一个对象,也就实现了立即加载型单例模式。

延迟加载/懒汉模式

延迟加载就是在调用get()方法是实例才被创建,常见的实现办法就是在get()方法中进行new实例化操作。延迟加载又叫懒汉模式。

饿汉模式时会出现多个实例,在多线程环境下这是与单例模式相背离的。

下面的代码是完全错误的,不能实现保持单例的状态。

public class MyObject {

    /** Field myObject */
    private static MyObject myObject;

    /**
     * Constructs MyObject
     *
     */
    private MyObject() {}

    /**
     * Method getInstance
     *
     *
     * @return
     */
    public static MyObject getInstance() {
        try {
            if (myObject != null) {}
            else {

                // 模仿在创建对象时的一些准备工作
                Thread.sleep(3000);
                myObject = new MyObject();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return myObject;
    }
}


public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}


public class Run {

    /**
     * Method main
     *
     *
     * @param args
     */
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();
    }
}


/*result:
1127673581
1516995504
1905381423
*/

此实验会出现取出多个实例的情况,这就是错误的单例模式。

延迟加载的解决方案。

添加sybchronized关键字 ==pass==

在实例化方法中添加synchronized关键字,即可实现得到同一实例,但此方法运行效率非常低,是同步运行的。

public static synchronized MyObject getInstance() {
        try {
            if (myObject != null) { }
            else {

                // 模仿在创建对象时的一些准备工作
                Thread.sleep(3000);
                myObject = new MyObject();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return myObject;
    }
    
    
/*result:
1127673581
1127673581
1127673581
*/    

使用同步代码块 ==pass==

此方法有所改进,但效率依旧比较低。

public static MyObject getInstance() {
        try {
            synchronized (MyObject.class) {
                if (myObject != null) {}
                else {

                    // 模仿在创建对象时的一些准备工作
                    Thread.sleep(3000);
                    myObject = new MyObject();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return myObject;
    }
    
/*result:
853747565
853747565
853747565
*/

针对某些重点代码单独同步 ==pass==

此方法运行效率较高。但多线程情况下无法实现只取一个实例对象。

public static MyObject getInstance() {
        try {
            if (myObject != null) { }
            else {

                // 模仿在创建对象时的一些准备工作
                Thread.sleep(3000);

                synchronized (MyObject.class) {
                    myObject = new MyObject();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return myObject;
    }
    
/*result:
1127673581
936272565
726367991
*/

DCL双检查锁机制。

最后步骤中使用DCL双检查锁机制来实现多线程环境下延迟加载的单例模式,正确且效率高。

public static MyObject getInstance() {
        try {
            if (myObject != null) { }
            else {

                // 模仿在创建对象时的一些准备工作
                Thread.sleep(3000);
                synchronized (MyObject.class) {
                    if (myObject == null) {
                        myObject = new MyObject();
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return myObject;
    }
    
/*result:
1108767984
1108767984
1108767984
*/
    

使用静态内置类实现单例模式

DCL可以解决多线程单例模式的非线程安全问题,但是也有其他方法也能实现。

public class MyObject {

    /**
     * Constructs MyObject
     *
     */
    private MyObject() {}

    /**
     * Method getInstance
     *
     *
     * @return
     */
    public static MyObject getInstance() {
        return MyObjectHandler.myObject;
    }

    /**
     * Class MyObjectHandler
     *
     *
     * @version        1.0, 18/05/05
     * @author         tz
     */
    private static class MyObjectHandler {

        /** Field myObject */
        private static MyObject myObject = new MyObject();
    }
}


public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}

public class Run {

    /**
     * Method main
     *
     *
     * @param args
     */
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();
    }
}

/*result:
264320363
264320363
264320363
*/

上面代码证明使用内置内部类的方法也可以实现懒汉模式下多线程的单实例模式。

序列化和反序列化的单例模式实现

静态内置类可以达到线程安全的目的,但是如果遇到序列化对象时,使用默认的方法运行得到的结果还是多例的。

public class MyObject implements Serializable {

    /** Field serialVersionUID */
    private static final long serialVersionUID = 888L;

    /**
     * Constructs MyObject
     *
     */
    private MyObject() {}

    /**
     * Method getInstance
     *
     *
     * @return
     */
    public static MyObject getInstance() {
        return MyObjectHandler.myObject;
    }

    /**
     * Class MyObjectHandler
     *
     *
     * @version        Enter version here..., 18/05/05
     * @author         Enter your name here...    
     */
    private static class MyObjectHandler {

        /** Field myObject */
        private static final MyObject myObject = new MyObject();
    }
}


public class SaveAndRead {

    /**
     * Method main
     *
     *
     * @param args
     */
    public static void main(String[] args) {
        try {
            MyObject           myObject = MyObject.getInstance();
            FileOutputStream   fosRef   = new FileOutputStream(new File("myObjectFile.txt"));
            ObjectOutputStream oosRef   = new ObjectOutputStream(fosRef);

            oosRef.writeObject(myObject);
            oosRef.close();
            fosRef.close();
            System.out.println(myObject.hashCode());
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            FileInputStream   fisRef   = new FileInputStream(new File("myObjectFile.txt"));
            ObjectInputStream iosRef   = new ObjectInputStream(fisRef);
            MyObject          myObject = (MyObject) iosRef.readObject();

            iosRef.close();
            fisRef.close();
            System.out.println(myObject.hashCode());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}


/*result:
1836019240
2093631819
*/

上述程序反应了序列化对象的情况下,内置内部类不能实现单例模式,解决方法就是使用反序列化。

ublic class MyObject implements Serializable {

    /** Field serialVersionUID */
    private static final long serialVersionUID = 888L;

    /**
     * Constructs MyObject
     *
     */
    private MyObject() {}

    /**
     * Method getInstance
     *
     *
     * @return
     */
    public static MyObject getInstance() {
        return MyObjectHandler.myObject;
    }

    protected  Object readResolve() {
        System.out.println("调用了readResolve方法!");
        return MyObjectHandler.myObject;
    }

    /**
     * Class MyObjectHandler
     *
     *
     * @version        Enter version here..., 18/05/05
     * @author         Enter your name here...
     */
    private static class MyObjectHandler {

        /** Field myObject */
        private static final MyObject myObject = new MyObject();
    }
}


/*result:
1836019240
调用了readResolve方法!
1836019240
*/

反序列化后即实现单例。

使用static代码块实现单例模式

静态代码块中的代码在使用类的时候就已经执行了,所以可以应用静态代码块的这个特性来实现单例设计模式

public class MyObject {

    /** Field instance */
    private static MyObject instance = null;

    static {
        instance = new MyObject();
    }

    /**
     * Constructs MyObject
     *
     */
    private MyObject() { }

    /**
     * Method getInstance
     *
     *
     * @return
     */
    public static MyObject getInstance() {
        return instance;
    }
}


public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(MyObject.getInstance().hashCode());
        }
    }
}


public class Run {

    /**
     * Method main
     *
     *
     * @param args
     */
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        t1.start();
        t2.start();
        t3.start();
    }
}


/*result:
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
*/

使用enum枚举数据类型实现单例模式

枚举enum和静态代码块的特性相似,在使用枚举类时构造方法会自动被调用,也可以应用其这个特性实现单例设计模式。

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

推荐阅读更多精彩内容

  • 本文的知识点非常重要,通过单例模式与多线程技术相结合,在这个过程中能发现很多以前未考虑过的情况,一些不良的程序设计...
    Java_Explorer阅读 676评论 1 2
  • 单例模式简介 想要唯一的创建一个对象,我们不通过约定,而是通过制定约束的方式去限制。虽然我们可以建立一个全局变量。...
    wa1terwen阅读 753评论 2 5
  • 单例模式(SingletonPattern)一般被认为是最简单、最易理解的设计模式,也因为它的简洁易懂,是项目中最...
    成热了阅读 4,252评论 4 34
  • 1.立即加载/饿汉模式 package singleton;public class MyThread exten...
    有奶喝先森阅读 265评论 0 1
  • 向前走吧,做下一个自己。 人生向前走,暂时要做加法,慢慢的做减法。 否定我的人通常没好下场 你愿意帮助那些富有远见...
    导演张升志阅读 199评论 0 0