1、单例模式(设计模式笔记)

一、分类

  • 创建型模式:
    单例模式、工厂模式、抽象工厂模式、建造者模式、原形模式

  • 结构型模式:
    适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式

  • 行为型模式:
    模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式

二、单例模式

保证一个类只有一个实例,并且提供一个访问该实例的全局访问点

2.1 常用场景

  • windowsTask Manager(任务管理器)就是很典型的单例模式
  • windows的回收站也是。
  • 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。
  • 网站的计数器
  • 应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加
  • 数据库连接池
  • 操作系统的文件系统
  • application也是单例的典型应用(servlet编程中会涉及到)
  • spring中,每个bean默认就是单例的,这样做的有点是spring容器便于管理
  • servlet编程中,每个servlet也是单例
  • struts1中,控制器对象也是单例

2.2 优点

  • 由于单例模式只是生成了一个实例对象,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过

  • 在应用启动时直接产生一个单例对象,然后永久驻留在内存的方式来解决。

  • 单例模式可以在系统设置全局的访问点,优化共享资源的访问,例如设计一个单例类,负责所有数据表的映射处理。

2.3 常见的五种单例模式实现方式

2.3.1 主要

  • 饿汉式(线程安全,调用效率高,但是,不能延时加载)SingletonDemo01
package cn.itcast.day226.singleton;
//饿汉式
public class SingletonDemo01 {
    
    //提供静态属性,这里就不管后面会不会用到,初始化时就加载一个实例
    private static SingletonDemo01 instance = new SingletonDemo01();
    
    private SingletonDemo01(){}//构造器私有化
    
    //这里不需要使用同步,因为加载类的时候(就是在加载上面的属性时)本身就是线程安全的了
    //同时这里没有同步,当然调用效率就高了
    public static /*synchronized*/ SingletonDemo01 getInstance(){
        return instance;
    }
}
  • 懒汉式(线程安全,效率不高,但是可以延时加载)SingletonDemo02
package cn.itcast.day226.singleton;
//懒汉式
/*
 * 用的时候才会加载,资源利用率提高了,但每次调用都需要同步,调用效率就低了
 * */
public class SingletonDemo02 {
    private static SingletonDemo02 instance ;
    
    private SingletonDemo02(){}
    
    public static synchronized SingletonDemo02 getInstance(){
        if(instance == null){
            instance = new SingletonDemo02();
        }
        return instance;
    }
}

2.3.2 其他

  • 双重检测锁式(由于jvm底层内部模型原因,偶尔会出现问题,不建议使用)SingletonDemo03
package cn.itcast.day226.singleton;
//双重检测锁模式,不建议使用
public class SingletonDemo03 {
    private static SingletonDemo03 instance = null;
    private SingletonDemo03(){}
    
    public static SingletonDemo03 getInstance(){
        //之前在懒汉式中我们是将方法同步了,但是这里我们在方法一开始不进行同步
        //而只有在第一次加载的时候发现对象没有实例化才同步
        if(instance == null){
            SingletonDemo03 sc ;
            synchronized (SingletonDemo03.class) {
                sc = instance;
                if(sc == null){
                    synchronized (SingletonDemo03.class) {
                        if(sc == null){
                            sc = new SingletonDemo03();
                        }
                    }
                    instance = sc ;
                }
            }
        }
        return instance;
    }
}
  • 静态内部类式(线程安全,调用效率不高,但是可以延时加载)SingletonDemo04
package cn.itcast.day226.singleton;
//静态内部类模式(其实也是一种懒加载方式)
/*
 * 外部类没有static属性,则不会像饿汉式那样立即加载对象
 * 只有真正调用getInstance方法才会加载静态内部类。加载类时是线程安全的。instance是static final
 * 类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性
 * 兼备了并发高效调用和延时加载的优势
 * */
public class SingletonDemo04 {
    
    private SingletonDemo04(){}
    
    private static class SingletonClassInstance {
        private static final SingletonDemo04 instance = new SingletonDemo04();
    }
    
    public static SingletonDemo04 getInstance(){
        return SingletonClassInstance.instance;
    }
}
  • 枚举单例(线程安全,调用效率高,不能延时加载)SingletonDemo05
package cn.itcast.day226.singleton;
//枚举模式,枚举天然就是单例的
/*
 * 实现简单,枚举本身就是单例,由jvm从根本上提供保障。避免通过反射和反序列化的漏洞,但是不能延时加载
 * */
public enum SingletonDemo05 {
    
    INSTANCE;//定义一个枚举元素,它就代表了Singleton的一个实例
    
    //单例可以有自己的操作
    public void singletonOperation(){
        //功能处理
    }
    
    //使用例子
    public static void main(String[] args) {
        SingletonDemo05 sd1 = SingletonDemo05.INSTANCE;
        SingletonDemo05 sd2 = SingletonDemo05.INSTANCE;
        System.out.println(sd1 == sd2);
        sd1.singletonOperation();//调用方法
    }
}

