文章目录
一、简介
单例模式是
一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。
单例模式(Singleton Pattern
)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。
这种方式简化了在复杂环境下的配置管理
二、单例设计模式结构与写法
单例模式的主要有以下角色:
- 单例类。只能创建一个实例的类
- 访问类。使用单例类
而对于单例模式的写法,从创建时机来看,有两种:分别是饿汉式
, 懒汉式
。
- 饿汉式:类加载就会导致该单实例对象被创建。
- 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
下面,我来对这两种创建方式进行介绍:
2.1 饿汉式
饿汉就是类一旦加载,就把单例初始化完成,保证getInstance
的时候,单例是已经存在的了。时机是类的初始化阶段。可以通俗的理解就是一个男人很饿,一看到食物就马上动手吃,不管三七二十一就开始吃饭。
下面是几种饿汉式的定义方式,保证在getInstance
的时候,单例就已经是存在的了。
1. 静态变量方式
public class Singleton {
//1、私有构造方法,防止通过new来创建对象,没有构造方法,在对象实例化就不能完成
private Singleton() {}
//2、在成员位置创建该类的对象
private static Singleton instance = new Singleton();
// 对外提供静态方法获取该对象的接口
public static Singleton getInstance() {
return instance;
}
}
该方式在成员位置声明Singleton
类型的静态变量,并创建Singleton
类的对象instance
。instance
对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
2. 静态代码块方式
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
static {
instance = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}
该方式在成员位置声明Singleton
类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的静态变量方式基本上一样,当然该方式也存在内存浪费问题。
3. 枚举方式
枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了Java程序设计中枚举设计的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
public enum Singleton {
INSTANCE;
}
2.2 懒汉式
而懒汉比较懒,只有当调用getInstance
的时候,才回去初始化这个单例。也就不会在一开始就初始化浪费资源。
这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized
,所以严格意义上它并不算单例模式。
所以,为了解决线程安全的问题,所以,变得复杂起来,我们要保证每次获取都是同一个对象。
方式1:线程不安全
演示一下线程不安全问题。
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
从上面代码我们可以看出该方式在成员位置声明Singleton
类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?
当调用getInstance()
方法获取Singleton
类的对象的时候才创建Singleton
类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。
由于if
判断在JVM层面不是原子操作。所以,在高并发的情况下,很可能有两个线程同时进入到if的条件中,导致new了多个对象,不满足单例的基本要求。
1. 测试多线程下暴露的问题
测试代码:
public class Client {
private final static int THREAD_NUMBER = 200;
public static void main(String[] args) {
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread("thread"+ i){
@Override
public void run() {
Singleton instance = Singleton.getInstance();
System.out.println(Thread.currentThread().getName() + " :: " + instance);
}
}.start();
}
}
}
为了能看到效果,我们让对象初始化的时间变长,如下代码:
public class Singleton {
// 私有构造方法
private Singleton(){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 声明singleton类型变量为instance
private static Singleton instance; // 只是声明,没有赋值
// 提供对外访问
public static Singleton getInstance(){
// 判断instance是否是null,如果是null,说明还没有创建singlton对象
if(instance == null) instance = new Singleton();
return instance;
}
}
执行结果:
发现,并不符合单例的原则。
方式2:线程安全的方式
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
通过上述说明,直接在if
方法中new
对象的方式,在JVM指令方面不是原子操作,可能会导致多个线程同时进入if方法内部。导致获取到的对象不是同一个。
我们,想到直接在对应的方法加上一个锁。这样就解决了线程安全问题。
该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()
方法上添加了synchronized
关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance
的时候才会出现线程安全问题,一旦初始化完成就不存在了。
于是,我们就看看能不能在方式2的基础上进行优化。
方式3:双重检查锁机制
再来讨论一下懒汉模式中加锁的问题,对于getInstance()
方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。
由此也产生了一种新的实现模式:双重检查锁模式
public class Singleton {
//私有构造方法
private Singleton() {}
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为null
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题。
上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile
关键字, volatile
关键字可以保证可见性和有序性。
public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为空
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
方式4:静态内部类方式
上面的方法已经完美解决了懒汉式下的所有问题。接下来,我们提供另外几种解决方案。
静态内部类单例模式中实例由内部类创建,由于 JVM
在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static
修饰,保证只被实例化一次,并且严格保证实例化顺序。
public class Singleton {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
第一次加载Singleton
类时不会去初始化INSTANCE
,只有第一次调用getInstance
,虚拟机加载SingletonHolder
并初始化INSTANCE
,这样不仅能确保线程安全,也能保证 Singleton
类的唯一性。
tips:
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
三、破坏单例模式的几种情况及其解决方案
使上面定义的单例类(Singleton)
可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射
。
3.1 序列化反序列化
1. 问题出现
Singleton类:
public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
测试代码Client
public class Client {
public static void main(String[] args) throws Exception {
// writeObject2File();
readObjectFromFile();
readObjectFromFile();
}
public static void readObjectFromFile() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\fangshaolei\\Desktop\\a.txt"));
Singleton singleton = (Singleton) ois.readObject();
System.out.println(singleton);
}
// 向文件中写数据
public static void writeObject2File () throws Exception{
// 1.获取singleton对象
Singleton instance = Singleton.getInstance();
// 2. 创建对象输出流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\fangshaolei\\Desktop\\a.txt"));
// 3. 写对象
oos.writeObject(instance);
// 4. 释放资源
oos.close();
}
}
测试结果:
测试结果中,发现,当前两次反序列化之后的对象,并不是同一个。
2. 序列化、反序列方式破坏单例模式的解决方法
在Singleton
类中添加readResolve()
方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new
出来的对象。
public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* 下面是为了解决序列化反序列化破解单例模式
*/
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}
❓ 为什么加上readResolve
方法可以避免这个问题?
源码解析
:
🎶🎂✨归根到底,在创建出了不同的对象就是在读对象的时候,出现了问题。
所有,我们从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);//重点查看readObject0方法
.....
}
private Object readObject0(boolean unshared) throws IOException {
...
try {
switch (tc) {
...
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));//重点查看readOrdinaryObject方法
...
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
private Object readOrdinaryObject(boolean unshared) throws IOException {
...
//isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类,
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;
}
3.2 反射
1. 问题出现
Singleton
public class Singleton {
private Singleton(){}
public static class SingletonHolder{
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
测试类:
public class Client {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// 获取字节码对象
Class clazz = Singleton.class;
// 获取无参的构造方法
Constructor cons = clazz.getDeclaredConstructor();
// 取消访问检查
cons.setAccessible(true);
// 创建singleton对象
Singleton s2 = (Singleton) cons.newInstance();
Singleton s1 = (Singleton) cons.newInstance();
System.out.println(s1 == s2);
}
}
结果分析:
对象不是同一个。
2. 反射方式破解单例的解决方法
public class Singleton {
//私有构造方法
private Singleton() {
/*
反射破解单例模式需要添加的代码
*/
if(instance != null) {
throw new RuntimeException();
}
}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}
这种方式比较好理解。当通过反射方式调用构造方法进行创建创建时,直接抛异常。不运行此中操作。
四、从JDK源码解析-Runtime类看单例设计模式
Runtime
类就是使用的单例设计模式。
- 通过源代码查看使用的是哪儿种单例模式
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
...
}
从上面源代码中可以看出Runtime
类使用的是恶汉式(静态属性)方式来实现单例模式的。
2. 试着使用Runtime
中的方法.
public class RuntimeDemo {
public static void main(String[] args) throws IOException {
//获取Runtime类对象
Runtime runtime = Runtime.getRuntime();
//返回 Java 虚拟机中的内存总量。
System.out.println(runtime.totalMemory());
//返回 Java 虚拟机试图使用的最大内存量。
System.out.println(runtime.maxMemory());
//创建一个新的进程执行指定的字符串命令,返回进程对象
Process process = runtime.exec("ipconfig");
//获取命令执行后的结果,通过输入流获取
InputStream inputStream = process.getInputStream();
byte[] arr = new byte[1024 * 1024* 100];
int b = inputStream.read(arr);
System.out.println(new String(arr,0,b,"gbk"));
}
}
五、总结
一般在开发应用中,建议使用第1种饿汉式单例
,而不推荐使用第2种懒汉式单例,除非明确需要延迟加载
时,才会使用静态内部类单例,若涉及到反序列化创建对象时,推荐使用枚举式单例。其他也可以考虑使用双重检锁式单例。
创建的步骤:
1. 私有化构造方法,防止外界私自创建对象。
2. 一个返回对象的方法。
3. 可以返回对象的变量。
常见的场景:
- Windows的任务管理器,每次只能打开一个。
- 应用程序的日志应用。
- 网上在线人数统计。
- 配置文件的访问类。
- 数据库的连接池。
- 多线程的线程池。
- 操作系统的文件系统
Notice: 本文的所有代码已经放在GitHub仓库中:单例设计模式(Singleton Pattern)GitHub Link
参考:
《设计模式之禅》