一、单例模式的介绍
1.1 什么是单例模式
单例模式
指的是一个类只会有一个实例 ,即一个类只有一个对象实例。它的特点有:
单例类只能有一个 实例
单例类必须自己创建自己 的唯一实例
单例类必须给所有其他对象提供这一实例
1.2 单例模式的应用场景
(1)一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;售票时,一共有100张票,可有有多个窗口同时售票,但需要保证不要超售(这里的票数余量就是单例,售票涉及到多线程)。
(2)在前端创建工具箱窗口,工具箱要么不出现,出现也只出现一个。
遇到问题:每次点击菜单都会重复创建“工具箱”窗口。
解决方案:使用 if 语句,在每次创建对象的时候首先进行判断是否为 null ,如果为 null 再创建对象。
(3)如果在 5 个地方需要实例出工具箱窗体。
遇到问题:这个小 bug 需要改动 5 个地方,并且代码重复,代码利用率低
解决方案:利用单例模式,保证一个类只有一个实例,并提供一个访问它的全局访问点。
二、传统单例模式的实现
传统的单例模式实现可以分为懒汉式
和饿汉式
:
2.1 懒汉
懒汉实现又分为线程安全和线程不安全这两种写法。先说下线程不安全的写法,这种写法是懒加载很明显 ,但是在多线程不能正常工作,存在线程安全问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Singleton { private static Singleton instance; private Singleton () {} public static Singleton getInstance () { if (instance == null ) { instance = new Singleton(); } return instance; } }
下面是线程安全的写法,这种写法在getInstance()
方法中加入了synchronize
锁。能够在多线程中很好的工作,而且也具备很好的懒加载 ,但是效率很低(因为锁),并且大多数情况下不需要同步这个功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Singleton { private static Singleton instance; private Singleton () {} public static synchronized Singleton getInstance () { if (instance == null ) { instance = new Singleton(); } return instance; } }
懒汉模式的优点 :
懒汉模式的缺点 :
在不加锁的情况下,线程不安全,可能出现多份实例
在加锁的情况下,会是程序串行化,使系统有严重的性能问题
2.2 饿汉
另一种单例类别是饿汉,这种方式基于 ClassLoder 机制避免了多线程的同步问题,不过 instance 在类装载时就实例化,这时候初始化 instance 显然没有达到懒加载的效果 。
1 2 3 4 5 6 7 8 9 10 11 12 class Singleton { private static Singleton instance = new Singleton(); private Singleton () {} public static Singleton getInstance () { return instance; } }
对上面代码稍微修改下,就出现了饿汉的一个变种,本质上是一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Singleton { private static Singleton instance = null ; static { instance = new Singleton(); } private Singleton () {} public static Singleton getInstance () { return instance; } }
饿汉模式的优点 :
由于使用了 static 关键字,保证了在引用这个变量时,关于这个变量的所以写入操作都完成,所以保证了 JVM 层面的线程安全
饿汉模式的缺点 :
不能实现懒加载,造成空间浪费,如果一个类比较大,我们在初始化的时就加载了这个类,但是我们长时间没有使用这个类,这就导致了内存空间的浪费。
2.3 静态内部类
1 2 3 4 5 6 7 8 9 10 11 class Singleton { private static class SingletonHolder { private static Singleton instance = new Singleton(); } private Singleton () {} public static Singleton getInstance () { return SingletonHolder.instance; } }
这种方式同样利用了ClassLoder的机制来保证初始化instance时只有一个线程,但是恶汉模式只要Singleton类被装载了,那么instance就会被实例化,而这种方式只有显示通过调用getInstance()
方法时,才会显示装载SingletonHolder类,从而实例化instance,从而达到懒加载 的效果。
2.4 双重校验锁
在懒汉模式中对加锁的处理,对于getInstance()
方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的问题。由此也产生了一种新的实现模式:双重检查锁模式 。它是线程安全版懒汉的升级版,在 JDK1.5 之后,使用双重检查锁定才能够正常达到单例效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Singleton { private volatile static Singleton singleton; private Singleton () {} public static Singleton getInstance () { if (singleton == null ) { synchronized (Singleton.class) { if (singleton == null ) { singleton = new Singleton(); } } } return singleton; } }
2.5 唯一实例为什么是 static
单例模式实现过程如下:
首先,将该类的构造函数私有化 (目的是禁止其他程序创建该类的对象);
其次,在本类中自定义一个对象 (既然禁止其他程序创建该类的对象,就要自己创建一个供程序使用,否则类就没法用,更不是单例);
最后,提供一个可访问类自定义对象的类成员方法 (对外提供该对象的访问方式)。
直白的讲就是,你不能用该类在其他地方创建对象,而是通过该类自身提供的方法访问类中的那个自定义对象。那么问题的关键来了,程序调用类中方法只有两种方式:
上面说了,构造函数私有化后第一种情况就不能用,只能使用第二种方法。
而使用类名直接调用类中方法,类中方法必须是静态的,而静态方法不能访问非静态成员变量,因此类自定义的实例变量也必须是静态的。
2.6 双重校验锁实现为什么要加 volatile
先说结论,作用主要有两个:
保证该变量在多线程下的可见性
限制编译器指令重排
第一点就不解释了,关于 volatile 详细介绍可以看这篇文章:《Java并发编程——volatile关键字解析》
关于第二点,首先解释下编译器的指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
1 2 3 4 int a = 10 ; int r = 2 ; a = a + 3 ; r = a * a;
这段代码有4个语句,那么可能的一个执行顺序是: 语句2—>语句1—>语句3—>语句4。但是不可能是: 语句2—>语句1—>语句4—> 语句3。
因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令 Instruction 2 必须用到 Instruction 1 的结果,那么处理器会保证 Instruction 1 会在 Instruction 2 之前执行。虽然重排序不会影响单个线程内程序执行的结果,但是多线程下就有可能出现问题,例如:
1 2 3 4 5 6 7 8 9 context = loadContext(); inited = true ; while (!inited ){ sleep() } doSomethingwithconfig(context);
上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行 doSomethingwithconfig(context) 方法,而此时 context 并没有被初始化,就会导致程序出错。
介绍完指令重排序,下面言归正传:
对于Java编译器而言,初始化实例和将对象地址写到字段中并非是原子操作,且这两个阶段的执行顺序是未定义的。假设某个线程执行了 new Singleton(),构造方法还未被调用,编译器仅仅为该对象分配了内存空间并设定默认值,此时若其他线程调用 getInstance() 方法,由于 instance != null,但是此时 instance 对象还没有被赋予真正的有效值,从而无法取到正确的单例对象。
使用 volatile 关键字限制编译器对它的相关读写操作,对它的读写操作进行指令重排,确定对象实例化后才返回引用。
三、反射与序列化
3.1 反射问题
《Effective Java》这本书中着重推荐的一种实现。在这本书中,首先它对传统饿汉、懒汉实现评价如下:
享有特权的客户端可以借助 AccessibleObject.setAccessible 方法,通过反射机制调用私有构造器。
也就是说利用反射机制是可以破坏它的单例性的,举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class SingletonTest { public static void main (String[] args) throws Exception { Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true ); Singleton s3 = constructor.newInstance(); System.out.println(s1 + "\n" + s2 + "\n" + s3); System.out.println("正常情况下,实例化两个实例是否相同:" + (s1 == s2)); System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:" + (s1 == s3)); } } class Singleton { private volatile static Singleton singleton; private Singleton () {} public static Singleton getInstance () { if (singleton == null ) { synchronized (Singleton.class) { if (singleton == null ) { singleton = new Singleton(); } } } return singleton; } }
通过反射机制,调用私有的构造方法,就可以破坏它的单例性,解决办法也很简单,在构造方法中抛出异常即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class SingletonTest { public static void main (String[] args) throws Exception { Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true ); Singleton s3 = constructor.newInstance(); System.out.println(s1 + "\n" + s2 + "\n" + s3); System.out.println("正常情况下,实例化两个实例是否相同:" + (s1 == s2)); System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:" + (s1 == s3)); } } class Singleton { private volatile static Singleton singleton; private Singleton () throws IllegalAccessException { if (singleton != null ) { throw new IllegalAccessException("Do not allow access" ); } } public static Singleton getInstance () throws IllegalAccessException { if (singleton == null ) { synchronized (Singleton.class) { if (singleton == null ) { singleton = new Singleton(); } } } return singleton; } }
3.2 序列化问题
普通的 Java 类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新 new 出来的,所以这就破坏了单例。
如下代码所示,如果单例类实现了序列化接口,序列化前和序列化后不是同一个对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class SingletonTest { public static void main (String[] args) throws Exception { Singleton s1 = Singleton.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj" )); oos.writeObject(s1); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("SerSingleton.obj" ); ObjectInputStream ois = new ObjectInputStream(fis); Singleton s2 = (Singleton)ois.readObject(); ois.close(); System.out.println(s1+"\n" +s2); System.out.println("序列化前后两个是否同一个:" +(s1==s2)); } } class Singleton implements Serializable { private volatile static Singleton singleton; private Singleton () {} public static Singleton getInstance () { if (singleton == null ) { synchronized (Singleton.class) { if (singleton == null ) { singleton = new Singleton(); } } } return singleton; } }
四、枚举类实现
枚举类实现单例模式被认为是最完美的实现,它规避了传统单例模式的反射问题和序列化问题。
1 2 3 4 5 6 public enum Singleton { INSTANCE; public static Singleton getInstance () { return INSTANCE; } }
首先来测试下是否存在反射问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class SingletonTest { public enum Singleton { INSTANCE; public static Singleton getInstance () { return INSTANCE; } } public static void main (String[] args) throws Exception { Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); System.out.println("正常情况下,实例化两个实例是否相同:" + (s1 == s2)); Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true ); Singleton s3 = constructor.newInstance(); System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:" + (s1 == s3)); } }
当反射调用构造方法时,抛出异常,这里的根本原因是反射对于枚举类型,无法通过构造方法创建实例。
再来看看它在序列化中的表现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class SingletonTest { public enum Singleton { INSTANCE; public static Singleton getInstance () { return INSTANCE; } } public static void main (String[] args) throws Exception { Singleton s1 = Singleton.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj" )); oos.writeObject(s1); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("SerSingleton.obj" ); ObjectInputStream ois = new ObjectInputStream(fis); Singleton s2 = (Singleton)ois.readObject(); ois.close(); System.out.println(s1+"\n" +s2); System.out.println("序列化前后两个是否同一个:" +(s1==s2)); } }
在序列化的时候Java仅仅是将枚举对象的 name 属性输出到结果中,反序列化的时候则是通过 java.lang.Enum
的 valueOf
方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject
、readObject
等方法。