单例模式:毫无疑问就是保证整个应用中对象的实例只有一个。然而单例模式有几种写法,每种写法的优缺点以及问题的由来等你又知道吗?
1、饿汉式
这种方式比较简单,在类加载的时候完成初始化,避免线程安全问题
package com.my.test.design.singleinstance;
public class SingleInstance1 {
private static SingleInstance1 s1 = new SingleInstance1();
private SingleInstance1() {}
public static SingleInstance1 getInstnce() {
return s1;
}
}
饿汉式变体
public class SingleInstance1 {
private static SingleInstance1 s1;
private SingleInstance1() {}
// 构造块作用:给对象进行初始化,对象一建立就执行,而且优先于构造函数执行 // 和构造函数区别:构造块是给所有不同对象统一初始化,构造函数是给对应的对象进行初始化 static {
s1 = new SingleInstance1();
}
public static SingleInstance1 getInstnce() {
return s1;
}
}
2、懒汉式
在需要的时候才会去初始化,明显的缺点是在单线程的环境下是没问题的,但是在多线程的情况单例
对象可能会被实例化好几次
package com.my.test.design.singleinstance;
public class SingleInstance2 {
// volatile关键字防止指令重排
private volatile static SingleInstance2 s2;
private SingleInstance2() {}
// 不能保证多线程环境下单例
public static SingleInstance2 getInstance1() {
if (s2 == null) {//如果多个线程同时执行到这里那么这个对象会被实例化好几次
s2 = new SingleInstance2();
}
return s2;
}
// 方法加锁避免线了程安全问题,但是如果该方法被多个线程频繁调用将会导致程序的性能下降
// 锁的粒度比较大效率低
synchronized public static SingleInstance2 getInstance2() {
if (s2 == null) {
s2 = new SingleInstance2();
}
return s2;
}
// 鉴于以上的2个问题才有双重锁机制,双重锁机制并不是完美无缺的,用双重锁机制实例化的对象必须要用
// volatile 关键字修饰,因为为了提高程序性能编译器和处理器常常会对指令做重排序,指令重排在单线
// 程环境下没有问题,但是在多线程环境下有可能发生问题
public static SingleInstance2 getInstnce3() {
if (s2 == null){
synchronized (SingleInstance2.class){
if (s2 == null){
// 其实实例化一个对象在指令层面需要三个步骤
// 1、给对象分配内存空间
// 2、实例化对象
// 3、将实例化对象指向内存空间
// 其中2和3可能会出现指令重排,导致线程A其实并没有实例化完成,
// 但是该对象已经指向内存,另一个线程B判断
// if (s2 == null)为false直接退出,此时的singleton 可能并没有初始化完成
// 线程B拿到一个未实例化完成的对象去操作就会有问题
s2 = new SingleInstance2();
}
}
}
return s2;
}
}
s2 = new SingleInstance2();// 这句话大概包含三行代码
memory = allocate(); // 1、给对象分配内存空间
ctorInstance(memory) // 2、实例化对象
instance = memory; // 3、将实例化对象指向内存空间其中2和3可能重排序
3、内部类机制
这是比较推荐的一种方式,首先外部类无法访问内部类的SINGLETON,只能通过Singleton3的getInstance方法
初始化时机并不是Singleton3被加载的时候,而是在调用getInstance方法的时候
package com.my.test.design.singleinstance;
public class SingleInstance3 {
private SingleInstance3 (){}
public static SingleInstance3 getInstance(){
return Instance.s3;
}
private static class Instance {
private static SingleInstance3 s3 = new SingleInstance3();
}
}
4、枚举方式
类的构造器只能被一个线程在类加载的初始化阶段执行,所以枚举类每个实例在Java堆中只有一个副本
package com.my.test.design.singleinstance;
public enum SingleInstance4 {
INSTANCE;
}
测试类
package com.my.test.design.singleinstance;
public class Test {
public static void main(String[] args) {
// 饿汉式
SingleInstance1 s1 = SingleInstance1.getInstnce();
// 懒汉式->双重锁
SingleInstance2 s2 = SingleInstance2.getInstnce3();
// 内部类
SingleInstance3 s3 = SingleInstance3.getInstance();
// 枚举
SingleInstance4 s4 = SingleInstance4.INSTANCE;
}
}
通常我们创建一个对象有以下几个方法
1、new Object()
2、反射
class.forName("xxxxx")
类.class;
new Object.getClass();
3、克隆
4、反序列化
我们可以利用反射破坏单例模式,单例模式的本质是,构造函数私有化,禁止外部通过new的方式创建对象,但是反射可以获得构造器,并且设置setAccessible(true);
无论是饿汉式、懒汉式、还是双重锁机制亦或者是内部类都可以使用反射机制破坏,但是枚举的方式是不行的,有兴趣的话,你们可以自己做一下实验就能看到