GOF23设计模式之单例模式(singleton)
一、单例模式概述
保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。
由于单例模式只生成一个实例,减少了系统性能开销。所以当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
单例模式可以在系统设置全局的访问点,优化共享资源访问,例如可以设计一个单例类,复制所有数据表的映射处理。
1. 懒汉式
1)线程不安全
① 构造器私有化,避免外部直接创建对象
② 声明一个私有的静态属性
③ 对外创建一个公共的静态方法访问该属性
如果属性没有该对象,创建该对象
2)线程安全
使用同步方法
3)双重检测(线程安全)double checking DCL
使用同步块
好处:第一次是为了不必要的同步,第二次是在属性等于null的情况下才创建实例
2. 饿汉式
① 构造器私有化,避免外部直接创建对象
② 声明一个私有的静态属性,同时创建该对象
③ 对外创建一个公共的静态方法访问该属性
好处:类加载时就已经加载该类对象,当需要该对象时,若没有则创建,若有直接返回
3. 静态内部类
① 构造器私有化,避免外部直接创建对象
② 使用静态内部类声明一个私有的静态属性,同时创建该对象
③ 对外创建一个公共的静态方法访问该属性
好处:不调用静态方法不加载内部类,延缓加载,提高效率
4.枚举
枚举式 (jdk1.4及以前建议使用)
二、单例模式的五种写法
1.饿汉式
优点:线程安全,效率高
缺点:无法延时加载
1 public class Singleton { 2 3 private static Singleton instance = new Singleton(); 4 5 //私有化构造器 6 private Singleton() {} 7 8 //提供全局访问点 9 public static Singleton getInstance() { 10 return instance; 11 } 12 13 }
1 public class Singleton { 2 3 private static Singleton instance = null; 4 5 static { 6 instance = new Singleton(); 7 } 8 9 //私有化构造器 10 private Singleton() {} 11 12 //提供全局访问点 13 public static Singleton getInstance() { 14 return instance; 15 } 16 17 }
2. 懒汉式
优点:线程安全,延时加载
缺点:效率较低
(1)非线程安全
1 public class Singleton { 2 3 private static Singleton instance; 4 5 //私有化构造器 6 private Singleton() {} 7 8 //提供一个全局的访问点 9 public static Singleton getInstance() { 10 if (instance == null) { 11 instance = new Singleton(); 12 } 13 return instance; 14 } 15 16 }
(2)线程安全
1 public class Singleton { 2 3 private static Singleton instance; 4 5 //私有化构造器 6 private Singleton() {} 7 8 //使用同步方法获取该类对象 9 public static synchronized Singleton getInstance() { 10 if (instance == null) { 11 instance = new Singleton(); 12 } 13 return instance; 14 } 15 16 }
1 public class Singleton { 2 3 private static Singleton instance; 4 5 //私有化构造器 6 private Singleton() {} 7 8 //使用同步块获取该类对象 9 public static Singleton getInstance() { 10 synchronized (Singleton.class) { 11 if (instance == null) { 12 instance = new Singleton(); 13 } 14 return instance; 15 } 16 } 17 18 }
3.双重检查锁
注意:由于编译器优化和JVM底层内部模型原因,偶尔会出问题,不建议使用
优点:线程安全,延时加载
缺点:效率较低,会出错误
1 public class Singleton { 2 3 private static Singleton instance; 4 5 private Singleton() {} 6 7 //第一次判断是为了避免不必要的同步,第二次判断是属性为null时创建实例 8 public static Singleton getInstance() { 9 if (instance == null) { 10 synchronized (Singleton.class) { 11 if (instance == null) { 12 instance = new Singleton(); 13 } 14 } 15 } 16 return instance; 17 } 18 19 }
4.静态内部类式
不调用静态方法不加载内部类,延缓加载(懒加载),提高效率
优点:线程安全,延时加载,效率高
1 public class Singleton { 2 3 //静态内部类 4 private static class SingletonHolder { 5 //final可加可不加,因为外部类的外部无法使用该内部类 6 private static /*final*/ Singleton instance = new Singleton(); 7 } 8 9 //私有化构造器 10 private Singleton() {} 11 12 //提供一个全局的访问点 13 public static Singleton getInstance() { 14 return SingletonHolder.instance; 15 } 16 17 }
5.枚举
注意:建议使用
优点:实现简单,线程安全,效率高,由于JVM从根本上实现保障,避免反射和反序列化的漏洞
缺点:无延时加载
1 1 public enum Singleton { 2 2 //这个枚举元素,本身就是一个单例对象 3 3 INSTANCE; 4 4 }
三、测试五种单例模式的耗时问题
1 /** 2 在多线程环境下测试使用单例设计模式时创建对象的耗时(100个线程创建10000个对象) 3 饿汉式:15ms 4 懒汉式:671ms 5 双重检测锁:65ms 6 静态内部类:23ms 7 枚举:32ms 8 * @author CL 9 * 10 */ 11 public class TestRuntime { 12 13 public static void main(String[] args) throws Exception { 14 long start = System.currentTimeMillis(); 15 16 int threadNum = 100; 17 final CountDownLatch countDownLatch = new CountDownLatch(threadNum); 18 //匿名内部类 19 for (int i = 0; i < threadNum; i++) { 20 new Thread(new Runnable() { 21 22 @Override 23 public void run() { 24 for(int i = 0; i < 10000; i++) { 25 //依次测试 26 Object o1 = SingletonDemo01.getInstance(); //饿汉式 27 // Object o2 = SingletonDemo02.getInstance(); //懒汉式 28 // Object o3 = SingletonDemo03.getInstance(); //双重检查锁 29 // Object o4 = SingletonDemo04.getInstance(); //静态内部类 30 // Object o5 = SingletonDemo05.INSTANCE; //枚举 31 } 32 } 33 }).start(); 34 countDownLatch.countDown(); //递减锁存器的计数,如果计数到达零,则释放所有等待的线程 35 } 36 37 countDownLatch.await(); //阻塞main线程,知道计数器为0才开始继续执行 38 39 long end = System.currentTimeMillis(); 40 System.out.println("总耗时:"+(end - start)+"ms"); 41 } 42 43 }
注意,根据电脑配置等因素时间会有差异,但时间比大致相同。
如何选用单例模式?
(1)单例对象占用资源少,不需要延时加载时:枚举式好于饿汉式
(2)单例对象占用资源大,需要延时加载时:静态内部类式好于懒汉式
四、破解单例模式
1.使用反序列化破解单例模式
步骤:(1)使用序列化将已经创建的对象写出到系统文件
(2)使用反序列化读取文件中的对象
1 import java.io.Serializable; 2 3 /** 4 * 使用反序列化破解单例模式 5 * @author CL 6 * 7 */ 8 public class Singleton implements Serializable { 9 10 private static class SingletonHolder { 11 private static Singleton instance = new Singleton(); 12 } 13 14 private Singleton() {} 15 16 public static Singleton getInstance() { 17 return SingletonHolder.instance; 18 } 19 20 }
1 import java.io.FileInputStream; 2 import java.io.FileOutputStream; 3 import java.io.ObjectInputStream; 4 import java.io.ObjectOutputStream; 5 6 /** 7 * 测试反序列化破解单例模式 8 * @author CL 9 * 10 */ 11 public class TestSingleton { 12 13 public static void main(String[] args) throws Exception { 14 //先获得一个对象 15 Singleton s01 = Singleton.getInstance(); 16 System.out.println("先获取的对象:" + s01); 17 18 //(1)使用序列化将对象写出到系统文件 19 String filePath = "C:\\file\\obj.txt"; 20 ObjectOutputStream oos = new ObjectOutputStream( 21 new FileOutputStream(filePath)); 22 oos.writeObject(s01); 23 oos.flush(); 24 oos.close(); 25 26 //(2)使用反序列化读取文件中的对象 27 ObjectInputStream ois = new ObjectInputStream( 28 new FileInputStream(filePath)); 29 Singleton s02 = (Singleton) ois.readObject(); 30 ois.close(); 31 32 System.out.println("反序列化读取的对象:" + s02); 33 } 34 35 }
控制台输出:
先获取的对象:com.caolei.singleton.Singleton@15db9742
反序列化读取的对象:com.caolei.singleton.Singleton@33909752
显然已经通过反序列化已经创建了新的对象,解决办法是在类中添加如下代码:
1 /** 2 * 反序列化时,如果创建了readResolve()方法则直接返回已经创建好的对象,而不需要再重新创建新的对象 3 * @return 4 * @throws ObjectStreamException 5 */ 6 private Object readResolve() throws ObjectStreamException { 7 return instance; 8 }
现在再测试:
1 import java.io.ObjectStreamException; 2 import java.io.Serializable; 3 4 /** 5 * 使用反序列化破解单例模式 6 * @author CL 7 * 8 */ 9 public class Singleton implements Serializable { 10 11 private static class SingletonHolder { 12 private static Singleton instance = new Singleton(); 13 } 14 15 private Singleton() {} 16 17 public static Singleton getInstance() { 18 return SingletonHolder.instance; 19 } 20 21 //反序列化时,如果创建了readResolve()方法则直接返回已经创建好的对象,而不需要再重新创建新的对象 22 private Object readResolve() throws ObjectStreamException { 23 return SingletonHolder.instance; 24 } 25 26 }
1 import java.io.FileInputStream; 2 import java.io.FileOutputStream; 3 import java.io.ObjectInputStream; 4 import java.io.ObjectOutputStream; 5 6 /** 7 * 测试反序列化破解单例模式 8 * @author CL 9 * 10 */ 11 public class TestSingleton { 12 13 public static void main(String[] args) throws Exception { 14 //先获得一个对象 15 Singleton s01 = Singleton.getInstance(); 16 System.out.println("先获取的对象:" + s01); 17 18 //(1)使用序列化将对象写出到系统文件 19 String filePath = "C:\\file\\obj.txt"; 20 ObjectOutputStream oos = new ObjectOutputStream( 21 new FileOutputStream(filePath)); 22 oos.writeObject(s01); 23 oos.flush(); 24 oos.close(); 25 26 //(2)使用反序列化读取文件中的对象 27 ObjectInputStream ois = new ObjectInputStream( 28 new FileInputStream(filePath)); 29 Singleton s02 = (Singleton) ois.readObject(); 30 ois.close(); 31 32 System.out.println("反序列化读取的对象:" + s02); 33 } 34 35 }
控制台输出:
先获取的对象:com.caolei.singleton.Singleton@15db9742
反序列化读取的对象:com.caolei.singleton.Singleton@15db9742
现在创建的对象是同一对象,避免了反序列化破解单例模式出现的问题。
2.使用反射破解单例模式
步骤:(1)获取对象
(2)获取构造器
(3)跳过安全检查
(4)创建对象
1 /** 2 * 使用反射破解单例模式 3 * @author CL 4 * 5 */ 6 public class Singleton { 7 8 private static class SingletonHolder { 9 private static Singleton instance = new Singleton(); 10 } 11 12 private Singleton() {} 13 14 public static Singleton getInstance() { 15 return SingletonHolder.instance; 16 } 17 18 }
1 import java.lang.reflect.Constructor; 2 3 /** 4 * 测试反射破解单例模式 5 * @author CL 6 * 7 */ 8 public class TestSingleton { 9 10 public static void main(String[] args) throws Exception { 11 Singleton s1 = Singleton.getInstance(); 12 System.out.println("先获取的对象:" + s1); 13 14 //使用反射破解单例模式 15 //(1)创建对象 16 Class<Singleton> clazz = (Class<Singleton>) Class.forName("com.caolei.singleton.Singleton"); 17 //(2)获得构造器 18 Constructor<Singleton> c = clazz.getDeclaredConstructor(null); 19 //(3)跳过安全检查 20 c.setAccessible(true); 21 //(4)创建对象 22 Singleton s2 = c.newInstance(); 23 24 System.out.println("反射获取的对象:" + s2); 25 } 26 }
控制台输出:
先获取的对象:com.caolei.singleton.Singleton@15db9742
反射获取的对象:com.caolei.singleton.Singleton@6d06d69c
显然使用反射机制跳过安全检查通过私有的构造器创建了新的对象,解决办法是在私有的构造器中添加如下代码:
1 if (instance != null) { 2 throw new RuntimeException(); //再次创建对象时抛出异常 3 }
现在再测试:
1 /** 2 * 使用反射破解单例模式 3 * @author CL 4 * 5 */ 6 public class Singleton { 7 8 private static class SingletonHolder { 9 private static Singleton instance = new Singleton(); 10 } 11 12 private Singleton() { 13 if (SingletonHolder.instance != null) { 14 throw new RuntimeException(); //再次创建对象时抛出异常 15 } 16 } 17 18 public static Singleton getInstance() { 19 return SingletonHolder.instance; 20 } 21 22 }
1 import java.lang.reflect.Constructor; 2 3 /** 4 * 测试反射破解单例模式 5 * @author CL 6 * 7 */ 8 public class TestSingleton { 9 10 public static void main(String[] args) throws Exception { 11 Singleton s1 = Singleton.getInstance(); 12 System.out.println("先获取的对象:" + s1); 13 14 //使用反射破解单例模式 15 //(1)创建对象 16 Class<Singleton> clazz = (Class<Singleton>) Class.forName("com.caolei.singleton.Singleton"); 17 //(2)获得构造器 18 Constructor<Singleton> c = clazz.getDeclaredConstructor(null); 19 //(3)跳过安全检查 20 c.setAccessible(true); 21 //(4)创建对象 22 Singleton s2 = c.newInstance(); 23 24 System.out.println("反射获取的对象:" + s2); 25 } 26 }
控制台输出:
先获取的对象:com.caolei.singleton.Singleton@15db9742 Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:408) at com.caolei.singleton.TestSingleton.main(TestSingleton.java:24) Caused by: java.lang.RuntimeException at com.caolei.singleton.Singleton.<init>(Singleton.java:16) ... 5 more
五、单例模式常见的应用场景
(1)Windows的Task Manager(任务管理器)、Recycle Bin(回收站)、文件系统等都是典型的单例模式;
(2)在项目中,读取配置文件的类一般也只有一个对象,没有必要每次使用配件的数据时,都去new一个对象来获取;
(3)网站的计数器,一般也是采用单例模式实现,否则难以同步;
(4)数据库连接池的设计就是采用单例模式;
(5)在Spring中,每个Bean默认是单例的,便于Spring容器管理;
(6)每个Servlet都是单例的;
(7)………………