概念:
单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,他提供了全局访问的方法。单例模式是一种对象创建型模式。
Spring依赖注入Bean实例默认都是单例的,所以我们这一章回顾一下单例模式。
传统创建类代码
package pattern;
public class Case_1 {
public static void main(String args[]){
Singleten s1 = new Singleten();
Singleten s2 = new Singleten();
}
}
class Singleten{
}
在上段代码中,我们每次new Singleten(),都会创建一个Singleten实例,显然不符合一个类只有一个实例的要求。所以作出如下修改:
/**
* 单例模式实例
*/
public class Case_1{
public static void main(String args[]){
Singleten s = Singleten.getInstance();
}
}
/**
* 描述:单例类(饿汉模式)
* @author yangfan
* */
class Singleten{
//stap2.自行对外提供实例
private static final Singleten singleten = new Singleten();
//stap1.构造函数私有化
private Singleten(){
}
//stap3.提供外界可以获得该实例的方法
public static Singleten getInstance(){
return singleten;
}
}
单例模式的写法有很多种,上述代码是一个简单的饿汉模式的实现代码,实现步骤总共有三步:
- 构造函数私有化
- 自行对外提供实例
- 提供外界可获得该实例的方法
与饿汉模式相对应的还有懒汉模式,懒汉模式有延迟加载的意思,具体代码如下:
/**
* 懒汉模式
*/
private static Singleten singleten = new Singleten();
private Singleten(){};
public static Singleten getInstance(){
//1.判断对象是否创建
if(null==singleten){
singleten = new Singleten();
}
return singleten;
}
如果创建单例对象会消耗大量资源,那么懒汉模式的延迟创建是个不错的选择
但是
懒汉模式有个明显的问题,就是没有考虑到线程的安全问题,在多线程的并发情况下,会并发调用getInstance()方法,从而导致系统同时创建多个单例类实例,这显然不符合要求。
我们可以通过给getInstance()添加锁来解决该问题:
/**
* 懒汉模式(添加Synchronized锁)
*/
private static Singleten singleten = new Singleten();
private Singleten(){};
public static synchronized Singleten getInstance(){
//1.判断对象是否创建
if(null==singleten){
singleten = new Singleten();
}
return singleten;
}
添加synchronized锁虽然可以保证线程安全,但是每次调用个体Instance()方法时,都会有加锁和解锁操作,其次synchronized锁是添加在方法上,锁的范围过大,而单例类是全局唯一的。
因此,需要使用“双重校验锁”进行优化:
/**
* 双重校验锁(指令重排问题)
*
*/
private static Singleten singleten = new Singleten();
private Singleten(){};
public static Singleten getInstance(){
//第一次校验
if(singleten == null){
synchronized(Singleten.class){
//第二次校验
if(singleten == null){
singleten =new Singleten();
}
}
}
return singleten;
}
以上双重校验锁或出现指令重排的问题:
指令重排:指JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能的提高并行度
singleten = new Singleten()可以抽象为以下JVM指令:
//1.分配对象的内存空间
memory = allocate();
//2.初始化对象
ctorInstance(memory);
//3.设置instance指向刚分配的内存地址
singleten = memory;
上述操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对他们进行指令的优化重新排序
排序后步骤如下
//1.分配对象的内存空间
memory =allocate();
//2.instance指向刚分配的地址,此时对象还没有初始化
singleten = memory;
//3.初始化对象
ctorInstance(memory);
为了解决指令重排问题,可以使用volatile关键字修饰singleten字段。volatile关键字的一个语义就是禁止指令的重新排序优化,阻止JVM指令重排,修改如下:
/**
* 双重校验锁(volatile解决指令重排问题)
*/
private static volatile Singleten singleten = new Singleten();
private Singleten(){};
public static Singleten getInstance(){
//第一次校验
if(singleten == null){
synchronized(Singleten.class){
//第二次校验
if(singleten == null){
singleten =new Singleten();
}
}
}
return singleten;
}
当然,除了双重校验锁的方法,还有一种比较好的单例模式写法:
静态内部类的单例模式
/**
* 静态内部类的单例模式(推荐写法)
*/
//2.私有的静态内部类,类加载器负责加锁
private static class SingletenHolder{
private static Singleten singleten = new Singleten();
}
//1.构造方法私有化
private Singleten(){};
//3.自行对外提高实例
public static Singleten getInstance(){
return SingletenHolder.singleten;
}