单例模式是我最早接触的设计模式,主要的构造方式是懒汉式与饿汉式两种,先说线程安全好实现的饿汉式
饿汉式:
public class SingleHungry {
/* 私有化构造函数 */
private SingleHungry() {}
/* 内部调用实例 */
private static SingleHungry instance = new SingleHungry();
/* 提供给外部一个访问方法 */
public static SingleHungry getInstance(){
return instance;
}
}
可以看出单例模式构造的核心思想其实就是先将构造函数私有化以禁止外部直接创建,然后提供一个外部直接获取对象的方法,懒汉式也是基于此思想上实现的
懒汉式
public class SingleLazy {
/* 私有化构造函数 */
private SingleLazy(){}
/* 定义一个空实例 */
private static SingleLazy instance = null;
/* 静态工厂方法 */
public static SingleLazy getInstance() {
/* 判断是否第一次创建 */
if (instance == null) {
instance = new SingleLazy();
}
return instance;
}
}
懒汉式与饿汉式的最大区别在于定义静态化实例的时候是否直接赋值,饿汉式意思就是人饿了会自己找东西吃,所以在定义静态实例时直接就赋值,而懒汉式就算饿了也要别人送到嘴边才肯吃,所以需要调用方法去创建(大概就是这么个意思...)
不过现在懒汉式这种写法是不能满足多线程需要的,因为当判断instance是否为空时如果两个线程同时进入就有可能创建两个实例了,我们需要进一步的改进
懒汉式-改进版
public class SingleLazy {
/* 私有化构造函数 */
private SingleLazy(){}
/* 定义一个空实例 */
private static SingleLazy instance = null;
/* 静态工厂方法 */
public static SingleLazy getInstance() {
if (instance == null) { //双重判断
synchronized (SingleLazy.class) { //增加同步锁
if (instance == null) {
instance = new SingleLazy();
}
}
}
return instance;
}
}
增加同步锁后看起来可以杜绝上面所说的两个线程同时进入的情况,因为当instance为空时才可以创建实例,即使存在两个线程都通过了第一层为空判断,在接下来的创建实例时也会被同步锁拦住,保证只会创建一个实例,等同步锁中的方法结束,下一个线程进入时就由第二层的为空判断将其拦住,不得再创建实例,看起来已经可以做到完美的实现多线程了,but...实际上还是存在一定缺陷的,这时候就这涉及到JVM的指令重排了
JVM的指令重排简单来讲就是创建对象的步骤会有变化,例如创建一个简单的instance = new SingleLazy()时,步骤如下
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
但是这些指令不一定每次都是这样,有时候JVM会改变顺序以优化创建过程,例如:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
发现问题没,当第三步如果提到初始化对象的前面时,这时候对象虽然还没有初始化,但是已经指向了分配的内存地址,也就是说这时候对象是不为空的,所以如果这时候有其他线程进入synchronized方法内部判断instance == null返回false,就直接返回对象了,这时候的对象只是指向了内存地址,并未完成初始化,所以返回的是一个空对象,怎么避免这种情况呢?这时候就要用到volatile这个修饰符了,说白了这个修饰符加上后就可以阻止JVM的指令重排了
懒汉式-最终版
public class SingleLazy {
/* 私有化构造函数 */
private SingleLazy(){}
/* 定义一个空实例 */
private volatile static SingleLazy instance = null; //增加volatile修饰符
/* 静态工厂方法 */
public static SingleLazy getInstance() {
if (instance == null) {
synchronized (SingleLazy.class) { //增加同步锁
if (instance == null) {
instance = new SingleLazy();
}
}
}
return instance;
}
}
到此,单例模式就成功创建了,再来说说单例模式一般会用到哪些地方呢?其实我们平时操作的Windows系统就有很多单例的实现,例如控制面板、任务管理器等,这些应用是不是都只能打开一个?不像文件夹可以重复打开多个,其实这就是典型的单例模式的应用,在实际开发中使用单例模式多是为了控制那些需要重复使用的资源,比如java开发中常用的线程池以及数据库的连接池等,总结下单例模式的使用场景
1. 需要频繁创建和销毁的对象
2. 需要频繁访问文件或数据库的对象
3. 有状态的工具类