Java/Kotlin 单例模式

设计模式对于编写程序来说十分重要,它是一种编写技巧,也是一种艺术理念。

单例模式

指一个类只有一个实例,且该类能自行创建这个实例的一种模式。
单例模式的 3 个特点:
1.单例类只有一个实例对象;
2.该单例对象必须由单例类自行创建;
3.单例类对外提供一个访问该单例的全局访问点

单例模式的结构与实现

单例模式分为两个部分:
1、单例类:包含一个实例且能自行创建这个实例的类
2、访问类:使用单例的类

单例模式的两种实现形式:
1、懒汉式单例

class LazySingleton {
    private static LazySingleton instance = null;
    private LazySingleton() {}
    public static LazySingleton getInstance() {
        if (null==instance) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

从代码中可以看到,在类加载时,并没有生成单例,静态成员变量赋值为null,在第一次调用getInstance时创建实例。
2、饿汉式单例

class HungrySingleton {
    private static HungrySingleton instance = new HungrySingleton();
    private HungrySingleton() {}
    public static HungrySingleton getInstance() {
        return instance;
    }
}

从代码中可以看到,类创建的同时创建里一个静态对象,这种用法的好处是静态对象创建之后不会再改变,所以它是线程安全的,可以直接用于多线程。
那么懒汉式有没有办法做到线程安全呢?当然可以:

class LazySingleton {
    private static volatile LazySingleton instance = null;
    private LazySingleton() {}
    public static LazySingleton getInstance() {
        if (null==instance) {
            synchronized (LazySingleton.class) {
                if (null==instance) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

上面的代码使用了双重检查加锁来确保线程安全,首先为什么不在getInstance直接加上synchronized关键字呢?
首先确保线程安全是确保instance的初始化线程安全,如果instance已经完成初始化了,之后的getInstance将会直接返回,但是由于synchronized此方法被上锁了,所以对性能产生影响。
接下来分析双重检查加锁,线程1进入getInstance,由于instance为null,线程1进入synchronized块,线程1让出cpu给到线程2,线程2进入getInstance方法,instance为null,尝试获取锁,获取失败进入阻塞,线程2让出cpu,线程1执行,初始化instance,退出synchronized并返回,线程2获取锁之后检查instance是否为null,由于此时instance已经初始化结束,线程2返回。
以为这样就结束了?并没有,由于java内存模型运行无序写入,双重检查加锁并不能保证一定顺利进行。
原因:在初始化阶段,线程1在构造函数执行之前,使得实例成为非null,并让出cpu,线程2检查实例非null直接返回构造结束但只有部分初始化instance引用对象。
所以,相对于通常懒汉式单例,双重检查加锁模式在instance声明上加上了volatile关键字,volatile关键字的一个功能就是禁止指令重排。

案例:

    enum Subject {
        // 班主任,语文老师,数学老师,英语老师
        HEADMASTER,CHINESE,MATH,ENGLISH
    }

    static class Student {
        public Teacher headMaster;
        public Teacher english;
        public Teacher chinese;
        public Teacher math;
        public Student() {}
    }

    static class Teacher {
        private static Teacher instance = null;
        public Subject subject;
        public String name;
        private Teacher() {
        }
        private Teacher(Subject subject, String name) {
            this.subject = subject;
            this.name = name;
        }
        public static Teacher getInstance() {
            if (null==instance) {
                instance = new Teacher(Subject.HEADMASTER,"老谷");
            }
            return instance;
        }
    }

上面新建了两个类和一个枚举,教师类是单例类,而学生类是访问类,接下来看测试代码:

    @Test
    public void main() {
        final int studentNum = 20;

        List<Student> students = new ArrayList<>(studentNum);

        for (int i = 0; i < studentNum; i++) {
            students.add(new Student());
        }

        for (int i = 0; i < studentNum; i++) {
            students.get(i).headMaster = Teacher.getInstance();
        }

        for (int i = 0; i < studentNum; i+=4) {
            System.out.println(String.format("学生编号:%d 的班主任是:%s 存储地址是:%s"
                    ,i
                    ,students.get(i).headMaster.name
                    ,students.get(i).headMaster));
        }
    }

----------------------------------------------------------------------
输出结果:
学生编号:0 的班主任是:老谷 存储地址是:$Teacher@1b9e1916
学生编号:4 的班主任是:老谷 存储地址是:$Teacher@1b9e1916
学生编号:8 的班主任是:老谷 存储地址是:$Teacher@1b9e1916
学生编号:12 的班主任是:老谷 存储地址是:$Teacher@1b9e1916
学生编号:16 的班主任是:老谷 存储地址是:$Teacher@1b9e1916
----------------------------------------------------------------------

从上面的例子可以看到,所有的学生都拥有同一个班主任,老师类不是唯一的,但是对于学生来说,班主任是唯一的,语文老师,数学老师,英语老师也是唯一的,扩展:

    static class Teacher {
        private static Map<Subject,Teacher> instance = new HashMap<>(8);
        public Subject subject;
        public String name;
        private Teacher(Subject subject, String name) {
            this.subject = subject;
            this.name = name;
        }
        public static Teacher getInstance(Subject subject) {
            Teacher teacher = instance.get(subject);
            if (null==teacher) {
                switch (subject) {
                    case HEADMASTER:
                        teacher = new Teacher(subject,"老谷");
                        break;
                    case CHINESE:
                        teacher = new Teacher(subject,"老刘");
                        break;
                    case MATH:
                        teacher = new Teacher(subject,"老李");
                        break;
                    case ENGLISH:
                        teacher = new Teacher(subject,"老张");
                        break;
                    default:
                        throw new IllegalArgumentException("Error subject!");
                }
                instance.put(subject, teacher);
            }
            return teacher;
        }
    }

    @Test
    public void main() {
        final int studentNum = 20;

        List<Student> students = new ArrayList<>(studentNum);

        for (int i = 0; i < studentNum; i++) {
            students.add(new Student());
        }

        for (int i = 0; i < studentNum; i++) {
            students.get(i).headMaster = Teacher.getInstance(Subject.HEADMASTER);
            students.get(i).chinese = Teacher.getInstance(Subject.CHINESE);
            students.get(i).math = Teacher.getInstance(Subject.MATH);
            students.get(i).english = Teacher.getInstance(Subject.ENGLISH);
        }

        for (int i = 0; i < studentNum; i+=10) {
            Student student = students.get(i);
            System.out.println(String.format("学生编号:%d 的班主任是:%s 存储地址是:%s , " +
                            "语文老师是:%s 存储地址是:%s ," +
                            "数学老师是:%s 存储地址是:%s ," +
                            "英语老师是:%s 存储地址是:%s ."
                    ,i
                    ,student.headMaster.name
                    ,student.headMaster
                    ,student.chinese.name
                    ,student.chinese
                    ,student.math.name
                    ,student.math
                    ,student.english.name
                    ,student.english));
        }
    }

----------------------------------------------------------------------
学生编号:0 的班主任是:老谷 存储地址是:$Teacher@3b764bce , 语文老师是:老刘 存储地址是:$Teacher@759ebb3d ,数学老师是:老李 存储地址是:$Teacher@484b61fc ,英语老师是:老张 存储地址是:$Teacher@45fe3ee3 .
学生编号:10 的班主任是:老谷 存储地址是:$Teacher@3b764bce , 语文老师是:老刘 存储地址是:$Teacher@759ebb3d ,数学老师是:老李 存储地址是:$Teacher@484b61fc ,英语老师是:老张 存储地址是:$Teacher@45fe3ee3 .
----------------------------------------------------------------------

为将单例类改为多例类,以静态hashmap保存teacher实例,以subject作为key值,从代码的输出可以看到,每个学生都拥有4个单例对象,分别对应班主任、语文、数学、英语,四位老师。

kotlin实现

饿汉式:

    object Teacher {
        val subject = Subject.HEADMASTER
        val name = "老谷"
    }

懒汉式:

    class Teacher private constructor(val subject: Subject,val name:String) {
        companion object {
            private var instance:Teacher?=null
                get() {
                    if (field==null) {
                        field = Teacher(Subject.HEADMASTER,"老谷")
                    }
                    return field
                }
            fun getSingleton():Teacher {
                return instance!!
            }
        }
    }

线程安全式:

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