[19/04/22-星期一] GOF23_创建型模式(单例模式)

一、概念

        《Design Patterns: Elements of Reusable Object-Oriented Software》(即后述《设计模式》一书),由 Erich Gamma、Richard Helm、Ralph Johnson

和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为"四人组(Group of Four)"。

创建型模式(5个):单例模式、工厂模式、抽象工厂模式、建造者模式、原型模式;

结构型模式(7个):适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式;

行为模式(11个):模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。

1、单例模式

核心:保证一个类有且只有一个对象(实例),并且提供一个访问该实例的全局访问点。

应用场景:Windows 的任务管理器(Task Manager);

Windows的回收站(Recycle Bin),在整个系统运行中,回收站一直维护着仅有的一个实例;

项目中,读取配置文件类,一般也只有一个类,没必要每次使用配置文件的数据,每次new一个对象去读取;

网站的计数器,一般也是采用单例模式,否则难以同步;

应用程序的日志,也是采用单例模式,这是由于共享文件的日志文件一直处于打开状态,因为只有一个实例去操作,否则内容不好追加;

数据库的连接池也是采用单例模式,因为数据库的连接的是一种数据库资源;

操作系统的文件系统,也是采用单例模式,因为一个操作系统只有一个文件系统;

Application 也是典型的单例模式;

在Spring中,每个Bean默认就是单例,优点是Spring容器都可以管理;

在servlet(Server Applet:小服务程序或服务连接器,Java编写的服务器端程序),每个servlet也是单例

在Spring MVC框架/struts1框架中,控制对象也是单例。

优点: 由于单例只生成一个示例,减少了系统的性能开销,当一个对象产生需要比较多的资源时,如读取配置,产生其他依赖对象时,则可以通过在应用启动时直接

产生一个单例对象,然后永久驻留内存的方式来解决。单例模式可以在系统设置全局的访问点,优化环共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理

5种实现方式:

主要有2种:

(1) 饿汉式(线程安全、调用效率,但是不能延时加载)

(2) 懒汉式(线程安全、调用效率不高,但是可以延时加载)

次要有3种:

(3)双重检测模式(由于JVM底层内部模型的原因,偶尔会出现问题,不建议使用)

(4)静态内部类式(线程安全、调用效率高,可以延时加载)

(5)枚举单例(线程安全、调用效率高,不能延时加载,但是可以防止反射和反序列化漏洞)

如何选用?

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

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

【初步认识】

/***
 * 23-1:单例模式 
 * 主要有2种:
(1) 饿汉式(线程安全、调用效率,但是不能延时加载)

(2) 懒汉式(线程安全、调用效率不高,但是可以延时加载)

次要有3种:

(3)双重检测模式(由于JVM底层内部模型的原因,偶尔会出现问题,不建议使用)

(4)静态内部类式(线程安全、调用效率高,可以延时加载)

(5)枚举单例(线程安全、调用效率高,不能延时加载)
 */
package cn.sxt.pattern;

/*  1)饿汉式(单例对象立即加载)
 *    static变量会在类加载时初始化,此时也不会涉及多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生
 * 并发访问的问题。因此synchronized关键字可以省略。
 * 问题:如果只是加载本类,而不是调用getInstance(),甚至永远没有调用,则会造成资源浪费。
 * */
class Singleton{
    //第2步:私有化一个静态对象。类初始化时立即加载这个对象instance
    private static Singleton instance=new Singleton();//instance没什么特殊含义,用s也行,实例化对象的名字

    private Singleton() { //第1步:私有构造器,只有自己可以用

    }
    //由于加载类时,天然的线程安全,方法不用同步,调用效率高
    public static Singleton getInstance() {
        return instance;    
    }    
}


/* 2)懒汉式(单例对象延时加载,不立即加载,中间用的时候才去new一个新的对象)
 * 延时加载,真正用的时候才去加载!
 * 问题:资源利用率高,但是,每次调用getInstance()方法时,要使用并发,效率低
 * */
