设计模式之单例模式的应用场景和各种写法的比较
单例模式
先简单说下设计模式:常见的一共20多种,仅仅的去了解哪个设计模式是怎么回事并不难,而把它用于实际的开发中很多初学者是做不到的,不少人说把设计模式融入开发中需要两三年的经验积累。我最初学习设计模式是因为要应付面试,很明显是一种背诵式的学习方法,至今没有实际性的用过几个。所以我打算在学习和总结这个模块的时候,把每个设计模式的应用场景或为何要使用它的原因重点说一下,只有清楚了这东西是干什么的,才有学习它的意义。
单例模式做的无非就是保证一种事物只有一个实例,放在Java中就是保证某个类只会产生唯一的一个实例对象。
举几个常见的应用场景:你电脑上好比有道词典,酷狗音乐,电脑管家这些软件,打开多少次都是一个模样,没有办法再次打开一个新的,这就是单例的一个体现。
放在JavaWeb里讲,对于一个web项目,servletContext这个上下文对象就是单例模式的体现,因为它对应着配置文件,是全局所共享的,jsp的内置对象application也是这个道理。
另外常见的数据库连接池和线程池一般也采用单例设计模式,这样既能方便资源间的互相通信,又能方便统一管理。
单例模式的几种常见写法多数人大概已经很熟了,我这里就简单概括一下(代码直接粘贴别处的)。
1:饿汉式(一般推荐)
1 public class Singleton { 2 private static Singleton instance = new Singleton(); 3 private Singleton (){} 4 public static Singleton getInstance() { 5 return instance; 6 } 7 }
饿,顾名思义就是类刚一加载就直接帮你创建好这个对象,也不管你用还是不用。虽然做到了线程安全,但是造成了资源浪费,基本是最low的写法,所以不推荐
2:非线程安全的懒汉式(不推荐)
1 public class Singleton { 2 private static Singleton instance; 3 private Singleton (){} 4 5 public static Singleton getInstance() { 6 if (instance == null) { 7 instance = new Singleton(); 8 } 9 return instance; 10 } 11 }
懒,顾名思义就是你只有在调用它的时候才会为你创建对象,避免了资源浪费,但是非线程安全,依然low,不推荐
3:双重校验锁(一般推荐)
1 public class Singleton { 2 private volatile static Singleton singleton; 3 private Singleton (){} 4 public static Singleton getSingleton() { 5 if (singleton == null) { 6 synchronized (Singleton.class) { 7 if (singleton == null) { 8 singleton = new Singleton(); 9 } 10 } 11 } 12 return singleton; 13 } 14 }
其实是对public static synchronized Singleton getInstance()的一个改进(因为直接锁住方法的话,线程多的时候阻塞会极为严重),绝大对数情况下能避免线程同步问题,但是由于jvm指令重排(jvm学明白了会再解释一下)的原因,也并非绝对安全的。
4:静态内部类(推荐)
1 public class Singleton { 2 private static class SingletonHolder { 3 private static final Singleton INSTANCE = new Singleton(); 4 } 5 private Singleton (){} 6 public static final Singleton getInstance() { 7 return SingletonHolder.INSTANCE; 8 } 9 }
利用了Java的类加载机制(类加载时只能有一个线程),因为INSTANCE这个对象只会跟随着静态内部类的加载而被初始化且只会初始化一次,所以做到了线程安全,且只有调用getInstance()方法的时候才会被加载,所以推荐。
5:枚举(推荐)
1 public enum Singleton { 2 INSTANCE; 3 private String name; 4 public void setName(String name){ 5 this.name=name; 6 } 7 public String getName(){ 8 return this.name; 9 } 10 }
简单粗暴,保证了线程安全,而且避免了通过反射(接下来会重点讲)和反序列化再次创建新对象这种情况,具体实现可查看源码。推荐。
前四种方式的不安全性--反射入侵
很明显,单例模式这些写法最核心的部分就是将构造函数私有化,这是保证只有一个实例的关键所在(因为在外部别人已无法使用new来再次创造了)。但是了解反射的都知道,私有构造函数是可以通过反射来入侵的。下面就举例子:(Singleton用的第四种)
1 public class Test2 { 2 public static void main(String[] args) throws Exception { 3 Singleton s1 = Singleton.getInstance(); 4 Singleton s2 = Singleton.getInstance(); 5 6 System.out.println(s1==s2);//true 7 //下面开始利用反射来破解私有构造器 8 Class<?> clazz=Singleton.class; 9 10 //获取私有构造器且设置为可访问状态 11 Constructor<?> c=clazz.getDeclaredConstructor(); 12 c.setAccessible(true); 13 //利用构造器再次创建对象 14 Singleton s3=(Singleton) c.newInstance(); 15 16 System.out.println(s1==s3);//false 17 } 18 }
由此可见外界已经重新创建出了一个新的对象,那么应该如何防止这种情况发生?可以设置一个判断变量boolean flag,下面重新写一下;
1 public class Singleton { 2 private static boolean flag=true; 3 private static class SingletonHolder { 4 private static final Singleton INSTANCE = new Singleton(); 5 } 6 private Singleton (){ 7 if(!flag){ 8 throw new RuntimeException("禁止入侵"); 9 } 10 flag=false; 11 } 12 13 public static final Singleton getInstance() { 14 return SingletonHolder.INSTANCE; 15 } 16 }
再次执行就会发生异常,如下:
至于如何防止序列化,等明天写关于Serializable时会提到。
这是本人的第一篇技术博客,挑选了单例模式这种早就烂大街的题材,以后会写一些有技术含量的。本篇不足之处还望多提建议。