Java 设计模式 -- 单例模式的实现(饿汉式枚举饿汉式懒汉式双检锁懒汉式内部类懒汉式)jdk 中用到单例模式的场景DCL实现单例需用volatile 修饰静态变量
Posted CodeJiao
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 设计模式 -- 单例模式的实现(饿汉式枚举饿汉式懒汉式双检锁懒汉式内部类懒汉式)jdk 中用到单例模式的场景DCL实现单例需用volatile 修饰静态变量相关的知识,希望对你有一定的参考价值。
文章目录
1. 单例模式
要求
- 掌握五种单例模式的实现方式
- 理解为何 DCL(Double Check Lock, 双检锁机制) 实现单例时要使用 volatile 修饰静态变量
- 了解 jdk 中用到单例的场景
1.1 何为单例模式
- 单例模式保证java应用程序中,一个类Class只有一个实例在,使用单例模式好处在于可以节省内存,节约资源,对于一般频繁创建和销毁对象的可以使用单例模式。
- 因为它限制了实例的个数,有利于java垃圾回收。好的单例模式也能提高性能。例如:数据库连接池、httpclient连接单例。
- 对于系统中的某些类来说,只有一个实例很重要,Windows中就只能打开一个任务管理器。
1.2 单例模式的实现(饿汉式 & 懒汉式)
单例设计模式分类两种:
- 饿汉式:类加载就会导致该单实例对象被创建。
- 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建。
2. 单例模式的实现
2.1 饿汉式
public class Singleton1 implements Serializable
private Singleton1()
if (INSTANCE != null)
throw new RuntimeException("单例对象不能重复创建");
private static final Singleton1 INSTANCE = new Singleton1();
public static Singleton1 getInstance()
return INSTANCE;
public Singleton1 readResolve()
return INSTANCE;
- 构造方法抛出异常是防止反射破坏单例
readResolve()
是防止反序列化破坏单例
2.2 枚举饿汉式
public enum Singleton2
INSTANCE;
private Singleton2()
public static Singleton2 getInstance()
return INSTANCE;
- 枚举饿汉式能天然防止反射、反序列化破坏单例
2.3 懒汉式
public class Singleton3 implements Serializable
private Singleton3()
private static Singleton3 INSTANCE = null;
// 锁住的是 Singleton3.class 字节码对象
public static synchronized Singleton3 getInstance()
if (INSTANCE == null)
INSTANCE = new Singleton3();
return INSTANCE;
- 其实只有首次创建单例对象时才需要同步,但该代码实际上每次调用都会同步。因此有了下面的双检锁改进。
2.4 双检锁懒汉式 DCL(Double Check Lock, 双检锁机制)
public class Singleton4 implements Serializable
private Singleton4()
private static volatile Singleton4 INSTANCE = null; // 可见性,有序性
public static Singleton4 getInstance()
if (INSTANCE == null)
synchronized (Singleton4.class)
if (INSTANCE == null)
INSTANCE = new Singleton4();
return INSTANCE;
2.4.1 为何必须加 volatile
INSTANCE = new Singleton4()
不是原子的,分成 3 步:创建对象、调用构造、给静态变量赋值。
其中后两步可能被指令重排序优化,变成先赋值、再调用构造。- 如果线程1 先执行了赋值,线程2 执行到第一个
INSTANCE == null
时发现 INSTANCE 已经不为 null,此时就会返回一个未完全构造的对象。
2.4.2 说明:内部第二次判断 INSTANCE == null 的原因
if (INSTANCE == null)
synchronized (Singleton4.class)
if (INSTANCE == null)
INSTANCE = new Singleton4();
-
刚开始INSTANCE为NULL,线程1和线程2都打算去竞争synchronized锁。
-
假如现在是线程2竞争锁成功。
-
线程2执行了赋值语句,成功解锁,这时线程1得到了synchronized锁。
-
然后线程1成功把线程2赋值的值给覆盖了。
2.4.3 volatile指令的作用
volatile可以保证共享变量的可见性和有序性。
保证可见性:
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public void actor2(I_Result r)
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
- 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
public void actor1(I_Result r)
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready)
r.r1 = num + num;
else
r.r1 = 1;
保证有序性:
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
public void actor2(I_Result r)
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
public void actor1(I_Result r)
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready)
r.r1 = num + num;
else
r.r1 = 1;
volatile 不能解决指令交错:
- 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
- 而有序性的保证也只是保证了本线程内相关代码不被重排序
2.5 内部类懒汉式
public class Singleton5 implements Serializable
private Singleton5()
private static class Holder
static Singleton5 INSTANCE = new Singleton5();
public static Singleton5 getInstance()
return Holder.INSTANCE;
- 避免了双检锁的缺点(加了static关键字的代码可以理解为静态代码块的代码,而静态代码块的代码不需要去考虑线程安全,JVM在处理静态代码块时是线程安全的)
3. jdk 中用到单例模式的场景
使用单例模式大部分都是在别的库里面使用了单例模式,你的项目中不要乱用单例模式,很容易用错。所以面试官如果问你在哪里用到了单例模式,最好不要说是在项目当中用到了单例模式。我们可以去列举 jdk 中用到单例模式的场景。因为jdk是写好的成熟的库,就算真的有一点小的问题,面试官也挑不出什么毛病。
-
Runtime 体现了饿汉式单例
我们可以看出,System类的exit方法和gc方法都是调用的Runtime类的方法。
-
Console 体现了双检锁懒汉式单例
4. 反序列化破坏单例和反射破坏单例代码实现
private static void serializable(Object instance) throws IOException, ClassNotFoundException
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(instance);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
System.out.println("反序列化创建实例:" + ois.readObject());
private static void reflection(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
System.out.println("反射创建实例:" + constructor.newInstance());
以上是关于Java 设计模式 -- 单例模式的实现(饿汉式枚举饿汉式懒汉式双检锁懒汉式内部类懒汉式)jdk 中用到单例模式的场景DCL实现单例需用volatile 修饰静态变量的主要内容,如果未能解决你的问题,请参考以下文章