class Singleton02{
    private static Singleton02 s;
    private  Singleton02() {//私有化构造器

    }
    //为啥加同步?因为当线程A执行到s==null后,睡觉去啦,当线程B进来后发现s也是null,会去创建一个对象,当线程A醒来之后也去创建
    //一个新的对象,这样就2个对象,违反单例的定义(即一个单例类有且只有一个示例(对象))
    public static synchronized Singleton02 getInstance() {
        if (s==null) {
            s=new Singleton02();
        }
        return s;
    }    
}

/*3)双重检测锁的实现(实际工作很少用),综合了懒汉和饿汉的模式
 * 这个模式将同步的内容下放到if内部,提高了执行效率,不必每次获取对象时都进行同步,只有第一次才同步,创建对象后就没必要同步了
 * 问题:由于编译器优化的原因和JVM底层内部模型原因,偶尔会出问题
 * */

class Singleton03{
    private static Singleton03 s=null;
    private  Singleton03() {    
    }
    
    public static Singleton03 getInstance() {
        
        if (s==null) {
            Singleton03 sc;
            synchronized (Singleton03.class) {
                sc=s;
                if (sc==null) {
                    synchronized (Singleton03.class) {
                        if (sc==null) {
                            sc=new Singleton03();
                        }                        
                    }
                    s=sc;
                }                
            }            
        }
        return s;        
    }    
}

/*4) 静态内部类的实现(也是一种懒加载)
 * 外部类没有static属性,则不会像饿汉式那样立即加载对象
 * 只有真正调用getInstance方法时才会加载静态内部类,加载类时线程是安全的,对象s是static final类型的,保证了内存中
 * 只有一个实例存在,而且只能赋值一次,从而保证了线程的安全性
 * 好处:兼备了并发高效调用和延迟加载的优势
 * 
 * */

class Singleton04{
    private static class SingletonClassInstance {
        private static final Singleton04 s=new Singleton04();
    }
    //初始化Singleton04类时不会立即初始化静态内部类SingletonClassInstance,只能用到时才会通过SingletonClassInstance.s
    //去调用对象s(这里也可以看作是它的属性),延时加载
    public static Singleton04 getInstance() {
        return SingletonClassInstance.s;//        
    }
    private  Singleton04() {
        
    }    
}

/*5)枚举方式(没有懒加载)
 * 优点:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障,避免通过反射和反序列的漏洞
 * 缺点:无法延时加载
 * */
enum Singleton05{//注意定义与class的区别
    
    //定义一个枚举元素INSTANCE,它就代表Singleton05的一个单例对象
    INSTANCE;
    
    //添加自己需要的操作
    public void singletonOperation() {
        
    }
    
}

public class Test_0422_Singleton {
    public static void main(String[] args) {
        /*getInstance是一个函数,在java中,可以用这种方式使用单例模式创建类的实例,所谓单例模式就是一个类有且只有一个实例,
        不像object ob=new object();的这种方式去实例化后去使用 */
        /*Singleton s1=Singleton.getInstance();
        Singleton s2=Singleton.getInstance();
        System.out.println(s1==s2);    //输出为true*/    
        
        Singleton02 s1=Singleton02.getInstance();
        Singleton02 s2=Singleton02.getInstance();
        System.out.println(s1==s2);    //输出为true
        
        Singleton05 s5=Singleton05.INSTANCE;
        Singleton05 s6=Singleton05.INSTANCE;
        System.out.println(s5==s6);    //输出为true

    }
}

【反射和序列化破解】

/***
 * 使用反射和反序列破解4种单例模式(不包含枚举,枚举基于JVM底层无法破解,最安全)
 * 以懒汉式为例
 */
package cn.sxt.pattern;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;


class Singleton06 implements Serializable{
    private static Singleton06 s;
    private  Singleton06() {//加了抛出异常的私有化构造器,可以防止破解。当线程试图创建多个对象时会抛出异常。一般不用考虑
        if (s!=null) {
            throw new RuntimeException();    
        }

    }
    /*private  Singleton06() {//没加抛出异常的私有化构造器
        
    }*/
    
