[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)+"毫秒"); } }