单例模式
什么是单例模式(Singleton Pattern)
单例模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式的必备条件
-
构造方法私有化
-
全局提供一个获取对象的入口
-
若不想被反射出来,可在构造方法中抛出异常
常见的单例模式
就目前而言比较常见的有如下几种:
-
饿汉式
-
懒汉式
-
双重检查
-
静态内部类
-
枚举
-
序列化
-
ThreadLocal
各种单例模式的写法
1.饿汉式
/**
* 好处 能够实现实现安全
*
* 坏处 不需要的时候也加载,浪费资源
*/
public class HungrySingle {
private static final HungrySingle hungrySingle = new HungrySingle();
//构造方法私有化
//构造方法私有化
private HungrySingle() {
/**
* 阻止了 反射获取单例模式
*/
if (HungrySingle.hungrySingle != null) {
throw new RuntimeException("不允许反射创建对象");
}
}
public static HungrySingle getHungrySingle() {
return hungrySingle;
}
}
饿汉式:就是不管有没有用到这个对象,我都先给你创建好了,用的时候随便用。优点:是一种线程安全的操作,缺点:可能这个对象用不到,但是我们也已经创建出来了,浪费资源。
2.懒汉式
public class LazySingle {
public static LazySingle lazySingle = null;
//构造方法私有化
private LazySingle() {
/**
* 阻止了 反射获取单例模式
*/
if (LazySingle.lazySingle != null) {
throw new RuntimeException("不允许反射创建对象");
}
}
// 线程不安全的情况下的使用方法 ,没有 synchronized 关键字
public static final LazySingle getInstance1(){
if (lazySingle == null) {
lazySingle = new LazySingle();
}
return lazySingle;
}
//线程安全的一种写法,有了 synchronized
//这种synchronized 锁住 static 方法,是一种类锁,运行太慢,线程多的时候会响应性能
public synchronized static final LazySingle getInstance2() {
if (null == lazySingle) {
lazySingle = new LazySingle();
}
return lazySingle;
}
}
懒汉式:懒汉式分为线程安全和线程不安全的两种情况,无非就是有没有 synchronized 关键字。它的解释:刚开始我什么都不敢,用的时候才创建对象。
3.双重检查机制
public class LazySingle {
public static LazySingle lazySingle = null;
//构造方法私有化
private LazySingle() {
/**
* 阻止了 反射获取单例模式
*/
if (LazySingle.lazySingle != null) {
throw new RuntimeException("不允许反射创建对象");
}
}
/**
* 为什么要有 双重的检查机制
* 但是 在内部的 sychronized 依旧可能会有多个线程 ,
* @return
*/
public static final LazySingle getInstance3() {
// 在这个地方,如果同时进来多个线程,会在这里等待,上一个线程执行完了,下个线程执行的时候,lazySingle就 // 会被重新开辟空间
if (null == lazySingle) {
synchronized (LazySingle.class) { //这里会有多个线程同时等待的可能
lazySingle = new LazySingle();
}
}
return lazySingle;
}
/**
* 这才是真正的双重检查机制
* 双重检查机制
* @return
*/
public static final LazySingle getInstance4() {
if (null == lazySingle) {
//这里纵然有了多个线程在等待,也不会重复开辟空间
synchronized (LazySingle.class) {
//会再次校验 lazySingle 是否为 null
if (null == lazySingle) {
lazySingle = new LazySingle();
}
}
}
return lazySingle;
}
}
双重检锁机制:是一种懒汉式的加强版,线程更加安全。
4.静态内部类
public class InnerSingle {
private InnerSingle() {
/**
* 阻止了 反射获取单例模式
*/
if (InnerClassSingle.innerSingle != null) {
throw new RuntimeException("不允许反射创建对象");
}
}
private static class InnerClassSingle {
private static InnerSingle innerSingle = new InnerSingle();
}
/**
* 巧妙地运用了内部类的执行 过程, 性能优秀
* @param args
*/
public static final InnerSingle getInstance() {
return InnerClassSingle.innerSingle;
}
}
内部类创建单例:这种写法的好处是充分利用了静态内部类的特点,它的初始化操作跟外部类是分开的。在没有调用 getInstance() 方法之前,静态内部类不会进行初始化,在第一次调用该方法后就生成了唯一一个实例。
5.枚举方式
public enum EnumSingle {
INNERSINGLE;
public static EnumSingle getEnumSingle() {
return INNERSINGLE;
}
}
枚举:枚举是一种JDK种天然的单例模式,在后面我们来 介绍一下源码种是怎样设计的。
6.序列化方法构建
//要实现 Serializable 接口
public class SerialSingle implements Serializable {
public static final SerialSingle serialSingle = new SerialSingle();
private SerialSingle() {
}
/**
* 反序列化 不被破环的关键
* @return
*
* 重写这个方法 只不过是反序列化出来的对象被 反射的对象代替了
* 在 jvM层面是 进行了两次对象的创建,最终返回的 反射的出来的
* 之前被被序列化的 被GC回收掉了
*/
public Object readResolve() {
return serialSingle;
}
public static void main(String[] args) {
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(new File("serail.obj")));
ObjectInputStream is = new ObjectInputStream(new FileInputStream(new File("serail.obj")));
os.writeObject(SerialSingle.serialSingle);
os.flush();
os.close();
//从这里我们进去看下源码的实现吧
SerialSingle serialSingle = (SerialSingle)is.readObject();
is.close();
System.out.println(serialSingle);
System.out.println(SerialSingle.serialSingle);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
单例使用反序列创建单例:必须要实现 readResolve() 方法,下面是解析为什么要实现这个方法的源码过程
7.ThreadLocal 各线程的单例
public class ThreadLocalSingle {
public static ThreadLocal threadLocal = new ThreadLocal();
public static void main(String[] args) {
threadLocal.set("1");
System.out.println(threadLocal.get());
}
}
ThreadLocal:这个只能保证 各自线程之间的线程安全,其实就是把值 放入到了一个全局的Map中,key是线程的名字,值就是我们set的值,这种像是 一种伪单例。
反射为什么是天然的单例
1.直接从代码层面进入
public static void main(String[] args) {
try {
Class clazz = EnumSingle.class;
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
//从这里进去看 源码
// 从这里可以看出从 jdk层面上我们就 已经限制了这个 枚举不能够进行反射
//所以说 枚举 是天然的 单例模式,这里会抛出异常
Object o = constructor.newInstance();
System.out.println(o);
} catch (Exception e) {
e.printStackTrace();
}
}
代码层面查看原因
反序列化是如何实现单例的、
1.同样通过代码的层面看源码
其实它反射调用 readResolve方法返回的就是我们早已经创建出来的对象我们同过Debug的方式查看是如何创建并返回对象的,如下图所示:
我们对使用反序列化方式获取单例的总结就是:在我们反序列化的时候创建了一个对象①,然后,会继续判断是否有readResolve方法,若有,则通过反射获取对象,之前被反序列化创建的对象会①被GC掉,所以,才实现了单例。
今天的单例模式就讲到这里,如果感到有点用处,或者想要了解更多的知识可关注公众号:
谢谢大家~