    //为啥加同步?因为当线程A执行到s==null后,睡觉去啦,当线程B进来后发现s也是null,会去创建一个对象,当线程A醒来之后也去创建
    //一个新的对象,这样就2个对象,违反单例的定义(即一个单例类有且只有一个示例(对象))
    public static synchronized Singleton06 getInstance() {
        if (s==null) {
            s=new Singleton06();
        }
        return s;
    }
    
    //防止通过反序列破解单例。意思是在反序列化时直接调用这个方法,通过这个方法去返回我们指导的对象s,而不是创建一个新的对象
    private Object readResolve() throws Exception {//这个方法自己不用调,实现反序列化时自动调用
        return s;
    }
}


public class Test_0422_Singleton2 {
    public static void main(String[] args) throws Exception {
        Singleton06 s1=Singleton06.getInstance();
        Singleton06 s2=Singleton06.getInstance();
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s1==s2);    //输出为true,s1和s2是同一对象
        
        /*//通过反射破解单例。通过反射直接调用私有构造器
        Class<Singleton06> clz = (Class<Singleton06>)Class.forName("cn.sxt.pattern.Singleton06");
        Constructor<Singleton06> constructor=clz.getDeclaredConstructor(null);
        constructor.setAccessible(true);//跳过权限检测,访问私有对象        
        Singleton06 s3=constructor.newInstance();
        Singleton06 s4=constructor.newInstance();
        System.out.println(s3==s4);//输出为false,显然s3和s4不是同一对象。证明跳过了单例
       */        
        
        /**序列化:将对象状态转换为字节流的过程,可以将其保存到磁盘文件中或通过网络发送到任何其他程序;
         * 反序列化:从字节流创建对象的相反的过程称为反序列化。而创建的字节流是与平台无关的,在一个平台上序列化的对象可以在
         * 不同的平台上反序列化。
         * */
        //通过反序列化破解单例
        FileOutputStream fos =new FileOutputStream("D:/a.txt");
        ObjectOutputStream oos=new ObjectOutputStream(fos);
        oos.writeObject(s1);//把对象s1经由ObjectOutputStream写出到文件 D:/a.txt中
        oos.close();
        fos.close();
        
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream("D:/a.txt"));
        Singleton06 s5=(Singleton06)ois.readObject();
        System.out.println(s5);//输出结果与s1和s2不同,证明破解了单例,若加了readResolve()方法,输出结果相同
        
    }    
}

【看看效率】

/***
 * 测试5种单例模式的执行效率
 * 懒汉式:效率最慢
 */
package cn.sxt.pattern;

import java.util.concurrent.CountDownLatch;

public class Test_0422_Singleton3 {
    public static void main(String[] args) throws InterruptedException {
        test();


    }
    
    public static void test() throws InterruptedException {
        int threadNum=5;
        long start=System.currentTimeMillis();

        /***解决时间不准的问题:主线程(与5个线程独立)可能已经执行完毕到end处了,输出主线程的时间。但是5个线程还有没有
         * 执行完毕的,达不到效果。
         * CountDownLatch类:同步辅助类,是一个统计是否线程结束计数器。 latch:插销 Down:向下 
         *  -countDown() 当前线程调用此方法,则计数器减一
         *  -await() 调用此方法会一直阻塞线程,直至计数器为0
         * */
        final CountDownLatch count=new CountDownLatch(threadNum);
        
        
        for (int i = 0; i < threadNum; i++) {//创建5个独立线程,让每个线程去执行调用100次单例模式的getInstance()方法
            new Thread(new Runnable() {
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        //Object object=Singleton04.getInstance();
                        Object object2=Singleton05.INSTANCE;
                    }
                    count.countDown();//执行完一个线程 ,调用countDown()方法自动将threadNum(正在执行的线程总数)减一
                }
            }).start();            
        }    
        count.await();//阻塞main线程,一直等待,循环检测,看其他线程是否执行完才会继续往下
        
        long end=System.currentTimeMillis();
        System.out.println("总耗时:"+(end-start)+"毫秒");    
    }
}

 

posted @ 2019-04-21 01:13  ID长安忆  阅读(173)  评论(0编辑  收藏  举报