2.4 如何选用

  • 如果单例对象占用资源要少,不需要延时加载:
    使用枚举式好于饿汉式

  • 如果单例对象占用资源大,需要延时加载:
    静态内部类式好于懒汉式

2.5 问题

  • 反射可以破解上面几种方式(除枚举式)
  • 反序列化可以破解上面几种方式(除枚举式)
    • 可以通过定义readResolve()防止获得不同对象
    • 反序列化时,如果对象所在类定义了readResolve(),(实际上是一种回调),定义返回哪个对象
package cn.itcast.day226.singleton;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
/*
 * 测试懒汉式(如何防止反射和反序列化漏洞)
 * */
public class SingletonDemo06 implements Serializable{
    private static SingletonDemo06 instance;

    private SingletonDemo06() {
        //只有在没有实例化对象的时候才能使用构造器
        if(instance != null){
            throw new RuntimeException();//通过抛出异常来防止反射漏洞
        }
    }

    public static synchronized SingletonDemo06 getInstance() {
        if (instance == null) {
            instance = new SingletonDemo06();
        }
        return instance;
    }
    
    //在反序列化时,如果我们定义了这个方法,则会直接调用这个方法返回当前的对象,而不是实例化一个新的对象
    private Object readResolve() throws ObjectStreamException{
        return instance;
    }

    public static void main(String[] args) throws Exception {
        SingletonDemo02 s1 = SingletonDemo02.getInstance();
        SingletonDemo02 s2 = SingletonDemo02.getInstance();
        System.out.println(s1 == s2);

        // 加反射
        Class<SingletonDemo02> clazz = (Class<SingletonDemo02>) Class
                .forName("cn.itcast.day226.singleton.SingletonDemo02");
        Constructor<SingletonDemo02> c = clazz.getDeclaredConstructor(null);// 获得无参构造器
        c.setAccessible(true);// 跳过权限检查
        SingletonDemo02 s3 = c.newInstance();
        SingletonDemo02 s4 = c.newInstance();
        System.out.println(s3 == s4);// 此时两个对象就不想等了
        
        //下面我们看上面改造之后的情况
        Class<SingletonDemo06> clazz1 = (Class<SingletonDemo06>) Class
                .forName("cn.itcast.day226.singleton.SingletonDemo06");
        Constructor<SingletonDemo06> c1 = clazz1.getDeclaredConstructor(null);// 获得无参构造器
        c.setAccessible(true);// 跳过权限检查
        SingletonDemo06 s5 = c1.newInstance();
        SingletonDemo06 s6 = c1.newInstance();
        System.out.println(s5 == s6);// 此时两个对象就不想等了
        
        //加反序列化
        //序列化:将s1对象写到磁盘上
        FileOutputStream fos = new FileOutputStream("d:/a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s1);
        oos.close();
        fos.close();
        
        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/a.txt"));
        SingletonDemo02 s7 = (SingletonDemo02) ois.readObject();
        System.out.println(s1 == s7);
        
        //防止反序列化,定义readResolve方法
        SingletonDemo06 s8 = SingletonDemo06.getInstance();
        //序列化:将s1对象写到磁盘上
        FileOutputStream fos1 = new FileOutputStream("d:/b.txt");
        ObjectOutputStream oos1 = new ObjectOutputStream(fos1);
        oos1.writeObject(s8);
        oos1.close();
        fos1.close();
        
        //反序列化
        ObjectInputStream ois1 = new ObjectInputStream(new FileInputStream("d:/b.txt"));
        SingletonDemo06 s9 = (SingletonDemo06) ois1.readObject();
        System.out.println(s8 == s9);
    }
}

2.6 测试五中模式的效率

  • 饿汉式:22ms
  • 懒汉式:636ms
  • 静态内部类式:28ms
  • 枚举式:32ms
  • 双重检测锁式:65ms
package cn.itcast.day226.singleton;
import java.util.concurrent.CountDownLatch;
//测试多线程环境下五种创建单例模式的效率
public class Client {
    public static void main(String[] args) throws InterruptedException {
        
        long start = System.currentTimeMillis();
        int threadNum = 10;
        final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
        
        for(int i = 0; i < 10; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j = 0; j < 100000; j++){
                        Object obj = SingletonDemo01.getInstance();//这里进行检测
                    }
                    //这里在上面定义CountDownLatch的时候需要加final,因为内部类不能直接使用外部的局部变量
                    countDownLatch.countDown();
                }
            }).start();;
        }
        countDownLatch.await();//等待所有线程都执行完
        long end = System.currentTimeMillis();
        //如果不使用CountDownLatch则下面的结果只是main线程的时间,而其他线程其实还没有运行完
        System.out.println(end - start);
    }
}

说明:

  • 这里我们通过CountDownLatch同步辅助类,在完成一组在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
  • countDown():当前线程调用此方法,则计数减一(建议放在finally中)
  • await():调用此方法会一直阻塞当前线程,直到计数器的值为0
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容