目录
1.什么是单例模式
单例模式是一种设计模式,单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例。单例模式的具体实现又分为饿汉模式和懒汉模式两种。
2.为什么需要单例模式?
有的类比较庞大和复杂,它的实例对象的创建和销毁对资源来说消耗很大,如果频繁的创建和销毁对象,并且这些对象是完全可以复用的,那么将会造成一些不必要的性能浪费。
3.如何实现单例模式
单例模式具体的实现方式分为“饿汉” 和 “懒汉” 两种方式
在实现单例模式时需要考虑两点:
1、是否线程安全
2、是否懒加载(不知道什么是懒加载的懒汉方式会解释到)
3.1饿汉方式
代码实现:
public class Singleton {
private Singleton(){}//构造私有构造方法
private static Singleton instance = new Singleton();//创建私有属性对象
//通过getInstance提供公开对象
public static Singleton getInstance(){
return instance;
}
}
饿汉模式线程是安全的,但是却不是懒加载的。
3.2懒汉模式
(1)非线程安全版
public class Singleton2 {
private Singleton2(){}//构造器私有
private static Singleton2 instance = null;//初始化实例对象
public static Singleton2 getInstance2(){
if(instance == null){
instance = new Singleton2();
}
return instance;
}
}
首先将构造器设置为private,那么其他类就无法通过new来直接构造这个分构成对象实例了,而其他类中需要使用Singleton2对象的话只能通过调用getInstance方法,在getInstance方法中,首先判断instance是否被构造过,如果构造过就直接使用,如果没有就当场构造。
懒加载:实例对象是第一次被调用的时候才真正构建的,而不是程序一启动它就够建好了等你调用的,这种滞后构建的方式就叫做懒加载。
在程序中,“懒” 是个好习惯,并且我们要尽可能的 “懒” 。那么懒加载的好处是什么?因为有的对象构建开销是比较大的,若该项目从项目启动就构建,万一从来都没被调用过,就会产生浪费,只有当真正需要使用了再去创建,才是更加合理的。
不难看出这段代码线程是不安全的,因为此时是并发执行状态,在执行if(inatance == null)时,可能会有多个不同的线程同时进入,这样就会实例化多次。
比如:
public class demo02 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Singleton2.getInstance2());
});
Thread t2 = new Thread(() -> {
System.out.println(Singleton2.getInstance2());
});
Thread t3 = new Thread(() -> {
System.out.println(Singleton2.getInstance2());
});
t1.start();
t2.start();
t3.start();
}
}
运行结果:
可以看出产生了两个实例,而本应该只产生一个,所以线程是不安全的,因此这种方式不可取,需要加以改进。
(2)线程安全版改进版1
public class Singleton3 {
private Singleton3(){}
private static Singleton3 instance = null;//初始化实例对象
public static synchronized Singleton3 getInstance3(){//通过加锁使其只能进入一个线程
if(instance == null){
instance = new Singleton3();
}
return instance;
}
}
想要在多线程状态下使其安全运行,我们可以通过加锁synchronized来改进第一版,这样getInstance3()方法同一时间只能进入一个线程,就可以保证线程安全。
但是这样又引入了新的问题,其实我们只想要对象在构建的时候同步线程,而这样的话每次在获取对象时就都要进行同步操作,对性能影响非常大,无疑是捡了芝麻露了西瓜,所以这种写法大多数情况下都不可取。
(3)线程安全版改进版2
public class Singleton4 {
private Singleton4(){}
private static Singleton4 instance = null;//初始化实例对象
public static Singleton4 getInstance4(){
if(instance == null){
synchronized (Singleton4.class){//此处进行加锁
instance = new Singleton4();
}
}
return instance;
}
}
在改进版中,现在getInstance是不需要竞争锁的,所有线程都可以直接进入,此时进行第二步判断,如果实例对象还没有构建,那么多个线程开始争抢锁,抢到手的线程开始创建实例对象,实例对象创建之后,以后所有的线程在执行到if判断时都可以直接跳过,返回实例对象来进行调用,这就解决了上一版的低效问题。
测试结果:
但是这段代码还是有些问题的,根据测试结果,我们发现,在多个线程执行if判断后,虽然只有一个线程能够抢到锁去执行if内部的代码,但是可能会有其他线程已经进入了if代码块,此时正在等待,一旦线程a执行完,线程b就会立即获取锁,然后进行对象创建,这样对象就会被创建多次。
(4)线程安全版改进最终版
public class Singleton5 {
private Singleton5(){}
private static volatile Singleton5 instance = null;//使用volatile关键字初始化实例对象
public static Singleton5 getInstance5(){
if(instance == null){//第一层加锁保证线程效率
synchronized (Singleton5.class){
if(instance == null){//第二层加锁保证线程安全
instance = new Singleton5();
}
}
}
return instance;
}
}
我们可以给上一版的锁外面再加一个if判断,这样就保证了我们的线程既是安全的又是高效的。
为什么要使用volatile关键字?
因为 instance == new Singleton() 在指令层面,这不是一个原子操作,他分为了三步:
- memory = allocate() //分配内存
- ctorInstanc(memory) //初始化对象
- instance = memory //设置instance指向刚分配的地址
在真正执行时,虚拟机为了效率可能会进行指令重排序,比如先执行第一步,再执行第三步,再执行第二步,如果按照这个顺序,a线程执行到了第三步时,此时instance还未被初始化,假设在此时b线程执行到了 if(instance == null) 这一步,此时在b线程中, instance == null 返回false,直接跳过,但是a线程内的instance还未初始化,就会导致b线程中调用 getInstance() 未初始化,出现线程不安全的情况。因此给instance 加上 volatile 修饰,就可以阻止作用在 instance 上的指令重排序问题
我们可以假设a b 两个线程同时进入getInstance()方法,a首先获取了锁然后进行了instance的构建,当它构建完之后它会交还锁,这时候b线程也会立即获得锁,在获得锁之后进行一个判空,此时我们可以看到instance因为已经被线程a初始化了,所以instance是不等于null的,于是b线程将会直接退出,返回实例,这样就不会造成线程不安全的问题了,这种对对象进行两次判空的操作叫做双重校验锁。
该篇博客借鉴了b站up主寒食君的视频讲解。
理解有限,如有不足,还望指出!!