1.单例模式的定义以及应用场景
单例模式(SingLeton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并且提供一个全局的访问点。单例模式属于创建型模式。单例模式在现实生活中也应用的非常广泛,例如公司的CEO。在J2EE标准中的ServletContext ,Spring框架中的ApplicationContext、数据库连接池等都用到了单例模式
2.饿汉式单例
先来看看饿汉式单例的类图
饿汉式单例是指在类初始化的时候就加载,并且创造单例对象。它绝对线程安全,在线程还没有出现之前就已经初始化了,不可能存在访问安全问题。
我们来看看饿汉式单例标准写法:
/**
* @author sj
* @date 2022/12/8 14:40
*/
public class HungrySingleton {
// static修饰在类加载时就会进行初始化
private static final HungrySingleton INSTANCE = new HungrySingleton();
// 私有化构造方法
private HungrySingleton() {
}
// 提供一个全局访问点
public static HungrySingleton getInstance() {
return INSTANCE;
}
}
还有一种写法,饿汉式静态单例,装个逼用
/**
* @author sj
* @date 2022/12/8 14:47
*/
public class HungryStaticSingleton {
private HungryStaticSingleton() {
}
private static final HungryStaticSingleton INSTANCE;
static {
INSTANCE = new HungryStaticSingleton();
}
public static HungryStaticSingleton getInstance() {
return INSTANCE;
}
}
这两种写法都非常简单,也非常好理解,饿汉式单例模式适用于单例对象较少的情况。这样写可以保证绝对线程安全、执行效率比较高。但是它的缺点也很明显,就是所有对象类加载的时候就实例化。这样一来,如果系统中有大批量的单例对象存在,那系统初始化是就会导致大量的内存浪费。也就是说不管对象用与不用都占着空间,浪费了内存,有可能“占着茅坑不拉屎”。那有没有更优的写法呢?下面我们来继续分析
3 懒汉式单例
为了解决饿汉式单例可能带来的内存浪费问题,于是就出现了懒汉式单例的写法,懒汉式单例模式的特点是,单例对象要在被使用时才会初始化,下面看懒汉式单例模式的简单实现LazySimpleSingleton :
/**
* @author sj
* @date 2022/12/8 14:51
*/
public class LazySimpleSingleton {
private LazySimpleSingleton() {
}
private static LazySimpleSingleton LAZY_INSTANCE = null;
public static LazySimpleSingleton getInstance() {
if (LAZY_INSTANCE == null) {
LAZY_INSTANCE = new LazySimpleSingleton();
}
return LAZY_INSTANCE;
}
}
但这样写又带来了一个新的问题,如果在多线程环境下,就会出现线程安全问题。我先来模拟一下编写线程类 ExecutorThread:
import Lazy.LazySimpleSingleton;
/**
* @author sj
* @date 2022/12/8 14:59
*/
public class ExecutorThread implements Runnable{
@Override
public void run() {
LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + instance);
}
}
测试如下:
/**
* @author sj
* @date 2022/12/8 15:08
*/
public class LazySimpleSingletonTest {
public static void main(String[] args) {
Thread thread = new Thread(new ExecutorThread());
Thread thread2 = new Thread(new ExecutorThread());
thread.start();
thread2.start();
System.out.println("结束");
}
}
我们来看看结果:
果然,上面的代码有一定概率出现两种不同结果,这意味着上面的单例存在线程安全隐患。我们通过调试运行再具体看一下。这里教大家一种新技能,用线程模式调试,手动控制线程的执行顺序来跟踪内存的变化。先给 ExecutorThread类打上断点,如下图所示:
使用鼠标右键单击断点,切换为 Thread 模式,如下图所示:
然后给 LazySimpleSingleton 类打上断点,同样标记为 Thread 模式,如下图所示:
切回客户端测试代码,同样也打上断点,同时改为 Thread 模式,如下图所示:
开始“Debug”之后,会看到 Debug 控制台可以自由切换 Thread 的运行状态,如下图所示:
通过不断切换线程,并观测其内存状态,我们发现在线程环境下 LazySimpleSingleton 被实例化了两次。有时我们得到的运行结果可能是相同的两个对象,实际上是被后面执行的线程覆盖了,我们看到了一个假象,线程安全隐患依旧存在。那么,我们如何来优化代码,使得懒汉式单例模式在线程环境下安全呢?来看下面的代码给 getlnstance0加上 synchronized 关键字,使这个方法变成线程同步方法
/**
* @author sj
* @date 2022/12/8 14:51
*/
public class LazySimpleSingleton {
private LazySimpleSingleton() {
}
private static LazySimpleSingleton LAZY_INSTANCE;
public static synchronized LazySimpleSingleton getInstance() {
if (LAZY_INSTANCE == null) {
LAZY_INSTANCE = new LazySimpleSingleton();
}
return LAZY_INSTANCE;
}
}
我们再来调试。当执行其中一个线程并调用 getlnstance0方法时,另一个线程在调用 getlnstancel方法,线程的状态由 RUNNING 变成了 MONITOR,出现阻塞。直到第一个线程执行完,第二个线程才恢复到 RUNNING 状态继续调用 getlnstance0方法,如下图所示:
上图完美地展现了 synchronized 监视锁的运行状态,线程安全的问题解决了。但是,用synchronized 加锁时,在线程数量比较多的情况下,如果 CPU分配压力上升,则会导致大批线程阻赛从而导致程序性能大幅下降。那么,有没有一种更好的方式,既能兼顾线程安全又能提升程序性能呢?答案是肯定的。我们来看双重检查锁的单例模式:
public class LazyDoubleSimpleSingleton {
private LazyDoubleSimpleSingleton() {
}
private static LazyDoubleSimpleSingleton LAZY_INSTANCE;
public static LazyDoubleSimpleSingleton getInstance() {
// 双重检查锁
if (LAZY_INSTANCE == null) {
synchronized (LazyDoubleSimpleSingleton.class) {
if (LAZY_INSTANCE == null) {
LAZY_INSTANCE = new LazyDoubleSimpleSingleton();
}
}
}
return LAZY_INSTANCE;
}
}
然后进行断点调式:
当第一个线程调用 getlnstance0 方法时,第二个线程也可以调用。当第一个线程执行到synchronized 时会上锁,第二个线程就会变成 MONITOR 状态,出现阻塞。此时,阻塞并不是基于整个LazySimpleSingleton 类的阻塞,而是在 getlnstance0方法内部的阻塞,只要逻辑不太复杂,对于调用者而言感知不到。
但是,用到 synchronized 关键字总归要上锁,对程序性能还是存在一定影响的。难道就真的没有更好的方案吗?当然有。我们可以从举初始化的角度来考虑,看下面的代码,采用静态内部类的方式:
public class LazyStaticInnerClassSingleton {
private LazyStaticInnerClassSingleton() {
}
public static LazyStaticInnerClassSingleton getInstance() {
return LazyHolder.LAZY_INSTANCE;
}
private static class LazyHolder {
public static final LazyStaticInnerClassSingleton LAZY_INSTANCE = new LazyStaticInnerClassSingleton();
}
}
这种方式兼顾了饿汉式单例模式的内存浪费问题和 synchronized 的性能问题。内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。由于这种方式比较简单,我们就不带大家一步一步调试了。但是,金无足赤,人无完人,单例模式亦如此。这种写法真的就完美了吗 ?
4.反射破坏单例
现在我们来看一个事故现场。大家有没有发现,上面介绍的单例模式的构造方法除了加上 private关键字,没有做任何处理。如果我们使用反射来调用其构造方法,再调用 getlnstance0方法,应该有两个不同的实例。现在来看一段测试代码,以 LazylnnerClassSingleton 为例:
import Lazy.LazyStaticInnerClassSingleton;
import java.lang.reflect.Constructor;
/**
* @author sj
* @date 2022/12/8 15:58
*/
public class LazyStaticInnerClassSingletonTest {
public static void main(String[] args) {
try {
Class<?> clazz = LazyStaticInnerClassSingleton.class;
Constructor<?> constructor = clazz.getDeclaredConstructor(null);
// 因为我们私有化了构造方法,所以需要暴力访问一波
constructor.setAccessible(true);
Object o = constructor.newInstance();
System.out.println("第一次" + o);
Object o2 = constructor.newInstance();
System.out.println("第二次" + o2);
System.out.println(o == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行测试一波,看结果:
不难看出,这里创造了两个不同的实例,全局唯一并没有保证。那怎么办呢?我们来做一次优化。现在,我们在其构造方法中做些限制,一旦出现多次重复创建,则直接抛出异常。来看优化后的代码:
/**
* @author sj
* @date 2022/12/8 15:47
*/
public class LazyStaticInnerClassSingleton {
private LazyStaticInnerClassSingleton() {
if (LazyHolder.LAZY_INSTANCE != null) {
throw new RuntimeException("不允许创造多个实例");
}
}
public static LazyStaticInnerClassSingleton getInstance() {
return LazyHolder.LAZY_INSTANCE;
}
private static class LazyHolder {
public static final LazyStaticInnerClassSingleton LAZY_INSTANCE = new LazyStaticInnerClassSingleton();
}
}
再次运行代码,结果如下:
是不是开始觉得非常牛逼了,反正我是这么想的。但是,上面看似完美的单例写法还是有方法破坏,接下来我们来看看怎么被破坏。
5.序列化破坏单例
一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码:
package seriable;
import java.io.Serializable;
/**
*
//序列化
//把内存中对象的状态转换为字节码的形式
//把字节码通过IO输出流,写到磁盘上,永久保存下来,持久化
//反序列化
//将持久化的字节码内容,通过IO输入流读到内存中来,转化成一个Java对象
*/
class SerializableSingleton implements Serializable {
public final static SerializableSingleton INSTANCE = new SerializableSingleton();
private SerializableSingleton() {
}
public static SerializableSingleton getInstance() {
return INSTANCE;
}
}
编写厕所代码:
import seriable.SerializableSingleton;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
/**
* @author sj
* @date 2022/12/8 16:22
*/
public class SerializableSingletonTest {
public static void main(String[] args) {
SerializableSingleton s1 = null;
SerializableSingleton s2 = SerializableSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SerializableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SerializableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SerializableSingleton)ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行看看结果:
从运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例模式的设计初衷。那么,我们如何保证在序列化的情况下也能够实现单例模式呢?其实很简单,只需要增加 readResolve0方法即可。来看优化后的代码:
package seriable;
import java.io.Serializable;
/**
*
//序列化
//把内存中对象的状态转换为字节码的形式
//把字节码通过IO输出流,写到磁盘上
//永久保存下来,持久化
//反序列化
//将持久化的字节码内容,通过IO输入流读到内存中来
//转化成一个Java对象
*/
public class SerializableSingleton implements Serializable {
public final static SerializableSingleton INSTANCE = new SerializableSingleton();
private SerializableSingleton() {
}
public static SerializableSingleton getInstance() {
return INSTANCE;
}
private Object readResolve() {
return INSTANCE;
}
}
再次运行:
哇哦一致了,是不是很神奇,到底是为什么勒,有兴趣的朋友可以去看看JKD的源码,进入 ObjectlnputStream 类的 readObject0方法,进行查看
6.注册式单例
注册式单例模式又称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。
1.枚举式单例
编写代码如下:
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
2.容器式单例
其实枚举式单例,虽然写法优雅,但是也会有一些问题。因为它在类加载之时就将所有的对象初始化放在类内存中,这其实和饿汉式并无差异,不适合大量创建单例对象的场景。那么,接下来看注册式单例模式的另一种写法,即容器式单例模式,创建 ContainerSingleton 类:
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className){
Object instance = null;
if(!ioc.containsKey(className)){
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
}catch (Exception e){
e.printStackTrace();
}
return instance;
}else{
return ioc.get(className);
}
}
}
容器式单例模式适用于需要大量创建单例对象的场景,便于管理。但它是非线程安全的。到此,注册式单例模式介绍完毕。
7.完结
学习永无止境,只有不断学习,才能不断进步,成为优秀的自己,师承咕泡汤姆!
大家可以想想容器式单例怎么保证线程安全哦!