模式介绍
所谓类的单例模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
八种实现方式
饿汉式(静态变量)
示例代码
package model;
/**
* 饿汉式(静态变量)
*
* @author asyyr
*/
public class SingletonTest01 {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2); // 结果为true
}
}
class Singleton {
// 1、构造器私有化,外部不能new实例
private Singleton() {
}
// 2、本类内部创建对象实例
private static final Singleton instance = new Singleton();
// 提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
return instance;
}
}
优缺点说明
1、优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题;
2、缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果一直都没使用过这个实例,则会造成内存的浪费;这种方式基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,在单例模式中大多数都是调用 getInstance 方法,但是导致类装载的原因有很多种,因此不能确定有其它的方式(或者其它的静态方法)导致类装载,这时候初始化 instance 就没有达到 Lazy Loading 的效果;
3、结论:这种单例模式可用,可能会造成内存浪费。
饿汉式(静态代码块)
示例代码
package model;
/**
* 饿汉式(静态代码块)
*
* @author asyyr
*/
public class SingletonTest01 {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
class Singleton {
// 1、构造器私有化,外部不能new实例
private Singleton() {
}
private static final Singleton instance;
// 2、本类内部静态代码块中创建对象实例
static {
instance = new Singleton();
}
// 提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
return instance;
}
}
优缺点说明
1、这种方式的优缺点跟饿汉式(静态变量)类似,只是将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。
2、结论:这种单例模式可用,可能会造成内存浪费。
懒汉式(线程不安全)
示例代码
package model;
/**
* 懒汉式(线程不安全)
*
* @author asyyr
*/
public class SingletonTest01 {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
class Singleton {
// 1、构造器私有化,外部不能new实例
private Singleton() {
}
private static Singleton instance;
// 提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
if (null == instance) {
instance = new Singleton();
}
return instance;
}
}
优缺点说明
1、起到了 Lazy Loading 的效果,但是只能在单线程下使用;
2、如果在多线程下,一个线程进入了 if (singleton == null) 判断语句块,还没来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例,所以在多线程环境下不可使用这种方式;
3、结论:在实际开发中,不能使用这种方式。
懒汉式(线程安全,同步方法)
示例代码
package model;
/**
* 懒汉式(线程安全,同步方法)
*
* @author asyyr
*/
public class SingletonTest01 {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
class Singleton {
// 1、构造器私有化,外部不能new实例
private Singleton() {
}
private static Singleton instance;
// 提供一个公有的静态方法,返回实例对象
public static synchronized Singleton getInstance() {
if (null == instance) {
instance = new Singleton();
}
return instance;
}
}
优缺点说明
1、解决了线程不安全问题;
2、效率太低了,每个线程在想获得类的实例时候,执行 getInstance() 方法都要进行同步。而其实这个方法只执行一次实例化就够了,后面的想获得该类实例,直接 return 就可以了。方法进行同步效率太低;
3、结论:在实际开发中,不推荐使用这种方式
懒汉式(线程安全,同步代码块)
示例代码
package model;
/**
* 懒汉式(线程安全,同步代码块)
*
* @author asyyr
*/
public class SingletonTest01 {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
class Singleton {
// 1、构造器私有化,外部不能new实例
private Singleton() {
}
private static Singleton instance;
// 提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
if (null == instance) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
优缺点说明
1、这种同步不能起到线程安全的作用,在多线程下,一个线程进入了 if (singleton == null) 判断语句块,还没来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例;
2、结论:在实际开发中,不能使用这种方式。
双重检查
示例代码
package model;
/**
* 双重检查
*
* @author asyyr
*/
public class SingletonTest01 {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
class Singleton {
// 1、构造器私有化,外部不能new实例
private Singleton() {
}
private static volatile Singleton instance;
// 提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
if (null == instance) {
synchronized (Singleton.class) {
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
优缺点说明
1、双重检查是进行了两次 if (singleton == null) 检查,这样就可以保证线程安全了;
2、实例化代码只需执行一次,往后再次访问的话,判断 if(singleton == null) ,直接 return 实例化对象,也避免了反复执行方法同步;
3、线程安全、延迟加载、效率较高;
4、结论:在实际开发中,推荐使用这种单例设计模式。
静态内部类
示例代码
package model;
/**
* 静态内部类
*
* @author asyy
*/
public class SingletonTest01 {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
class Singleton {
// 1、构造器私有化,外部不能new实例
private Singleton() {
}
private static class SingletonInstance {
private static Singleton instance = new Singleton();
}
// 提供一个公有的静态方法,返回实例对象
public static Singleton getInstance() {
return SingletonInstance.instance;
}
}
优缺点说明
1、采用了类装载的机制来保证初始化实例时只有一个线程;
2、静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载 SingletonInstance 类,从而完成 Singleton 的实例化;
3、类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的;
4、优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高;
5、结论:实际工作开发中,推荐使用。
枚举
示例代码
package model;
/**
* 枚举
*
* @author asyyr
*/
public class SingletonTest01 {
public static void main(String[] args) {
Singleton singleton1 = Singleton.INSTANCE;
Singleton singleton2 = Singleton.INSTANCE;
System.out.println(singleton1 == singleton2); // true
System.out.println(singleton1.hashCode() == singleton2.hashCode()); // true
singleton1.getInstance();
}
}
enum Singleton {
INSTANCE;
public void getInstance() {
System.out.println("获取实例");
}
}
优缺点说明
1、借助 JDK1.5 中添加的美剧来实现单例模式,不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象;
2、这种方式是 Effective Java 作用 Josh Bloch 提倡的方式;
3、结论:实际工作中推荐使用。
两种破坏方式
序列化破坏单例模式
package model;
import java.io.*;
/**
* 使用序列化反序列化方式破坏单例
*
* @author asyyr
*/
public class SerialSingle {
public static void main(String[] args) throws Exception {
String filePath = "E:\selfCode\design-pattern\staticClass.txt";
delFile(filePath);
// 静态内部类 单例模式
StaticClassDemo clazz = StaticClassDemo.getInstance();
System.out.println("序列化前对象的地址:" + clazz);
writeObjectToFile(clazz, filePath);
Object object = readObjectFromFile(filePath);
System.out.println("反序列化对象后的地址:" + object);
// 判断两个反序列化后的对象是否是同一个对象
System.out.println(clazz == object);
}
public static Object readObjectFromFile(String filePath) throws Exception {
// 创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
Object object = ois.readObject();
ois.close();
return object;
}
public static void writeObjectToFile(Object object, String filePath) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
oos.writeObject(object);
oos.close();
}
public static void delFile(String filePath) {
File file = new File(filePath);
file.deleteOnExit();
}
}
上面代码运行结果是false,表明序列化和反序列化已经破坏了单例设计模式。
注意:枚举方式不会出现这个问题。
解决方式
在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新创建出来的对象。
package model;
import java.io.Serializable;
/**
* desc:静态内部类 单例模式,线程安全
* @author asyyr
*/
public class StaticClassDemo implements Serializable {
private static final long serialVersionUID = 888L;
private StaticClassDemo() {
System.out.println("实例化对象");
}
private static class InnerClassDemo {
private static final StaticClassDemo INSTANCE = new StaticClassDemo();
}
public static StaticClassDemo getInstance() {
return InnerClassDemo.INSTANCE;
}
/**
* 当反序列化对象时,JVM会由readResolve返回指定对象,也就保证了单例;
* 假如缺乏readResolve方法,反序列化获取的对象跟序列化时的对象不一致;
*
* @return
* @throws Exception
*/
protected Object readResolve() throws Exception {
return StaticClassDemo.getInstance();
}
}
源码分析
核心类:ObjectInputStream 类
public final Object readObject() throws IOException, ClassNotFoundException {
... 忽略其次代码
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);
... 忽略其次代码
}
private Object readObject0(boolean unshared) throws IOException {
... 忽略其次代码
case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared));
... 忽略其次代码
}
private Object readOrdinaryObject(boolean unshared) throws IOException {
... 忽略其次代码
// isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类,
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
... 忽略其次代码
// 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod()方法执行结果为true
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
// 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量
// 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。
Object rep = desc.invokeReadResolve(obj);
}
... 忽略其次代码
return obj;
}
反射破坏单例模式
package model;
import java.lang.reflect.Constructor;
/**
* 使用反射方式破坏单例
*
* @author asyyr
*/
public class ReflectSingle {
public static void main(String[] args) throws Exception {
// 获取Singleton类的字节码对象
Class<StaticClassDemo> clazz = StaticClassDemo.class;
// 获取Singleton类的私有无参构造方法对象
Constructor<StaticClassDemo> constructor = clazz.getDeclaredConstructor();
// 取消访问检查
constructor.setAccessible(true);
// 创建Singleton类的对象s1
StaticClassDemo staticClassDemo1 = constructor.newInstance();
System.out.println(staticClassDemo1);
// 创建Singleton类的对象s2
StaticClassDemo staticClassDemo2 = constructor.newInstance();
System.out.println(staticClassDemo2);
// 判断通过反射创建的两个Singleton对象是否是同一个对象
System.out.println(staticClassDemo1 == staticClassDemo2);
}
}
上面代码运行结果是false,表明可以通过反射破坏单例设计模式。
注意:枚举方式不会出现这个问题。
解决方式
package model;
import java.io.Serializable;
/**
* desc:静态内部类 单例模式,线程安全
* @author asyyr
*/
public class StaticClassDemo {
private static final long serialVersionUID = 888L;
private StaticClassDemo() {
// 反射破解单例模式需要添加的代码
if (InnerClassDemo.INSTANCE != null) {
throw new RuntimeException("单例模式禁止反射创建实例");
}
System.out.println("实例化对象");
}
private static class InnerClassDemo {
private static final StaticClassDemo INSTANCE = new StaticClassDemo();
}
public static StaticClassDemo getInstance() {
return InnerClassDemo.INSTANCE;
}
}
单例类的状态
有状态的单例类
一个单例类可以是有状态的,一个有状态的单例对象一般也是可变(mutable)单例对象。
有状态的可变的单例对象常常当做状态库(repositary)使用。比如一个单例对象可以持有一个 int 类型的属性,用来给一个系统提供一个数值唯一的序列号码,作为某个贩卖系统的账单号码。
当然,一个单例类可以持有一个聚集,从而允许存储多个状态。
无状态的单例类
单例类可以是没有状态的,仅用作提供工具性函数的对象。既然是为了提供工具性函数,也就没有必要创建多个实例,因此使用单例模式很合适。一个没有状态的单例类也就是不变(Immutable)单例类。
多个 JVM 系统的分散式系统
EJB 容器有能力将一个 EJB 的实例跨过几个 JVM 调用。由于单例对象不是 EJB,因此,单例类局限于某一个 JVM 中,换言之,如果 EJB 在跨过 JVM 后仍然需要引用同一个单例类的话,这个单例类就会在数个 JVM 中被实例化,造成多个单例对象的实例出现。一个 J2EE 应用系统可能分布在数个 JVM 中,这时候不一定需要 EJB 就能造成多个单例类的实例出现在不同 JVM 中的情况。
如果这个单例类是没有状态的,那么就没有问题。因为没有状态的对象是没有区别的。但是如果这个单例类是有状态的,那么问题就来了,举例来说,如果一个单例对象可以持有一个 int 类型的属性,用来给一个系统提供一个数值唯一的序列号吗,作为某个贩卖系统的账单号码的话,用户会看到同一个号码出现好几次。
多个类加载器
同一个 JVM 中会有多个类加载器,当两个类加载器同时加载同一个类时,会出现两个实例。在很多 J2EE 服务器允许同一个服务器内有几个 Servlet 引擎时,每一个引擎都有独立的类加载器,经由不同的类加载器加载的对象之间是绝缘的。
除非系统有协调机制,不然应当尽量避免使用有状态的单例类。
注意事项
1、单例模式保证了系统内存中只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能;
2、当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new;
3、单例模式使用的场景:需要频繁的进行创建和销毁的对象,创建对象时耗时过多或者耗费资源过多(即重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或者文件的对象(比如数据源、session 工厂等)。
4、一般而言,双重检查对 Java 编译器不成立。由于类的初始化与变量赋值的顺序不可预料,如果一个线程在没有同步化的条件下读取变量引用,并调用这个对象的方法的话,可能会发现对象的初始化过程尚未完成,从而造成崩溃。
应用场景
JDK Runtime 类
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
}
从上面源代码中可以看出Runtime类使用的是饿汉式(静态变量)方式来实现单例模式的。
package model;
import java.lang.reflect.Constructor;
/**
* 使用反射方式破坏单例
*
* @author asyyr
*/
public class ReflectSingle {
public static void main(String[] args) throws Exception {
// 获取Singleton类的字节码对象
Class<Runtime> clazz = Runtime.class;
// 获取Singleton类的私有无参构造方法对象
Constructor<Runtime> constructor = clazz.getDeclaredConstructor();
// 取消访问检查
constructor.setAccessible(true);
// 创建Singleton类的对象s1
Runtime staticClassDemo1 = constructor.newInstance();
System.out.println(staticClassDemo1);
// 创建Singleton类的对象s2
Runtime staticClassDemo2 = constructor.newInstance();
System.out.println(staticClassDemo2);
// 判断通过反射创建的两个Singleton对象是否是同一个对象
System.out.println(staticClassDemo1 == staticClassDemo2);
}
}
上面代码运行结果是false,表明可以通过反射破坏Runtime类实现的单例模式。但是由于Runtime没有实现Serializable,没法通过序列化方式破坏单例模式。