设计模式之单例模式详解(java)
Posted 小样5411
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式之单例模式详解(java)相关的知识,希望对你有一定的参考价值。
目录
一、单例模式
单例模式的实现方式有许多种,重点是以下五种:饿汉式、懒汉式、双重校验锁(DCL懒汉式)、静态内部类,枚举
所属类型:创建型模式(new对象的作用)
单例模式特点:
a)只能有一个实例(getInstance获得的实例要一致)
b)必须自己创建自己的唯一实例(构造器私有,只能所在类自己创建)
c)必须给所有其他对象提供这一实例(public修饰getInstance方法)
1.1 饿汉式
饿汉式的意思就是一上来就加载,也就是随着类加载而加载,不管是否真的会使用。饿汉式中一个重要思想是构造器私有,单例一定要有构造器私有,构造器私有就是保证内存中只有一个对象,只能在本类Hungry中new,别处就无法new这个对象,这样就更为安全,有点像对属性的私有化、封装。
//饿汉式单例
public class Hungry
//一上来就会加载,但一直没有使用,可能会造成空间浪费
private byte[] data = new byte[1024*1024];
//构造器私有化(重点)
private Hungry()
private final static Hungry HUNGRY = new Hungry();
public Hungry getInstance()
return HUNGRY;
但这种单例存在一种问题,也是饿汉式的问题,一上来就加载,如果从始至终从未使用过这个实例,则会造成内存的浪费。
所以我们介绍懒汉式,懒汉式的意思就是用的时候再加载,不用就不加载,这样就不会造成内存的浪费。
注意:JVM中只有5种情况会对类进行初始化:
1、new一个关键字,如new String()
2、一个实例化对象,如new Student()
3-4、读取或设置一个静态字段(final修饰,已经编译初始化的常量除外,即final修饰的只会初始化加载一次)
5、调用一个类的静态方法
上面由于编译时读取到final关键字,于是就会随着类加载而加载
1.2 懒汉式
懒汉式就是使用时再调用getInstance()
获得实例对象,这就可以避免浪费
//懒汉式单例
public class LazyMan
//构造器私有化
private LazyMan()
private static LazyMan lazyMan;
public LazyMan getInstance()
if (lazyMan == null)
lazyMan = new LazyMan();
return lazyMan;
上面确实实现了懒汉式单例,但是存在线程不安全问题,单线程没问题,但多线程就会出现不安全问题,如下多线程并发执行
//懒汉式单例
public class LazyMan
//构造器私有化
private LazyMan()
System.out.println(Thread.currentThread().getName() + "->OK");
private static LazyMan lazyMan;
public static LazyMan getInstance()
if (lazyMan == null)
lazyMan = new LazyMan();
return lazyMan;
//多线程并发,获取同一个资源,即同时调用getInstance()
public static void main(String[] args)
for (int i = 0; i < 8; i++)
new Thread(()->
LazyMan.getInstance();
).start();
执行了四次每次结果都不一样,甚至有些线程都没有执行,没有获取到资源,每次结果不一样肯定是线程不安全的。
如何解决???可以加锁,用双重检验锁,也称DCL懒汉式,DCL全称(Double Check Lock)
1.3 DCL懒汉式(双重检验锁)
//懒汉式单例
public class LazyMan
//构造器私有化
private LazyMan()
System.out.println(Thread.currentThread().getName() + "->OK");
private static LazyMan lazyMan;
//双重检测锁(双重的意思的两个lazyMan == null,锁就是锁它的类 .class)
//简称DCL懒汉式
public static LazyMan getInstance()
//对LazyMan类进行加锁
if (lazyMan == null)
synchronized (LazyMan.class)
if (lazyMan == null)
lazyMan = new LazyMan();
return lazyMan;
//多线程并发,获取同一个资源,即同时调用getInstance()
public static void main(String[] args)
for (int i = 0; i < 8; i++)
new Thread(()->
LazyMan.getInstance();
).start();
然后运行,就只会一个获取到锁,怎么运行都是一个,每次结果都一样,加锁就保证了线程安全
但还存在问题,就是getInstance()方法中的lazyMan = new LazyMan();
不是原子性操作,new LazyMan()底层其实有三个操作
1、分配内存空间指令
2、执行构造方法,初始化对象指令
3、将对象引用指向这个分配的空间指令
我们知道程序在编译时会进行指令重排,可能会出现132, 231情况,虽然概率很小,但理论存在,为了防止指令重排可能带来的影响,我们就要加一个volatile关键字,完整的DCL懒汉式如下,也可以说是优化的DCL(Double Check Lock)
//懒汉式单例
public class LazyMan
//构造器私有化
private LazyMan()
System.out.println(Thread.currentThread().getName() + "->OK");
//禁止指令重排
private volatile static LazyMan lazyMan;
//双重检测锁(双重的意思的两个lazyMan == null,锁就是锁它的类 .class)
//简称DCL懒汉式
public static LazyMan getInstance()
//对LazyMan类进行加锁
if (lazyMan == null)
synchronized (LazyMan.class)
if (lazyMan == null)
lazyMan = new LazyMan();
return lazyMan;
//多线程并发,获取同一个资源,即同时调用getInstance()
public static void main(String[] args)
for (int i = 0; i < 8; i++)
new Thread(()->
LazyMan.getInstance();
).start();
1.4 静态内部类
也可以用静态内部类实现,静态内部类也是线程安全的,因为JVM中的类加载器会自动负责加锁,保证线程安全,之前已经说明了,JVM中只有5种情况会对类进行初始化
1、new一个关键字,如new String()
2、一个实例化对象,如new Student()
3-4、读取或设置一个静态字段(final修饰,已经编译初始化的常量除外,即final修饰的只会初始化加载一次)
5、调用一个类的静态方法
外部类加载时并不会立即加载内部类,因为不属于上面的5种情况,所以内部类不被加载则不去初始化实例,故而不占内存,只有调用了getInstance时JVM才会初始化。这也称为懒加载,不会随着外部类加载一并加载,只有调用静态内部类的时候才会加载,并且静态内部类不用担心指令重排问题,因为JVM初始化时能保证不会发生这个问题。
优势总结:相比DCL代码简洁,线程安全,相比饿汉式不会有空间浪费
public class Holder
//构造器私有化
private Holder()
//获取单例 public
public static Holder getInstance()
System.out.println(Thread.currentThread().getName() + "-> Ok");
return InnerClass.HOLDER;
//静态内部类
public static class InnerClass
//final修饰,不允许改变
private static final Holder HOLDER = new Holder();
1.5 枚举
为什么讲枚举???因为上面讲的虽然可以保证线程安全、不会浪费空间,但是可以用神器反射!!! 破解,因为反射连私有private关键字都可以获取,使得整个类都暴露出来
//懒汉式单例
public class LazyMan
//构造器私有化
private LazyMan()
System.out.println(Thread.currentThread().getName() + "->OK");
private volatile static LazyMan lazyMan;
//双重检测锁(双重的意思的两个lazyMan == null,锁就是锁它的类 .class)
//简称DCL懒汉式
public static LazyMan getInstance()
//对LazyMan类进行加锁
if (lazyMan == null)
synchronized (LazyMan.class)
if (lazyMan == null)
lazyMan = new LazyMan();
return lazyMan;
//反射破坏单例
public static void main(String[] args) throws Exception
//获取实例
LazyMan instance = LazyMan.getInstance();
//获取LazyMan的无参构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
//设置在使用构造器的时候不执行权限检查,从而不会进行设置的private权限
declaredConstructor.setAccessible(true);
//用反射new对象
LazyMan instance1 = declaredConstructor.newInstance();
//两个实例不一致,于是就破坏了单例
System.out.println(instance.hashCode());
System.out.println(instance1.hashCode());
System.out.println(instance == instance1);//fasle
执行结果
结论:反射能破坏单例,导致不是一个实例
所以引出枚举,枚举就不会被反射给破坏!!!
不太懂枚举,可以看看这个,一个很厉害的博主
面试官:为啥需要枚举?枚举有什么作用?怎么用枚举实现单例?
这篇文章讲的很明白,如为什么用枚举,用静态变量形式不行吗?给出如下回答,感兴趣可以仔细看看这篇好文,我们这里主要说,枚举实现的单例不会被反射破坏。
public enum EnumSingle
INSTANCE;
public EnumSingle getInstance()
return INSTANCE;
public static void main(String[] args)
EnumSingle instance = EnumSingle.INSTANCE;
EnumSingle instance1 = EnumSingle.INSTANCE;
System.out.println(instance == instance1);
执行结果true,说明用枚举获得的实例就是单例,枚举本身自带实现单例
我们再试试用反射创建实例
public static void main(String[] args) throws Exception
EnumSingle instance = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance1 = declaredConstructor.newInstance();//反射创建实例
System.out.println(instance == instance1);
会抛出异常,说明无法破坏
本文参考的视频讲解:https://www.bilibili.com/video/BV1K54y197iS
以上是关于设计模式之单例模式详解(java)的主要内容,如果未能解决你的问题,请参考以下文章