目录
单例模式大致介绍
相信接触java的小伙伴都对单例模式有一点了解。
单例模式属于创建型模式,其主要为:在一个系统或一个项目中,我们对于某一类对象只创建保存维护一个实例对象。
在我们常用的单例模式主要分为两种,一种是饿汉式,一种是懒汉式。
饿汉式与懒汉式的主要区别为:饿汉式是对象随着类的加载而创建,即一开始便创建好了对象,而懒汉式是指在你调用时才会创建对象。
由其区别,可以分析:
饿汉式的主要优点为:实例是随着类的加载而加载的,故此避免了线程同步问题,而且较为简单;
缺点也很明显:如果没用到懒汉式的这个实例,就会造成内存的浪费。
而懒汉式的优点为:能够充分利用,在调用时才创建。
缺点:就是容易造成线程同步,造成线程不安全问题。
单例模式细分下来,可达8种之多!
饿汉式分为2种,懒汉式分为6种!
单例模式之饿汉式
首先我们对于饿汉式的两种先做介绍。
饿汉式分为静态变量和静态代码块两种。
静态变量顾名思义,就是将对象作为类的静态变量,通过显式赋值的方式,来对对象进行初始化。
静态代码块则是通过静态代码块的形式进行赋值(即创建对象)。
由于不是很难,我便写上代码。
静态变量
package singleton;
/**
* @author liuweixin
* @create 2022-01-25 10:26
*/
//饿汉式1:静态变量
public class Singleton1 {
public static void main(String[] args) {
//验证其存在的对象实例是否唯一
Singleton1 singleton1 = Singleton1.getSingleton1();
Singleton1 singleton2 = Singleton1.getSingleton1();
System.out.println(singleton1==singleton2);//true
}
//此时的对象是在类的变量中显式创建的
private static Singleton1 singleton1= new Singleton1();
//构造器私有是为了防止外界通过new+构造器的方式创建对象
private Singleton1(){}
//我们通过对外暴露一个public的方法,供外界获取实例对象
//这里也符合设计模式原则之迪米特法则,即最小知道原则,尽可能提供少的public方法给外部,把其它不必要的进行隐秘封装
public static Singleton1 getSingleton1(){
return singleton1;
}
}
静态代码块
package singleton;
/**
* @author liuweixin
* @create 2022-01-25 10:30
*/
//饿汉式1:静态代码块
public class Singleton2 {
public static void main(String[] args) {
//验证其存在的对象实例是否唯一
Singleton2 singleton1 = Singleton2.getSingleton2();
Singleton2 singleton2 = Singleton2.getSingleton2();
System.out.println(singleton1==singleton2);//true
}
//定义变量,此时不显式赋值
private static Singleton2 singleton2;
static {//在静态代码块中创建对象
singleton2 = new Singleton2();
}
//构造器私有是为了防止外界通过new+构造器的方式创建对象
private Singleton2(){}
//我们通过对外暴露一个public的方法,供外界获取实例对象
public static Singleton2 getSingleton2(){
return singleton2;
}
}
单例模式之懒汉式
懒汉式共分为六种:
1) 懒汉式(线程不安全)
2) 懒汉式(线程安全,同步方法)
3) 懒汉式(线程安全,同步代码块)
4) 双重检查
5) 静态内部类
6) 枚举
我们通过在代码中探究:
1)懒汉式(线程不安全)
package singleton;
/**
* @author liuweixin
* @create 2022-01-25 10:37
*/
//懒汉式(线程不安全)
public class Singleton3 {
public static void main(String[] args) {
/*
分析问题:
对于本类中的getSingleton3方法,如果多个线程同时进入该方法的if判断,初始大家都会判断singleton3为null
会导致每个线程都会new一个对象
因为无法保证该系统中只存在一个实例,故此存在线程不安全的问题
但是如果是单线程通过,则不存在线程不安全的问题
*/
System.out.println("=====单线程下====");
Singleton3 singleton1 = Singleton3.getSingleton3();
Singleton3 singleton2 = Singleton3.getSingleton3();
System.out.println(singleton1 == singleton2);
}
//创建一个属性变量
private static Singleton3 singleton3;
//私有构造器,防止外界new对象
private Singleton3() {
}
//我们通过对外暴露一个public的方法,供外界获取实例对象
public static Singleton3 getSingleton3() {
//保证只有一个实例
if (singleton3 == null) {
singleton3 = new Singleton3();
}
return singleton3;
}
}
2) 懒汉式(线程安全,同步方法)
package singleton;
/**
* @author liuweixin
* @create 2022-01-25 10:51
*/
/*
对上一种方法的问题分析:
对于本类中的getSingleton3方法,如果多个线程同时进入该方法的if判断,初始大家都会判断singleton3为null
会导致每个线程都会new一个对象
因为无法保证该系统中只存在一个实例,故此存在线程不安全的问题
但是如果是单线程通过,则不存在线程不安全的问题
*/
//懒汉式(线程安全,同步方法)
public class Singleton4{
public static void main(String[] args) {
/*
对于本种方式的问题分析:
效率太低,每个线程进入getSingleton4方法时,都需要保持同步,在线程外等待
其实我们并不需要等待,假设发现线程进入了,我们只需要判断singleton4不为空时返回就行了
因为一旦有线程进入了,我们就知道,有线程在实例化,或者已经实例化完了
如果实例化完了,后续的线程仍在等待,这样效率是很低了,直接返回对象就可以了
*/
}
//创建一个属性变量
private static Singleton4 singleton4;
//私有构造器,防止外界new对象
private Singleton4() {
}
//我们通过对外暴露一个public的方法,供外界获取实例对象
//我们把该方法设置为同步方法,此时即能够避免线程不安全问题
public static synchronized Singleton4 getSingleton4() {
//保证只有一个实例
if (singleton4 == null) {
singleton4 = new Singleton4();
}
return singleton4;
}
}
3) 懒汉式(线程安全,同步代码块)
package singleton;
/**
* @author liuweixin
* @create 2022-01-25 11:21
*/
//懒汉式(线程安全,同步代码块)
public class Singleton5 {
/*
对上一种方法的问题分析:
效率太低,每个线程进入getSingleton4方法时,都需要保持同步,在线程外等待
其实我们并不需要等待,假设发现线程进入了,我们只需要判断singleton4不为空时返回就行了
因为一旦有线程进入了,我们就知道,有线程在实例化,或者已经实例化完了
如果实例化完了,后续的线程仍在等待,这样效率是很低了,直接返回对象就可以了
*/
public static void main(String[] args) {
/*
对于本次改进的问题分析:
初始多线程一起进入方法,singleton5的初始值为null,都进了if的判断里面
一个线程握住锁,其余线程都在外面等待
当握住线程的锁创建实例完毕之后,换下一个线程握锁,又重新实例化,
这就导致了线程不安全的问题,故此该方法看似线程安全,实则线程不安全
*/
}
//创建一个属性变量
private static Singleton5 singleton5;
//私有构造器,防止外界new对象
private Singleton5() {
}
//我们通过对外暴露一个public的方法,供外界获取实例对象
public static Singleton5 getSingleton5() {
//保证只有一个实例
if (singleton5 == null) {
//握住类锁
synchronized (Singleton5.class) {
singleton5 = new Singleton5();
}
}
return singleton5;
}
}
4) 双重检查
package singleton;
/**
* @author liuweixin
* @create 2022-01-25 11:32
*/
//双重检查
public class Singleton6 {
/*
对上一种方法的问题分析:
初始多线程一起进入方法,singleton5的初始值为null,都进了if的判断里面
一个线程握住锁,其余线程都在外面等待
当握住线程的锁创建实例完毕之后,换下一个线程握锁,又重新实例化,
这就导致了线程不安全的问题,故此该方法看似线程安全,实则线程不安全
*/
//创建一个属性变量
//volatile告诉jvm, 它所修饰的变量不保留拷贝,直接访问主内存中的对象
private static volatile Singleton6 singleton6;
//私有构造器,防止外界new对象
private Singleton6() {
}
//我们通过对外暴露一个public的方法,供外界获取实例对象
/*
解决方案:
对属性增添一个关键字volatile
我们采取双重检查的方式
握住锁之后,再进行一次空判断,即可达成目的
*/
public static Singleton6 getSingleton6() {
//保证只有一个实例
if (singleton6 == null) {
//握住类锁
synchronized (Singleton6.class) {
if (singleton6 == null) {//在锁里面同样进行一次空判断
singleton6 = new Singleton6();
}
}
}
return singleton6;
}
}
5) 静态内部类
package singleton;
/**
* @author liuweixin
* @create 2022-01-25 11:40
*/
//静态内部类
public class Singleton7 {
//创建一个属性变量
private static volatile Singleton7 singleton7;
public static void main(String[] args) {
Singleton7 singleton7 = Singleton7.getSingleton7();
Singleton7 singleton71 = Singleton7.getSingleton7();
System.out.println(singleton7==singleton71);//true
/*
本方法的原理:
我们知道,jvm在类加载中,会将其static修饰的变量同样进行加载,
在Singleton7这个类中,SingletonInstance作为其静态内部类,同样也会被加载
而SingletonInstance中的属性也会被加载,
也就是说,SingletonInstance中的属性是单例模式的对象,在类加载阶段就会被初始化,与(1)(2)方法同理
且因为jvm在加载类中,类只会被加载一次,这也就不存在线程安全问题
这也就保证了一个实例的情况
*/
}
//私有构造器,防止外界new对象
private Singleton7() {
}
//创建一个静态内部类
static class SingletonInstance{
//定义一个属性,属性值为单例模式的对象
//需要加final关键字,防止其值被修改
private static final Singleton7 INSTANCE = new Singleton7();
}
//我们通过对外暴露一个public的方法,供外界获取实例对象
public static Singleton7 getSingleton7() {
return SingletonInstance.INSTANCE;
}
}
6) 枚举
package singleton;
/**
* @author liuweixin
* @create 2022-01-25 11:50
*/
//枚举
public class Singleton8 {
public static void main(String[] args) {
/*
本方法的思想:
我们通过jdk1.5引入的enum枚举关键字,通过枚举方式实现单例模式
*/
Singleton instance = Singleton.INSTANCE;
Singleton instance1 = Singleton.INSTANCE;
System.out.println(instance==instance1);//true
}
enum Singleton {
INSTANCE; //属性
}
}
源码中探究单例模式
1)JDK中
我们可以看到在JDK源码中的Runtime类中就有使用到单例模式的身影,如图即采用了饿汉式的单例模式
2)Spring框架中
用过spring框架的小伙伴应该都知道,我们在spring的配置文件创建bean对象时,会有这么个配置
标签中的scope属性,便是指明,通过单例模式来创建该对象。该属性默认值为Singleton,即为单例模式.
通过追溯源码,我们可以看到,在AbstractBeanFactory类中的getBean()方法中调用了getSingleton()方法,即为获取单例对象。
我们追溯getSingleton()方法,发现其在DefaultSingletonBeanRegistry类中实现,通过观察其实现代码,我们可以得知,spring中的单例模式的获取采用的是上述懒汉式的(4)双重检查的方式。
再追溯一下,发现getSingleton()方法是定义在一个接口SingletonBeanRegistry()中,而DefaultSingletonBeanRegistry则是实现了该接口,并重写实现了getSingleton()方法,至此,我们可以画出spring中单例模式对应的UML类图:
总结:
饿汉式与懒汉式各有千秋,需要根据具体业务需求采取对应合适的方式,对于饿汉式两种方式都可以使用,对于懒汉式,推荐使用后三种,即4) 双重检查、5) 静态内部类、6) 枚举
注意事项:
1) 单例模式保证了 系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
2) 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用 new
3) 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session 工厂等)