点击 ​Mr.绵羊的知识星球​ 解锁更多优质文章。

一、介绍

1. 简介

    一个类只能实例化一个对象。

2. 特点

    (1) 在Java程序中,一个类有且仅有一个实例化的对象。

    (2) 单例类必须自己创建自己的唯一实例。

    (3) 单例类必须给所有其他对象提供这一实例。

3. 优点

    (1)减少了内存的占用。

    (2) 优化对共享资源的访问,避免对共享资源的多重占用。

    (3) 避免了频繁创建和回收实例的消耗。

4. 缺点

    (1) 单例类的扩展有很大的困难。

    (2) 成员变量线程不安全。

    如果真的有必要使用单例模式,共享变量一般都是通过加锁或使用JUC中的类来解决线程安全问题。

二、使用

    本文主要围绕单例模式中的饿汉式和懒汉式进行使用描述。

1. 饿汉式

    类被加载的时候就创建了该实例。(不了解类的加载可以看这篇文章:​​类的加载​​)

import java.util.concurrent.atomic.AtomicInteger;

/**
* 单例设计模式-饿汉式
*
* @author wxy
* @date 2023-03-02
*/
public class StarvedManMode {
/**
* 类加载时会自动加载常量或成员变量, 所以随着类的加载, 会创建该实例。
*/
private static final StarvedManMode STARVED_MAN_MODE = new StarvedManMode();

/**
* 由于全局仅有一个实例, 避免操作共享变量出现线程安全问题, 所以使用原子类。
*/
private AtomicInteger count;

private StarvedManMode() {
// 必须私有化构造,避免外部直接创建实例(不考虑反射创建对象实例)
}

/**
* 获取实例
*
* @return StarvedManMode实例
*/
public static StarvedManMode getInstance() {
return STARVED_MAN_MODE;
}
}

2. 懒汉式

    使用时才创建该实例。(该创建方式使用了volatile关键字,不了解可以看这篇文章: ​​volatile详解​​)

import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;

/**
* 单例设计模式-懒汉式
*
* @author wxy
* @date 2023-03-02
*/
public class LazyManMode {
/**
* 需要使用volatile修饰, 避免指令重排。
*/
private static volatile LazyManMode lazyManMode = null;

/**
* 由于全局仅有一个实例, 避免操作共享变量出现线程安全问题, 所以使用原子类。
*/
private AtomicInteger count;

private LazyManMode() {
// 必须私有化构造,避免外部直接创建实例(不考虑反射创建对象实例)
}

/**
* 获取实例
*
* @return LazyManMode实例
*/
public static LazyManMode getInstance() {
// DCL双端检锁, 如果判断少一层或者不加锁, 多线程下创建对象可能不是单例的
if (Objects.isNull(lazyManMode)) {
synchronized (LazyManMode.class) {
if (Objects.isNull(lazyManMode)) {
lazyManMode = new LazyManMode();
}
}
}

return lazyManMode;
}
}

3. 验证多线程创建单实例

/**
* 单例设计模式-懒汉式
*
* @author wxy
* @date 2023-03-02
*/
public class DesignPatternTest {
public static void main(String[] args) {
for (int count = 0; count < 5; count++) {
new Thread(() -> {
for (int index = 0; index < 5; index++) {
// 多线程中创建并获取懒汉式实例
LazyManMode lazyManModeInstance = LazyManMode.getInstance();
/*
不重写toString方法, 打印实例地址, 打印结果:
懒汉式实例地址: com.***.LazyManMode@4e9c993a
懒汉式实例地址: com.***.LazyManMode@4e9c993a
...
*/
System.out.println("懒汉式实例地址: " + lazyManModeInstance);

/*-----------------分割线------------------*/

// 多线程中创建并获取饿汉式实例
StarvedManMode starvedManModeInstance = StarvedManMode.getInstance();
/*
不重写toString方法, 打印实例地址, 打印结果:
饿汉式实例地址: com.***.StarvedManMode@405b7e99
饿汉式实例地址: com.***.StarvedManMode@405b7e99
...
*/
System.out.println("饿汉式实例地址: " + starvedManModeInstance);
}
}).start();
}
}
}

4. 饿汉式和懒汉式应用场景

    当系统对内存使用要求比较高时或者实例中没有共享变量可以使用单例模式,如果实例中有共享变量,使用时要保证其线程安全。

    (1) 如果这个实例和实例中方法经常被使用,建议用饿汉式。

    (2) 如果这个实例和实例中的方法就是为了偶然突发状况用那么一次,建议使用懒汉式。