【面试:基础篇09:单例模式全总结】
01.简介
单例模式是指 一个类控制它只有一个实例,实现单例模式有五种方式:饿汉式、枚举饿汉式、懒汉式(无优化)、懒汉式(双检锁优化)、懒汉式(内部类优化)
02.饿汉式单例:
特点:
饿汉式实现单例的一种方式,它的特点是 初始化时 静态成员变量就被单例模式的唯一实例赋值,饿汉式是线程安全的
代码:
class Singleton1 implements Serializable{
private Singleton1(){
System.out.println("饿汉式 初始化");
}
private static final Singleton1 single = new Singleton1();
public static Singleton1 getInstance(){
return single;
}
public static void other(){
System.out.println("饿汉式 调用其他方法也会初始化实例");
}
}
public class danli {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
Singleton1.other();// 调用other方法时就已经创建好了单例对象
System.out.println("==================================");
System.out.println(Singleton1.getInstance());
System.out.println(Singleton1.getInstance());
}
}
结果
可以看出饿汉式 只会创建一个实例,且在调用其他类之前就完成了构造函数初始化
02.1反射破坏单例的代码优化
优化方式:
在构造函数里加一个判断,如果没有创造过单例对象则可以创建 否则抛出异常
class Singleton1 implements Serializable{
private Singleton1(){
// 一:无优化时
/*if (single!=null){ // 避免反射暴力破坏单例模式
throw new RuntimeException("单例对象不能重复创建");
}*/
// 二:优化后
if (single!=null){ // 避免反射暴力破坏单例模式
throw new RuntimeException("单例对象不能重复创建");
}
System.out.println("饿汉式 初始化");
}
private static final Singleton1 single = new Singleton1();
public static Singleton1 getInstance(){
return single;
}
public static void other(){
System.out.println("饿汉式 调用其他方法也会初始化实例");
}
}
public class danli {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
Singleton1.other();// 调用other方法时就已经创建好了单例对象
System.out.println("==================================");
System.out.println(Singleton1.getInstance());
System.out.println(Singleton1.getInstance());
// 反射破坏单例
reflection(Singleton1.class);
}
// 反射获取实例代码
private static void reflection(Class<?> clazz) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<?> constructor = clazz.getDeclaredConstructor();// 获取构造函数
constructor.setAccessible(true);// true表示可以使用私有构造
System.out.println("反射创建实例:"+constructor.newInstance());// 实例又被创建了 破坏单例
}
}
结果
无优化:
优化:
可以看出无优化时 反射创建了一个不同的实例 成功破坏了单例模式,在我们优化后成功的抛出了异常。
02.2反序列化破坏单例的代码优化
优化方式:
在单例模式中添加readResolve()方法
// 饿汉式
class Singleton1 implements Serializable{
private Singleton1(){
System.out.println("饿汉式 初始化");
}
private static final Singleton1 single = new Singleton1();
public static Singleton1 getInstance(){
return single;
}
public static void other(){
System.out.println("饿汉式 调用其他方法也会初始化实例");
}
// 反序列化无优化时
/*public Object readResolve(){ // 避免反序列化破坏单例模式
return single;
}*/
// 反序列化优化后
public Object readResolve(){ // 避免反序列化破坏单例模式
return single;
}
}
public class danli {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
Singleton1.other();// 调用other方法时就已经创建好了单例对象
System.out.println("==================================");
System.out.println(Singleton1.getInstance());
System.out.println(Singleton1.getInstance());
// 反序列化破坏单例
serializable(Singleton1.getInstance());
}
// 反序列化获取实例代码
private static void serializable(Object instance) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();// 创建字节数组输出流
ObjectOutputStream oos = new ObjectOutputStream(bos);// 放入ObjectOutputStream管道
oos.writeObject(instance);// 把instance写入字节流
// 把字节流重新读取出来 还原了新的对象,不会走构造方法
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
System.out.println("反序列化创建实例:"+ois.readObject());
}
}
无优化:
优化后:
可以看出无优化时 反序列化创建了一个不同的实例 成功破坏了单例模式,在我们优化后成功避免了这个问题。
02.3 Unsafe破坏单例的代码(不能优化)
// 饿汉式
class Singleton1 implements Serializable{
private Singleton1(){
System.out.println("饿汉式 初始化");
}
private static final Singleton1 single = new Singleton1();
public static Singleton1 getInstance(){
return single;
}
public static void other(){
System.out.println("饿汉式 调用其他方法也会初始化实例");
}
}
public class danli {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
Singleton1.other();// 调用other方法时就已经创建好了单例对象
System.out.println("==================================");
System.out.println(Singleton1.getInstance());
System.out.println(Singleton1.getInstance());
// Unsafe 破坏单例,无法预防
unsafe(Singleton1.class);
}
private static void unsafe(Class<?> clazz) throws InstantiationException {
Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);// 可以根据类型创建类型实例,不会走构造方法
System.out.println("Unsafe 创建实例:"+o);
}
}
结果
可以看出利用unsafe方法也可以另外创建单例实例,且这种方式无法避免。
03枚举类实现饿汉式
介绍:
枚举类是由单例实现的,并且枚举类已经天然防范了 反射破坏单例以及反序列化破坏单例
代码
// 枚举类实现饿汉式
enum Singleton2{
single;// 唯一的实例,之后的代码可以不要 主要是验证它确实是单例
Singleton2(){// 构造函数默认是private Singleton2,所以private可以不加
System.out.println("enum实现饿汉式 初始化");
}
@Override
public String toString(){
return getClass().getName()+"@"+Integer.toHexString(hashCode());
}
public static Singleton2 getInstance(){// 可以不提供这个方法 枚举类型默认是public
return single;
}
public static void other(){// 验证是不是饿汉式
System.out.println("饿汉式 调用其他方法也会初始化实例");
}
}
public class danli2 {
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Singleton2.other();// 调用other方法时就已经创建好了单例对象
System.out.println("==================================");
System.out.println(Singleton2.getInstance());
System.out.println(Singleton2.getInstance());
// 反射破坏单例
// reflection(Singleton2.class);
// 反序列化破坏单例
// serializable(Singleton2.getInstance());
// Unsafe 破坏单例,无法预防
unsafe(Singleton2.class);
}
// 枚举类不怕反射破坏单例
private static void reflection(Class<?> clazz) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int.class);// 获取有参构造函数
constructor.setAccessible(true);// true表示可以使用私有构造
System.out.println("反射创建实例:"+constructor.newInstance("OTHER",1));
}
// 枚举类不怕反序列化破坏单例
private static void serializable(Object instance) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();// 创建字节数组输出流
ObjectOutputStream oos = new ObjectOutputStream(bos);// 放入ObjectOutputStream管道
oos.writeObject(instance);// 把instance写入字节流
// 把字节流重新读取出来 还原了新的对象,不会走构造方法
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
System.out.println("反序列化创建实例:"+ois.readObject());
}
private static void unsafe(Class<?> clazz) throws InstantiationException {
Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);// 可以根据类型创建类型实例,不会走构造方法
System.out.println("Unsafe 创建实例:"+o);
}
}
正常情况的结果
可以看出枚举类实现的单例 就是饿汉式的 会在调用类时就加载构造函数,且只有一个单例对象
使用反射破坏单例的结果
可以看出 我们并没有对枚举实现的单例模式 做任何的处理 它依旧可以避免反射破坏单例
使用反序列化破坏单例的结果
可以看出 我们并没有对枚举实现的单例模式 做任何的处理 它依旧可以避免反序列化破坏单例
使用Unsafe方法破坏单例
可以看出 如果使用Unsafe方法依旧可以破坏单例。
04懒汉式单例:
特点
懒汉式单例 是实现单例模式的一种方式,他与饿汉式单例的主要区别在于,饿函数单例是在类加载时 静态成员变量就被单例模式的唯一实例赋值 而 懒汉式则是在调用获取实例对象方法时才创建实例进而赋值,懒汉式是线程不安全的。
代码
public class danli3 { public static void main(String[] args) { Singleton3.other(); System.out.println("=================================="); System.out.println(Singleton3.getInstance()); System.out.println(Singleton3.getInstance()); } } // 懒汉式单例(线程不安全) class Singleton3 implements Serializable { private Singleton3(){ System.out.println("懒汉式 初始化"); } private static Singleton3 single = null; public static Singleton3 getInstance(){ if (single==null){// 假如有两个线程同时到这 会创建两个单例对象 single=new Singleton3(); } return single; } public static void other(){ System.out.println("懒汉式 调用其它方法 不会初始化单例对象"); } }
结果
可以发现懒汉式单例 初始化时不会 初始化构造函数 且 与不会初始化单例对象,需要在调用getInstance()方法后获取单例对象时才会创建单例对象
懒汉式优化并发问题
问题介绍
假如有两个线程同时运行到getInstance()方法时 判断单例对象是否创建时会出现并发问题 导致有两个单例对象被创建
代码
public class danli3 {
public static void main(String[] args) {
Singleton3.other();
System.out.println("==================================");
System.out.println(Singleton3.getInstance());
System.out.println(Singleton3.getInstance());
}
}
// 懒汉式单例(线程不安全)
class Singleton3 implements Serializable {
private Singleton3(){
System.out.println("懒汉式 初始化");
}
private static Singleton3 single = null;
public static synchronized Singleton3 getInstance(){
if (single==null){// 加锁 实现线程同步
single=new Singleton3();
}
return single;
}
public static void other(){
System.out.println("懒汉式 调用其它方法 不会初始化单例对象");
}
}
解决方法就是 加锁,每次只能有一个线程进入getInstance() 这样就可以只创建一个实例 解决了并发问题。
05懒汉式单例优化 双检锁:
问题介绍
上一种方法虽然解决了并发问题 但因为我们在getInstance()上加锁 导致我们每次调用getInstance()获取实例时都会进行加锁 严重的降低了效率,所以我们在此基础上再进行优化优化方式是加双检锁 顾名思义 就是加了两层判断 外层判断优化速度 内存判断解决并发问题
代码
public class danli4 {
public static void main(String[] args) {
Singleton4.other();
System.out.println("==================================");
System.out.println(Singleton4.getInstance());
System.out.println(Singleton4.getInstance());
}
}
// 对上一种懒汉式 多线程加锁的优化:双检锁,外层锁优化速度 内层锁为了保证线程安全
class Singleton4 implements Serializable {
private Singleton4(){
System.out.println("懒汉式 初始化");
}
// 双检锁必须加volatile修饰,作用是解决共享变量的 可见性 有序性,这里主要是为了保证 有序性
private static volatile Singleton4 single = null;
public static Singleton4 getInstance(){
if (single==null){// 再最外面加了一个判断 为了避免每次获取实例 都得走加锁逻辑
synchronized (Singleton4.class){
if (single==null){// 里面判断 为了避免多线程第一次同时创建实例 导致创建多个单例对象
single=new Singleton4();
}
}
}
return single;
}
public static void other(){
System.out.println("懒汉式 调用其它方法 不会初始化单例对象");
}
}
注意:
这里有一个注意点就是 我们在修饰静态变量时 加了volatile,这里它的作用是 为了保证 程序指令的有序性。==问题描述:==
single=new Singleton4();这行代码背后的指令主要有三个:
1.创建Singleton4对象
2.初始化构造函数
3.给静态变量single赋值
其中2,3指令在程序运行时是可以互换的,在单线程中也完全正确 但是在多线程中就错了。具体看下图
程序有序性原理图
由上图可以看出如果不加volatile会导致指令的顺序可能改变 从而使得线程2的操作插入在 初始化构造函数前,导致虽然单例对象已经创建但是单例对象需要的一些初始化数据 还没有被构造函数初始化,导致初始化不完整。
06懒汉式单例优化 内部类:
特点
我们看到上述实现懒汉式单例的方法都过于复杂了,复杂的原因是因为并发问题,我们又想为什么饿汉式不用考虑并发问题,其根本原因是 饿汉式是最开始就给静态变量赋值 使其变成静态代码块 在类加载时静态代码块就执行了一次且只执行一次 这就导致肯定不可能出现 并发问题,所以我们的优化思路是 把懒汉式单例 初始化静态变量时也变成静态代码块
代码
/*
为什么饿汉式不需要考虑线程安全,因为饿汉式是最开始就给静态变量赋值,这段代码最终放在静态代码块其线程安全
jvm帮我们做了,因为当类第一次被调用时 执行静态代码块且只执行一次 所以一定线程安全
所以我们现在需要把懒汉式 也放入静态代码块就好了
*/
public class danli5 {
public static void main(String[] args) {
Singleton5.other();
System.out.println("==================================");
System.out.println(Singleton5.getInstance());
System.out.println(Singleton5.getInstance());
}
}
class Singleton5 implements Serializable{
private Singleton5(){
System.out.println("内部类懒汉式 初始化");
}
private static class Holder{// 静态内部类,静态变量赋值,实现静态代码块
static Singleton5 single = new Singleton5();
}
public static Singleton5 getInstance(){
return Holder.single;
}
public static void other(){
System.out.println("调用其它方法");
}
}
结果
可以看出完美的实现了 懒汉式单例 且 这种方式不会出现并发问题,是懒汉式里最推荐的方式。
07JDK中哪些地方体现了单例模式
Runtime类
很明显Runtime类是饿汉式单例
System类中的Console类型单例
很明显是静态变量被volatile修饰 说明是优化版懒汉式
补充:Collections类中很多实现都是单例,可以自己分析