单例模式
什么是单例模式?
单例模式(Singleton Pattern)用于确保某个类在整个应用程序中只有一个实例,并提供一个全局访问点来获取该实例。
优缺点
- 优点
- 单例模式可以保证内存里只有一个实例,减少了内存的开销。
- 可以避免对资源的多重占用。
- 单例模式设置全局访问点,可以优化和共享资源的访问。
- 缺点
- 单例模式一般没有接口,扩展困难。
- 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则
单例模式实现方式
通常有如下5种写法:
- 饿汉式 ——>Hungry
@Data
public class Hungry implements Serializable {
private static final long serialVersionUID = 1L;
private static boolean instanceCreated = false;
private Hungry(){
/*//防止单例被破坏
if (instanceCreated) {
throw new RuntimeException("请使用 Hungry.getInstance() 方法获取一个单例实例");
}
instanceCreated = true;
*/
System.out.println(Thread.currentThread().getName() + " is creating an instance.");
}
private static final Hungry hungry = new Hungry();
/**
* @Author: javafa
* @Description: 在饿汉式单例模式中,单例实例在类加载时就被创建,并且只会创建一次
* 优点:
* 1、简单直观,易于实现。
* 2、线程安全,因为实例在类加载时就已经创建。
*
* 缺点:
* 1、可能会造成资源浪费,因为实例在类加载时就创建了,即使从未使用它。
* 基于此缺点,引入了懒汉式单例模式
*
* @Date: 2024/7/9 13:37
* @Param:
* @return: com.fivemillion.algorithm.designpatterns.singleton.Hungry
* @see SingletonTest#useHungryTest()
**/
public static Hungry getInstance(){
return hungry;
}
public Object readResolve() throws ObjectStreamException {
return hungry;
}
private int id;
private String name;
private int age;
private String address;
}
- 懒汉式 ——>LazyMan
/**
* @Author: javafa
* @Date: 2024/7/9 13:47
* @Description: 懒汉式 单例模式
*/
public class LazyMan {
private static LazyMan lazyMan;
public LazyMan(){
System.out.println(Thread.currentThread().getName());
}
/**
* @Author: javafa
* @Description: 懒汉式 单例模式
* 优点:
* 第一次调用才初始化,避免内存浪费。
* 缺点:
* 多线程调用时,并不能保证提供唯一实例,通过无参构造发现,会多次创建实例
* 基于此缺点,添加双重检验锁进行完善
* @Date: 2024/7/9 13:52
* @Param:
* @return: com.fivemillion.algorithm.designpatterns.singleton.LazyMan
* @see SingletonTest#useLayManTest()
**/
public static LazyMan getInstance(){
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}
}
- 双重校验锁 ——>LazyDoubleCheckMan,LazyDoubleCheckImproveMan
public class LazyDoubleCheckMan {
private static LazyDoubleCheckMan lazyMan;
public LazyDoubleCheckMan(){
System.out.println(Thread.currentThread().getName());
}
/**
* @Author: javafa
* @Description: 懒汉式-双重检验锁(double check lock)(DCL) 单例模式
* 优点:
* 懒汉模式的升级版,保证全局唯一实例
* 缺点:
* 由于存在cpu指令重排,可能导致创建的对象为null被返回
* 基于此缺点需要使用volatile 进行优化
*
* @Date: 2024/7/9 13:52
* @Param:
* @return: com.fivemillion.algorithm.designpatterns.singleton.LazyMan
* @see SingletonTest#useLayManTest()
**/
public static LazyDoubleCheckMan getInstance(){
if (lazyMan == null) { //第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
synchronized(LazyDoubleCheckMan.class){//第一层锁,保证只有一个线程进入
//双重检查,防止多个线程同时进入第一层检查(因单例模式只允许存在一个对象,故在创建对象之前无引用指向对象,所有线程均可进入第一层检查)
//当某一线程获得锁创建一个LazyMan对象时,即已有引用指向对象,lazyMan不为空,从而保证只会创建一个对象
//假设没有第二层检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
if (lazyMan == null) {//第二层检查
lazyMan = new LazyDoubleCheckMan();//这行代码存在的问题,不能保证原子性
//对象的创建并不是一个简单的原子操作,而是由多个步骤组成:(1)在堆上开辟空间;(2)属性初始化;(3)引用指向对象。
//由于 JVM 的指令重排优化,步骤 2 和步骤 3 可能会被重排,从而导致另外一个线程在步骤 3 完成而步骤 2 未完成时,看到一个不完整的对象
//假设以上三个内容为三条单独指令,因指令重排可能会导致执行顺序为1->3->2(正常为1->2->3),
// 当单例模式中存在普通变量需要在构造方法中进行初始化操作时,单线程情况下,顺序重排没有影响;
// 但在多线程情况下,假如线程1执行lazyMan = new LazyMan()语句时先1再3,由于系统调度线程2的原因没来得及执行步骤2,
// 但此时已有引用指向对象也就是lazyMan!=null,故线程2在第一次检查时不满足条件直接返回lazyMan,但此时lazyMan为null
}
}
}
return lazyMan;
}
}
双重校验锁改进版本
public class LazyDoubleCheckImproveMan {
//使用 volatile 修饰
//volatile作用:保证有序性、可见性。
private static volatile LazyDoubleCheckImproveMan lazyMan;
public LazyDoubleCheckImproveMan(){
System.out.println(Thread.currentThread().getName());
}
/**
* @Author: javafa
* @Description: 懒汉式-双重检验锁(double check lock)(DCL) 单例模式
* 优点:
* 懒汉模式的升级版,保证全局唯一实例,且使用volatite修饰,禁止了cpu指令重排,保证了多线程安全
* 缺点:
* volatile 会强制cpu即使把修改的值立即被更新到主存,且使用synchronized同步加锁,性能较低,基于此缺点使用静态内部内的单例模式,也是最为推荐的一种单例模式
*
* @Date: 2024/7/9 13:52
* @Param:
* @return: com.fivemillion.algorithm.designpatterns.singleton.LazyMan
* @see SingletonTest#useLayManTest()
**/
public static LazyDoubleCheckImproveMan getInstance(){
if (lazyMan == null) { //第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
synchronized(LazyDoubleCheckImproveMan.class){//第一层锁,保证只有一个线程进入
//双重检查,防止多个线程同时进入第一层检查(因单例模式只允许存在一个对象,故在创建对象之前无引用指向对象,所有线程均可进入第一层检查)
//当某一线程获得锁创建一个LazyMan对象时,即已有引用指向对象,lazyMan不为空,从而保证只会创建一个对象
//假设没有第二层检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
if (lazyMan == null) {//第二层检查
lazyMan = new LazyDoubleCheckImproveMan();
//对象的创建并不是一个简单的原子操作,而是由多个步骤组成:(1)在堆上开辟空间;(2)属性初始化;(3)引用指向对象。
//由于 JVM 的指令重排优化,步骤 2 和步骤 3 可能会被重排,从而导致另外一个线程在步骤 3 完成而步骤 2 未完成时,看到一个不完整的对象
//由于lazyMan变量声明为 volatile,就指示 JVM,修改的值立即被更新到主存,使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行
}
}
}
return lazyMan;
}
}
- 静态内部类(推荐) ——>StaticInner
public class StaticInner {
private StaticInner(){
System.out.println(Thread.currentThread().getName());
}
public static class InnerSingleton{
private static StaticInner staticInner = new StaticInner();
}
/**
* @Author: javafa
* @Description:
* 优点:
* 支持多线程,是线程安全的,由于jvm的classloder机制会确保在加载内部类时,只会有一个线程能够初始化 staticInner,从而保证了单例的线程安全性
* 支持懒加载,静态内部类的实例 staticInner 只有在 getInstance() 方法首次被调用时才会被初始化。这种懒加载的方式确保了单例实例在首次使用时才被创建
* 性能高,代码简洁,没有使用synchronized 同步锁
*
* @Date: 2024/7/9 16:19
* @Param:
* @return: com.fivemillion.algorithm.designpatterns.singleton.StaticInner
* @see SingletonTest#useStaticInnerTest()
**/
public static StaticInner getInstance(){
return InnerSingleton.staticInner;
}
}
- 枚举(推荐) ——>Student
@Data
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
// 私有构造方法,防止外部实例化
private Student() {
System.out.println(Thread.currentThread().getName());
}
private int id;
private String name;
private int age;
private String address;
private String gradeNo;
private int result;
/**
* 枚举类型是线程安全的,并且只会装载一次
*/
private enum SingletonStudent{
INSTANCE;
private final Student student;
SingletonStudent(){
student = new Student();
}
private Student getInstance(){
return student;
}
}
public static Student getInstance(){
return SingletonStudent.INSTANCE.getInstance();
}
/**
* @Author: javafa
* @Description:
* 如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例
* 所以要么不实现序列化接口,
* 如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象
* @Date: 2024/7/10 11:08
* @Param:
* @return: java.lang.Object
* @see SerializationTest#serializaStudentTest()
**/
private Object readResolve() {
return SingletonStudent.INSTANCE.getInstance();
}
}
破坏单例模式的方法及解决办法
- 除枚举方式外, 其他方法都会通过
反射
的方式破坏单例,反射是通过调用构造方法生成新的对象
- 反射破坏代码示例
public class ReflectionTest {
@Test
public void reflectStudentTest(){
try {
Class<?> clazz = Class.forName("Student$SingletonStudent");
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance("INSTANCE", 0);
} catch (Exception e) {
e.printStackTrace();
}
}
@Test
public void reflectHungryTest() throws Exception {
// 获取类的显式构造器
Constructor<Hungry> constructor = Hungry.class.getDeclaredConstructor();
// 将访问权限设为 true,从而可以访问类的私有构造器
constructor.setAccessible(true);
// 利用反射构造一个新对象
Hungry instance1 = constructor.newInstance();
// 再通过正常的单例模式获取单例对象
Hungry instance2 = Hungry.getInstance();
// 比较两个对象是不是同一个对象
System.out.println(instance1 == instance2); // 打印结果为 false
System.out.println(instance1.hashCode() == instance2.hashCode());// 打印结果为true
//hashCode()方法返回的哈希码值理论上应该能够唯一地标识一个对象,
// 但实际中可能会发生哈希冲突,即两个不同的对象可能具有相同的哈希码值,当测试为instance1和instance2同时赋予属性值时
//会看到产生了不同的hashCode
}
}
- 解决反射破坏方法
private static boolean instanceCreated = false;
private Hungry(){
//在无惨构造中增加状态,当生成实例后进行标记,当再次进行实例化时,抛出异常,防止单例被破坏
if (instanceCreated) {
throw new RuntimeException("请使用 Hungry.getInstance() 方法获取一个单例实例");
}
instanceCreated = true;
System.out.println(Thread.currentThread().getName() + " is creating an instance.");
}
- 序列化破坏单例
如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,
可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象
- 序列化破坏单例
public class SerializationTest {
@Test
public void serializaStudentTest() throws Exception {
Student student1 = Student.getInstance();
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.ser"));
oos.writeObject(student1);
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.ser"));
Student student2 = (Student) ois.readObject();
ois.close();
System.out.println(student1 == student2); // 输出: true
}
@Test
public void serializaHungryTest() throws Exception {
Hungry hungry1 = Hungry.getInstance();
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hungry.ser"));
oos.writeObject(hungry1);
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hungry.ser"));
Hungry hungry2 = (Hungry) ois.readObject();
ois.close();
//当Hungry单例中没有重写readResolve方法时,student1 == student2输出的对象地址为fasle,表明生成了不同的实例对象
System.out.println(hungry1 == hungry2); // 输出: true
}
}
- 解决序列化破坏单例
public Object readResolve() throws ObjectStreamException {
return hungry;
}
单例测试调用示例
public class SingletonTest {
/**
* @Author: javafa
* @Description: 饿汉式调用测试
* @Date: 2024/7/9 11:25
* @Param:
* @return: void
**/
@Test
public void useHungryTest() {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//通过无参构造中输出的线程名判断,在单例模式下内存中应该只有全局唯一的对象实例,即当第一个线程会调用无参构造,完成类的实例化,
//其余线程将直接获得由单例创模式创建的实例对象,即只打印一个线程名
Hungry.getInstance();
}).start();
}
}
/**
* @Author: javafa
* @Description: 懒汉式调用测试
* @Date: 2024/7/9 13:52
* @Param:
* @return: void
**/
@Test
public void useLayManTest() {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//通过无参构造中输出的线程名判断,在单例模式下内存中应该只有全局唯一的对象实例,即当第一个线程会调用无参构造,完成类的实例化,
//其余线程将直接获得由单例创模式创建的实例对象,即只打印一个线程名
LazyMan.getInstance();
}).start();
}
}
/**
* @Author: javafa
* @Description: 懒汉式-双重检验锁(double check lock)(DCL)
* @Date: 2024/7/9 13:52
* @Param:
* @return: void
**/
@Test
public void useLazyDoubleCheckManTest() {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//通过无参构造中输出的线程名判断,在单例模式下内存中应该只有全局唯一的对象实例,即当第一个线程会调用无参构造,完成类的实例化,
//其余线程将直接获得由单例创模式创建的实例对象,即只打印一个线程名
LazyDoubleCheckMan.getInstance();
}).start();
}
}
/**
* @Author: javafa
* @Description: 懒汉式-双重检验锁(double check lock)(DCL)-完善版
* @Date: 2024/7/9 13:52
* @Param:
* @return: void
**/
@Test
public void useLazyDoubleCheckImproveManTest() {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//通过无参构造中输出的线程名判断,在单例模式下内存中应该只有全局唯一的对象实例,即当第一个线程会调用无参构造,完成类的实例化,
//其余线程将直接获得由单例创模式创建的实例对象,即只打印一个线程名
LazyDoubleCheckImproveMan.getInstance();
}).start();
}
}
/**
* @Author: javafa
* @Description: 静态内部内 单例模式 (推荐使用)
* @Date: 2024/7/9 13:52
* @Param:
* @return: void
**/
@Test
public void useStaticInnerTest() {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//通过无参构造中输出的线程名判断,在单例模式下内存中应该只有全局唯一的对象实例,即当第一个线程会调用无参构造,完成类的实例化,
//其余线程将直接获得由单例创模式创建的实例对象,即只打印一个线程名
StaticInner.getInstance();
}).start();
}
}
/**
* @Author: javafa
* @Description: 枚举 单例模式 (推荐使用)
* @Date: 2024/7/9 13:52
* @Param:
* @return: void
**/
@Test
public void useStudentTest() {
// for (int i = 0; i < 10; i++) {
// new Thread(() -> {
//通过无参构造中输出的线程名判断,在单例模式下内存中应该只有全局唯一的对象实例,即当第一个线程会调用无参构造,完成类的实例化,
//其余线程将直接获得由单例创模式创建的实例对象,即只打印一个线程名
// Student.getInstance();
// }).start();
// }
// 获取单例实例
Student student1 = Student.getInstance();
Student student2 = Student.getInstance();
// 操作Student对象
System.out.println(student1 == student2); // 输出: true,证明是同一个实例
}
}