单例及单例模式概念
一个类只允许创建一个实例。这种设计模式就是单例模式

实现单例模式需要考虑什么?
- 构造函数私有,避免外部通过new来创建实例
- 考虑是否需要延时加载
- 考虑线程安全
单例模式的几种实现方式
1. 饿汉式:在类加载的时候,就已经初始化实例了。
public class Hungry {
// 一启动就被加载,还未使用就占用内存,造成资源浪费
private static final Hungry INSTANCE = new Hungry();
// 构造方法私有,外部无法进行实例化
private Hungry() {}
// 获取实例对象
public static Hungry getInstance() {
return INSTANCE;
}
}
2. 懒汉式:第一次使用时才进行实例化
public class Lasy {
// 启动时不加载,需要时再进行实例化
private static Lasy INSTANCE;
// 构造方法私有,外部无法进行实例化
private Lasy() {}
public static Lasy getInstance() {
if (null == INSTANCE) {
INSTANCE = new Lasy();
}
return INSTANCE;
}
}
但是懒汉式并不能保证单例,在多线程并发情况下,会多次调用构造方法进行实例化,示例如下:
public class Lasy {
// 启动时不加载,需要时再进行实例化
private static Lasy INSTANCE;
// 构造方法私有,外部无法进行实例化
private Lasy() {
System.out.println(Thread.currentThread().getName());
}
public static Lasy getInstance() {
if (null == INSTANCE) {
INSTANCE = new Lasy();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(Lasy::getInstance).start();
}
}
}
输出结果:
2.1 使用同步锁synchronized
//如果使用synchronized关键字,这种方式在并发下并发量等同于1,如果频繁使用该对象,会大大降低性能
public static synchronized Lasy getInstance() {
if (null == INSTANCE) {
INSTANCE = new Lasy();
}
return INSTANCE;
}
2.2 双重检测(DCL懒汉式单例)
public static Lasy getInstance() {
if (null == INSTANCE) {
synchronized (Lasy.class) {
if (null == INSTANCE) {
INSTANCE = new Lasy();
}
}
}
return INSTANCE;
}
// INSTANCE = new Lasy();
/** 以上操作并不是原子性操作,包括以下三个步骤:
* 1. 分配内存空间
* 2. 执行构造方法进行初始化
* 3. 将对象指向这个空间
*/
/** 在多线程情况下可能出现指令重排,每个线程执行INSTANCE = new Lasy()这个操作的顺序可能不一致,那么就有可能出现线程A先执行了第一步和第三步,此时还未进行初始化,线程B执行判断语句null == INSTANCE时为false,将直接返回线程A未进行初始化的INSTANCE对象,所以要避免指令重排,使用volatile关键字。*/
private static volatile Lasy INSTANCE;
public static Lasy getInstance() {
if (null == INSTANCE) {
// 对象未被实例化,等待类对象锁
synchronized (Lasy.class) {
// 进入对象锁,其他线程等待,再次判断是否被实例化
if (null == INSTANCE) {
INSTANCE = new Lasy();
}
}
}
return INSTANCE;
}
饿汉式和懒汉式比较:
①由于饿汉式是在类加载的时候进行实例化,线程安全;懒汉式是在第一次使用的时候进行实例化,此时可能多线程会同时进行实例化,所以线程不安全;
②饿汉式不支持延时加载,如果占用资源较多或者耗时较长,可能会影响启动时间;懒汉式支持延时加载,但是需要使用同步锁,如果这个类被频繁使用,可能导致性能问题。(相比较更加倾向于饿汉式单例,在启动时进行加载,如果遇到性能或者资源问题会提前暴露,相较于正在运行中出现问题更加可控)
3. 静态内部类
public class Test {
private Test() {}
private static class InnerTest{
// final修饰变量是不可变的,初始化对象之后便不会再指向另一个对象
private static final Test INSTANCE = new Test();
}
public static Test getInstance() {
return InnerTest.INSTANCE;
}
}
但是在反射机制下,所有的私有方法都能被破解,如下实例表示通过静态内部类获得的实例对象和通过反射获得的holder实例对象并不是同一个,仍然破坏了单例
public static void main(String[] args) throws Exception {
// 获取private的构造方法
Constructor<Test> constructor = Test.class.getDeclaredConstructor(null);
// 无视私有构造器
constructor.setAccessible(true);
Test test = constructor.newInstance();
System.out.println(Test.getInstance().equals(test));
}
输出结果:false
通过反射的源码可以看到,无法反射式创建枚举对象,所以我们可以考虑使用枚举来实现单例
4. 枚举
public enum SingleEnum {
INSTANCE;
}
以下示例可见确实无法反射式创建枚举对象,当使用反射创建枚举对象时,会抛出异常信息Cannot reflectively create enum objects
public static void main(String[] args) throws Exception {
// 获取private的构造方法(实际上枚举类的构造方法带有两个参数,并不是我们从源码看到的无参构造,需要使用源码工具来查看)
Constructor<SingleEnum> constructor = SingleEnum.class.getDeclaredConstructor(String.class, int.class);
// 无视私有构造器
constructor.setAccessible(true);
SingleEnum single = constructor.newInstance();
System.out.println(SingleEnum.INSTANCE.equals(single));
}
输出: