1.什么是单例模式?
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
2.单例模式有哪些应用?
对于实际应用中,有时我们会写一些工具类来辅助开发,设想一下这种情况,有多个Service和一个Util,Service负责提供服务,Util负责帮忙做字符串处理,做常见的做法是在每一个Service实例中区new一个实例对象Util,这样在Service的所有实例方法中就能使用这个Util了,但是这也造成了一个问题—>在每一个Service中对Util都是做一样的事情,并且对这个Util方法的调用不会根据Service的不同而不同,而现在的做法是在每一个Service中填充一个Util对象,一个两个Service还好,如果是100.1000,10000个呢?在堆空间中重复创建的Util是对堆内存的一种浪费,多个Service完全可以共用一个Util!而单例模式应运而生。
3.单例模式五种实现
3.1-饿汉式
饿汉式实现思想如它的名字一样,就怕需要的用的时候没办法拿到这个对象,所有我在类加载时就将这个对象创建出来以备不时之需 代码实现:
/**
* @author :huanghongbe
* @description:
* @date :2021/8/4 21:13
*/
public class Hungry {
private static Hungry hungry = new Hungry();
public static Hungry getHungry(){
return hungry;
}
}
3.2-懒汉式
于饿汉式想法,懒汉式等到真正要使用这个单例类的时候才去实例化它
/**
* @author :huanghongbe
* @description:
* @date :2021/8/4 22:56
*/
public class Lazy {
private static Lazy lazy = null;
public static Lazy getLazy(){
if(lazy == null){
return new Lazy();
}
return lazy;
}
}
这种写法的懒汉式并不支持多线程访问—>在多线程并发访问的情况下,假如线程A进入到getLazy()这个方法体,同时判断lazy为空成立,准备进入if语句,此时,由于系统调度,线程B获取到了系统资源时间片,同样进入了这个方法体并且执行完所有语句并返回,此时线程B完成了对象的创建并且获取到了这个lazy对象,之后系统调度A继续运行,根据程序计数器的指示A“跳过”了之前已经完成的语句直接进入到if语句块中,创建了一个lazy对象并返回。可以看出,这里已经发生了lazy 对象的重复创建并且两个线程对此一无所知,单例不再单例。
3.3-懒汉式改进
有没有什么方法改进懒汉式呢?当然有! 这里就要引入懒汉式的改进写法,双重锁模式懒汉式
public class DoubleCheck {
private static DoubleCheck doubleCheck = null;
public static DoubleCheck getDoubleCheck(){
if(doubleCheck==null){
synchronized (DoubleCheck.class){
if(doubleCheck==null){
doubleCheck=new DoubleCheck();
}
}
}
return doubleCheck;
}
}
整体思路:为了避免重复创建,使用synchronized锁住创建对象语句,强制多个线程在同时只能有一个进入到同步代码块,synchronized底层使用基于C语言实现的objectMonitor,直接于操作系统层面的线程挂钩,实现线程的互斥访问,这里先不展开赘述。同时,引入了双重锁保护—>所谓的双重锁其实是两层if判断语句,一层在进入同步块之前,一层在创建对象之前。 为什么要进行两次这样的判断呢? 第一次判断:进入synchronized意味着线程的互斥访问,得到锁的线程进入同步块,没得到锁的线程则阻塞等待,而懒汉式造成重复创建对象的问题仅仅发生在初次使用时创建这个单例的时候,在对象创建成功后多个线程获取这个对象其实只需要return doubleCheck; 如果不加上这层判断,则每次获取对象其他线程都会在这个方法中阻塞直到获取锁的线程进入同步块做完一切判断之后释放锁,而这种情况完全没必要。 第二次判断:假设两个线程同时执行完第一层if判断来到了同步代码块前,线程A先获得锁完成对象的创建并返回,线程B之后获得锁,但是因为B在获得锁之前已经经过了判断对象是否为空的步骤,所以,线程B自然的进入同步块同时也创建了一个singleTon。此时又发生了重复创建问题 这样改进的懒汉式就绝对安全吗?非也! 引入一个概念—>重排序,在程序运行过程中,我们的代码并不是如我们编写的顺序一般先后运行的,在满足语义相同的情况下,jvm和操作系统都会对我们的代码进行重排序。 接着我们再看创建对象这步doubleCheck=new DoubleCheck(); 表面上看只有一条语句,但是创建对象在虚拟机栈中的执行可不是一蹴而就的,事实上,它被分为三步—>1.创建一个空对象2.给对象初始化值,3.改变局部变量表的指针指向这个对象。 设想一下,当一个线程执行到了第一步即创建一个对象,由于重排序,第三步操作提前即线程A创建了一个空对象并修改了指针,之后,线程A响应中断释放了锁。线程B进入并进行判断,则线程B在判断这个对象是否为空时会得到意想不到的结果—>这个对象不是null。则B理所应当的走出同步代码块同时返回了刚刚A创建的这个空对象(这里的空不是指doubleCheck==null 而是doubleCheck=new doubleCheck() )。 那要怎么改进呢? 禁用重排序的方法—>volatile,当我们给对象加上volatile关键字时,程序会禁止代码重排序,其中的原理涉及JMM内存模型对可见性的保证的(happens-before原则) 所有最终的改进版懒汉式代码如下:
public class DoubleCheck {
private static volatile DoubleCheck doubleCheck = null;
public static DoubleCheck getDoubleCheck(){
if(doubleCheck==null){
synchronized (DoubleCheck.class){
if(doubleCheck==null){
doubleCheck=new DoubleCheck();
}
}
}
return doubleCheck;
}
}
3.4-静态内部类实现
/**
* @author :huanghongbe
* @description:
* @date :2021/8/4 23:38
*/
public class OuterClass {
private OuterClass(){}
private static class InnerClass{
private static final OuterClass INSTANCE = new OuterClass();
}
public static OuterClass getInstance(){
return InnerClass.INSTANCE;
}
}
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当OuterClass第一次被加载时,并不需要去加载InnerClass,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载InnerClass类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
3.5-枚举类
史上实现最简单的单例模式,直接上代码
/**
* @author :huanghongbe
* @description:
* @date :2021/8/4 23:44
*/
public enum EnumSignleTon {
INSTANCE;
public static EnumSignleTon getInstance(){
return INSTANCE;
}
}
上面的几种实现的单例模式都是基于Class的形式,对于一个类,我们可以使用反射机制修改破坏单例的实现。 对于反射问题,枚举类在编译时其实是生成了Enum类的子类,在反射使用Constructor.newInstance方法创建实例时会判断这个类是否为枚举类如果是则报异常 对于序列化反序列化问题,它是利用序列化和反序列化的方式返回的对象实例不同来实现破坏单例模式,对于一个可以序列化的类,在反序列化时ObjectStreamClass这个类会为其提供一个空构造方法,因此经过反序列化操作后可以得到类一个空对象,这就有可能破坏单例模式。而枚举类,由于其独特的序列化和反序列化措施,并不会对类内部的INSTANCE实例进行序列化,因此在反序列化时就得不到这个类的构造方法进而无法通过反射创建这个类的空对象
|