一、单例模式介绍
1.什么是单例模式
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
2.单例模式的使用场景
需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等)
3.评价单例模式方法的指标
- 是否是单例模式?
- 是饿汉式还是懒汉式?
- 是否是线程安全的?
- 效率如何?
4.实现单例模式的几种方法
具体看下文
二、饿汉式
饿汉式,实例在初始化的时候就已经建好了,不管你有没有用到,都先建好了再说。好处是没有线程安全的问题,坏处是浪费内存空间。
1.使用静态常量的方式实现
/**
* @author 四五又十
* @version 1.0
* @date 2020/6/1 19:50
*/
public class SingleTon01 {
//1. 构造器私有化, 外部能new
private SingleTon01() {}
//2.本类内部创建对象实例
private static final SingleTon01 instance = new SingleTon01();
//3.提供一个公有的静态方法,返回实例对象
public static SingleTon01 getInstance(){
return instance;
}
}
分析:使用静态常量的方式来创建,由于在类加载的时候创建该类的实例,所以不会有线程安全问题,也就是说是线程安全的,但是只要这个类一进行装载,那么该类就完成了实例化,也就是没有达到懒加载的目的。
优点:写法比较简单,就是在类装载的时候就完成实例化。避免了线程同 步问题
缺点:只要这个类一进行装载,那么该类就完成了实例化,这样有可能造成内存浪费
2.静态代码块实现
这种方式与上面就是表现形式不一样,其他都一样
/**
* @author 四五又十
* @version 1.0
* @date 2020/6/1 19:50
*/
public class SingleTon02 {
//1. 构造器私有化, 外部能new
private SingleTon02() {}
//2.本类内部创建对象实例
private static SingleTon02 instance;
static {
instance = new SingleTon02();
}
//3.提供一个公有的静态方法,返回实例对象
public static SingleTon02 getInstance(){
return instance;
}
}
三、懒汉式
1.有线程安全问题的实现
/**
* @author 四五又十
* @version 1.0
* @date 2020/6/1 19:50
*/
public class SingleTon03 {
//1. 构造器私有化, 外部能new
private SingleTon03() {}
private static SingleTon03 singleTon03;
public static SingleTon03 getInstance(){
if(singleTon03 == null){
singleTon03 = new SingleTon03();
}
return singleTon03;
}
}
分析:当需要使用该类时,getInstance()方法会判断当前对象类中是否有singleTon03的实例,如果没有那么创建,如果有直接返回,这样似乎做到了单例和懒加载,即需要时才进行加载;但是仔细一想,在多线程的环境下,如果其中A线程执行到了if判断并且通过了判断,然而此时切换到了B线程,那么显然B线程也是可以通过判断的那么会创建singleTon03的实例,如果此时切换回了A线程,那么A也会再次创建该对象的实例,这样就不是单例的效果!
所以这种方法不允许使用
2.加synchronized锁解决线程安全问题
那么很显然,要解决上述的问题,只需要在上述方法上加上synchronized锁即可:
/**
* @author 四五又十
* @version 1.0
* @date 2020/6/1 19:50
*/
public class SingleTon04 {
//1. 构造器私有化, 外部能new
private SingleTon04() {}
private static SingleTon04 singleTon03;
public static synchronized SingleTon04 getInstance(){
if(singleTon03 == null){
singleTon03 = new SingleTon04();
}
return singleTon03;
}
}
分析:当A,B线程想要同时执行getInstance方法时,由于加上了synchronized锁,那么其中一个线程只能等待,等轮到该等待的线程时,此时已经不能通过if判断,那么这样即使在多线程环境下也可以做到单例;synchronized是一个重量级的锁,很显然会带来效率问题
优点:解决了线程不安全问题
缺点: 效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行 同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例, 直接return就行了。方法进行同步效率太低
3.解决效率过低:Double-Check
这时,为了解决效率多低的问题,想当然的会想到一种方式:
/**
* @author 四五又十
* @version 1.0
* @date 2020/6/1 19:50
*/
public class SingleTon05 {
//1. 构造器私有化, 外部能new
private SingleTon05() {}
private static SingleTon05 singleTon03;
public static SingleTon05 getInstance(){
if(singleTon03 == null){
synchronized(SingleTon05.class){
singleTon03 = new SingleTon05();
}
}
return singleTon03;
}
}
也就是在代码块上加上synchronized锁
那么,这样到底有没有解决问题呢?没有!当线程A执行到if判断并且通过if判断时,此时如果线程B也执行if判断那么显然结果和第一种具有线程安全的实现是一样的,也就是没有解决线程安全问题。
为了要解决这个问题,提出了双重验证的方法,具体如下:
/**
* @author 四五又十
* @version 1.0
* @date 2020/6/1 19:50
*/
public class SingleTon06 {
//1. 构造器私有化, 外部不能new
private SingleTon06() {}
private static volatile SingleTon06 singleTon06;
public static SingleTon06 getInstance(){
if(singleTon06 == null){
synchronized(SingleTon06.class){
if(singleTon06 == null){
singleTon06 = new SingleTon06();
}
}
}
return singleTon06;
}
}
分析:还是按照上面分析的思路,如果线程A,B同时进入if判断并且通过,那么先进入到if里面的线程进行里面的方法时,则会被加上一把synchronized锁,当执行完成创建好了该实例后,另外一个线程进入if里面,并且也执行加上了一把synchronized锁的代码,但是此时却是不能通过第二个if判断,因为singleTon06已经被先执行的进程创建好了,那么这样就实现了多线程下的安全问题,也同时大大提高了效率!
优点:线程安全;延迟加载;效率较高
在实际开发中,建议使用
volatile是一个轻量级的锁,主要有两个作用:
被volatile修饰的变量保证对所有线程可见
2.禁止指令重排序优化
在这里,关键字volatile的作用主要是禁止指令重排序优化。首先先理解在执行new SingleTon06()中,CPU大概干了些什么事情:
1.看class对象是否加载,如果没有就先加载class对象;
2.分配内存空间,初始化实例;
3.调用构造函数;
4.返回地址给引用
但是在实际的过程中,CPU为了优化程序,可能会进行指令重排序,打乱这3,4这几个步骤,导致实例内存还没分配,就被使用了。
线程A执行到new Singleton06(),开始初始化实例对象,由于存在指令重排序,这次new操作,先把引用赋值了,还没有执行构造函数。这时时间片结束了,切换到线程B执行,线程B调用new Singleton06()方法,发现引用不等于null,就直接返回引用地址了,然后线程B执行了一些操作,就可能导致线程B使用了还没有被初始化的变量,所以要加上volatile禁止CPU进行指令重排
4.使用静态内部类
/**
* @author 四五又十
* @version 1.0
* @date 2020/6/1 19:50
*/
public class SingleTon07 {
//1. 构造器私有化, 外部能new
private SingleTon07() {}
private static class singleTon07Instance{
private static final SingleTon07 INSTANCE = new SingleTon07();
}
public static SingleTon07 getInstance(){
return singleTon07Instance.INSTANCE;
}
}
要理解这种方式首先需要明确一点:
静态内部类不持有外部类的引用(《java核心技术》)
也就是说当外部类加载时,静态内部类并不会随之加载,那么这样利用类加载机制保证了懒加载!并且这个过程是线程安全的
四、使用枚举
/**
* @author 四五又十
* @version 1.0
* @date 2020/6/1 19:50
*/
public enum SingleTon08 {
INSTANCE;
public void method(){}
}
利用jdk1.5增加的枚举可以很好的实现单例,线程安全,懒汉式