目录
单例设计模式概述
单例设计模式:保证一个类仅有一个实例,并且提供一个全局访问点。
所谓类的单例设计模式,就是采取一定的措施保证在整个的软件系统中,使某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的访问权限设置为 private
,这样,就不能用 new
操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象。在类的外部一开始还无法获得类的对象,只能调用该类的某个静态方法以返回类内部创建的对象。静态方法只能访问类中的静态成员变量,所以,指向类内部创建的该类对象的变量也必须声明成静态的。
主要步骤
1)构造器私有化(防止在类的外部通过new创建该类对象)
2)在类的内部创建一个对象实例
3)对外提供一个公共的静态方法 getInstance(),返回实例对象
单例模式实现方案
1. 饿汉式(静态常量,线程安全)
饿汉式:类初始化时就创建类的对象完成实例化。
JVM在类初始化阶段(即在Class被加载后,且线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
class Singleton {
//类的内部创建对象
private final static Singleton INSTANCE = new Singleton();
//构造器私有化(防止外部通过new创建对象)
private Singleton() {}
//对外提供一个公共的静态方法 getInstance(),返回实例对象
public static Singleton getInstance() {
return INSTANCE;
}
}
public class SingletonTest1 {
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2);
}
}
运行结果:
true
优缺点说明
1)优点:写法简单,在类初始化时就完成实例化,避免了线程同步问题,JVM保证线程安全
2)缺点:在类初始化时就完成实例化,没有达到延迟加载的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
3)这种方式基于类的初始化机制,避免了多线程的同步问题,不过,instance 在类初始化时就完成实例化,在单例模式中大多数都是调用getInstance()方法,但是导致类初始化的原因有很多种,因此不能确定有其他的方式(或者调用类的其他静态方法)导致类初始化,这时实例化 instance 就没有达到延迟加载的效果
4)结论:可用,但可能造成内存浪费
2. 饿汉式(静态代码块,线程安全)
将上面创建对象的步骤放入静态代码块中
class Singleton {
private final static Singleton INSTANCE;
//在静态代码块中创建对象
static {
INSTANCE = new Singleton();
}
//构造器私有化(防止外部通过new创建对象)
private Singleton() {}
//对外提供一个公共的静态方法 getInstance(),返回实例对象
public static Singleton getInstance() {
return INSTANCE;
}
}
优缺点说明
1)这种方式和上面的方式类似,只不过将类实例化的过程放在了静态代码块中,也是在类初始化的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面方式是一样的。
2)结论:可用,但可能造成内存浪费
3. 懒汉式(线程不安全)
懒汉式:延迟加载,真正使用到对象时(调用 getInstance() 方法)才去创建。在Java程序中,有时候可能需要推迟一些高开销对象的实例化操作,并且只有在使用这些对象的时候才实例化,此时,程序员可能会采用延迟实例化。
class Singleton {
private static Singleton instance;
//构造器私有化(防止外部通过new创建对象)
private Singleton() {}
//对外提供一个公共的静态方法 getInstance(),当需要使用到该类的对象时才去创建
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优缺点说明
1)起到了延迟加载的效果,但是只能在单线程下使用。
2)如果在多线程下,一个线程进入了 if (singleton == null) 判断语句块,还未来得及往下执行,如果另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
3)结论:不要使用
4. 懒汉式(同步方法,线程安全,但性能差)
在 getInstance() 方法声明加上 synchronized 关键字,保证线程安全。同一时刻只能有一个线程进入同步方法执行。
class Singleton {
private static Singleton instance;
//构造器私有化(防止外部通过new创建对象)
private Singleton() {}
//对外提供一个公共的静态方法 getInstance(),当需要使用到该类的对象时才去创建
//使用synchronized关键字修饰getInstance()方法保证线程安全
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优缺点说明
1)保证了线程安全
2)效率太低了,每个线程在想获得类的实例时,执行getInstance()方法都要进行同步操作,频繁的加锁、释放锁需要消耗资源,使用synchronized关键字是一种重量级的同步机制,开销庞大、效率低。而其实这个方法只执行一次实例化代码就够了,如果对象已经创建,直接返回已创建的对象就行了,无需执行同步操作
3)结论:在实际开发中,不推荐使用这种方式
5. 懒汉式(同步代码块,但并不能保证线程安全)
同一时刻只能有一个线程进入同步代码块执行
class Singleton {
private static Singleton instance;
//构造器私有化(防止外部通过new创建对象)
private Singleton() {}
//对外提供一个公共的静态方法 getInstance(),当需要使用到该类的对象时才去创建
public static Singleton getInstance() {
if (instance == null) {
//可能有多个线程通过if判断
synchronized(Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
优缺点说明
1)这种方式,本意是想对第四种实现方式改进,因为同步方法效率太低, 改为同步产生实例化的代码块
2)但是这种同步并不能保证线程安全,跟第3种实现方式遇到的情形一 致,假如一个线程进入了 if (singleton == null) 判断语句块, 还未来得及往下执行,另一个线程也通过了这条判断语句,同步代码块就会被多次执行,这时便会产生多实例
3)结论:不能使用
6. 懒汉式(双重检查加锁,线程安全)
getInstance()方法进行两次if (singleton == null)判断,同一时刻只能有一个线程进入同步代码块执行,在同步代码块中再加一层if (singleton == null)判断
class Singleton {
private static volatile Singleton instance;
//构造器私有化(防止外部通过new创建对象)
private Singleton() {}
//对外提供一个公共的静态方法 getInstance(),当需要使用到该类的对象时才去创建
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
单例的引用需要用volatile关键字修饰,volatile关键字的作用:
禁止指令重排序
实际上创建对象要经过如下3个步骤:
1)分配对象所需的内存空间
2)初始化对象
3)设置 instance 变量指向刚分配的内存地址
2)和 3)的顺序可能会被重排序,在单线程下,访问对象是不受影响的;但在多线程下,可能会发生问题,访问一个没有初始化完成的对象操作其内部数据时,可能会抛出空指针异常。使用volatile关键字修饰可以禁止重排序,防止第3步先于第2步执行。
保证内存可见性
由于可见性问题,线程0创建了实例,且变量instance还未同步到主存中;此时线程1从主存中拿到的instance变量还是null
如果加上了volatile修饰instance之后,保证了内存可见性,一旦线程0返回了实例,线程1可以立即发现instance不为null(主要用在第一层if判断,同步代码块在第二层if判断就可以保证内存可见性)。
优缺点说明
1)进行了两次if (singleton == null)检查,可以保证线程安全
2)实例化代码只执行一次,后面再次访问时,判断if (singleton == null),直接返回已实例化对象,也避免反复进行方法同步
3)线程安全,延迟加载,效率较高
4)结论:推荐使用
7. 静态内部类(线程安全)
class Singleton {
//构造器私有化(防止外部通过new创建对象)
private Singleton() {}
//静态内部类定义静态属性,调用构造器,基于类初始化
private static class SingletonInnerClass {
private final static Singleton INSTANCE = new Singleton();
}
//对外提供一个公共的静态方法 getInstance(),返回静态内部类中创建的单例对象
public static Singleton getInstance() {
return SingletonInnerClass.INSTANCE;
}
}
优缺点说明
1)一个类只会被类加载器加载一次,JVM保证线程安全
2)装载外部类Singleton并不会导致内部类SingletonInnerClass的装载,调用getInstance方法访问静态属性时会装载内部类,从而完成Singleton的实例化
3)优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
4)结论:推荐使用
8. 枚举(线程安全)
利用枚举的实例是有限个数的特性实现单例模式,如果我们只定义一个实例就相当于是单例了。
enum Singleton {
//单例对象
INSTANCE;
//对外提供一个公共的静态方法 getInstance(),返回实例对象
public static Singleton getInstance() {
return INSTANCE;
}
//其他方法
public void method() {
System.out.println("方法执行了...");
}
}
优缺点说明
1)借助JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止通过反射、反序列化重新创建新的对象。
2)结论:推荐使用
反序列化破坏单例模式及解决方案
反序列化破坏饿汉式单例,让单例类实现Serializable接口:
package demo1;
import java.io.*;
class Singleton implements Serializable {
//类的内部创建对象
private final static Singleton INSTANCE = new Singleton();
//构造器私有化(防止外部通过new创建对象)
private Singleton() {}
//对外提供一个公共的静态方法 getInstance(),返回实例对象
public static Singleton getInstance() {
return INSTANCE;
}
}
public class SingletonTest1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//获取单例对象
Singleton instance = Singleton.getInstance();
//创建序列化流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
//将单例对象写入文件中
oos.writeObject(instance);
//创建反序列化流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
//从文件中读取对象
Singleton newInstance = (Singleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
运行结果:
demo1.Singleton@14ae5a5
demo1.Singleton@6d03e736
false
解决方案:
在单例类Singleton中添加一个readResolve()方法:
private Object readResolve() {
return INSTANCE;
}
调用readObject()方法会查找单例类一个方法名为 readResolve 的方法,若存在方法名为 readResolve 的方法,则执行该方法,在该方法中返回单例类中创建的单例对象,从而保证从单例类中获取的对象和通过反序列化获取的是同一个对象。
通过测试发现前7种方案均不能防反序列化重新创建新的对象。
反序列化破坏枚举单例:
package demo8;
import java.io.*;
enum Singleton {
INSTANCE;
//对外提供一个公共的静态方法 getInstance(),返回实例对象
public static Singleton getInstance() {
return INSTANCE;
}
public void method() {
System.out.println("方法执行了...");
}
}
public class SingletonTest8 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//获取单例对象
Singleton instance = Singleton.getInstance();
//创建序列化流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
//将单例对象写入文件中
oos.writeObject(instance);
//创建反序列化流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
//从文件中读取对象
Singleton newInstance = (Singleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
运行结果:
INSTANCE
INSTANCE
true
调用readObject()方法若发现单例类型是枚举,则返回枚举中已存在的枚举对象,因此枚举单例能够防反序列化重新创建新的对象。
反射破坏单例模式及解决方案
前7种方案均不能防反射重新创建新的对象
在类加载初始化时就已创建好单例对象的单例模式:
以饿汉式单例为例:
package demo1;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
class Singleton {
//类的内部创建对象
private final static Singleton INSTANCE = new Singleton();
//构造器私有化(防止外部通过new创建对象)
private Singleton() {}
//对外提供一个公共的静态方法 getInstance(),返回实例对象
public static Singleton getInstance() {
return INSTANCE;
}
}
public class SingletonTest1 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//正常获取单例对象
Singleton instance = Singleton.getInstance();
//获取单例类的Class对象
Class<Singleton> clazz = Singleton.class;
//获取单例类的私有无参构造器
Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
//放开构造器对象的访问权限
constructor.setAccessible(true);
//反射创建单例实例
Singleton newInstance = constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
运行结果:
demo7.Singleton@1b6d3586
demo7.Singleton@4554617c
false
解决方案:
在构造器中加上处理逻辑,只针对在类加载初始化时就已经创建好单例对象的单例模式有效,即饿汉式和基于静态内部类实现的懒加载单例模式有效,对其他懒加载实现方式无效
例如:若已存在单例对象,则直接抛出异常:
private Singleton() {
if (INSTANCE != null) {
throw new RuntimeException("单例构造器禁止反射调用");
}
}
将main方法首行获取单例对象的代码放在反射创建单例对象代码的后面,上面的解决方案仍然生效,程序抛出异常。
对于不是在类加载初始化时就已创建好单例对象的单例模式是无法避免的:
以双重检查加锁单例模式为例:
package demo6;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
class Singleton {
private static volatile Singleton instance;
//构造器私有化(防止外部通过new创建对象)
private Singleton() {
if (instance != null) {
throw new RuntimeException("单例构造器禁止反射调用");
}
}
//对外提供一个公共的静态方法 getInstance(),当需要使用到该类的对象时才去创建
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
public class SingletonTest6 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//获取单例类的Class对象
Class<Singleton> clazz = Singleton.class;
//获取单例类的私有无参构造器
Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
//放开构造器对象的访问权限
constructor.setAccessible(true);
//反射创建单例实例
Singleton newInstance = constructor.newInstance();
Singleton newInstance2 = constructor.newInstance();
//正常获取单例对象
Singleton instance = Singleton.getInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(newInstance2);
System.out.println(instance == newInstance);
System.out.println(newInstance2 == newInstance);
}
}
运行结果:
demo6.Singleton@1b6d3586
demo6.Singleton@4554617c
demo6.Singleton@74a14482
false
false
反射破坏枚举单例:
package demo8;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
enum Singleton {
INSTANCE;
//对外提供一个公共的静态方法 getInstance(),返回实例对象
public static Singleton getInstance() {
return INSTANCE;
}
public void method() {
System.out.println("方法执行了...");
}
}
public class SingletonTest8 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<Singleton> clazz = Singleton.class;
/*Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
for (Constructor<?> constructor : declaredConstructors) {
System.out.println("构造方法:" + constructor); //构造方法:private demo8.Singleton(java.lang.String,int)
}*/
Constructor<Singleton> declaredConstructor = clazz.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
Singleton instance = declaredConstructor.newInstance("instance", 0);
System.out.println(instance);
}
}
反射创建单例对象失败,程序抛出异常:
Exception in thread “main” java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at demo8.SingletonTest8.main(SingletonTest8.java:53)
Constructor类的newInstance方法中有这样一段代码:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
当单例类型是枚举时,直接抛出 IllegalArgumentException,因此枚举单例能够防反射重新创建新的对象
单例模式在JDK中的应用
例如:JDK 中,java.lang.Runtime类 就是经典的单例模式(饿汉式)
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
//其他方法
...
}