设计模式:单例模式
1.基本概念
目的: 保证类在内存中只有一个对象,可以直接访问,不需要实例化该类的对象
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
使用场景:
1、要求生产唯一序列号。
2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
注意事项: getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
2.代码实现
2.1.饿汉式(即时创建对象)
这种方式比较常用,但容易产生垃圾对象。空间换时间。
是否多线程安全: 是
实现难度: 易
优点: 没有加锁,执行效率会提高。
缺点: 类加载时就初始化,浪费内存。
public class Main {
public static void main(String[] args) {
//通过方法获得唯一对象
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
//判断是否为唯一对象
System.out.println(s1 == s2);//运行结果为true
}
}
class Singleton {
//1.私有构造方法,其他类不能访问该构造方法
private Singleton() {}
//2.创建本类对象
private static Singleton s = new Singleton();
//3.对外提供公共的访问方法
public static Singleton getInstance() {//获取实例
return s;
}
}
2.2.懒汉式(使用时才创建对象)
2.2.1.线程不安全的懒汉式
这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式,不要求线程安全,在多线程不能正常工作。
是否多线程安全: 否
实现难度: 易
public class Main {
public static void main(String[] args) {
//通过方法获得唯一对象
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
class SingletonLazy {
//1.私有构造方法,其他类不能访问该构造方法
private SingletonLazy() {}
//2.声明一个引用
private static SingletonLazy s;
//3.对外提供公共的访问方法
public static SingletonLazy getInstance() {//获取实例
//当没有对象时,进行创建
if(s==null){
s = new SingletonLazy();
}
return s;
}
}
2.2.2.线程安全的懒汉式
这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
是否多线程安全: 是
实现难度: 易
优点: 第一次调用才初始化,避免内存浪费。
缺点: 必须加锁 synchronized 才能保证单例,但加锁会影响效率。
public class Main {
public static void main(String[] args) {
//通过方法获得唯一对象
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
class SingletonLazy {
//1.私有构造方法,其他类不能访问该构造方法
private SingletonLazy() {}
//2.声明一个引用
private static SingletonLazy s;
//3.对外提供公共的访问方法(使用synchronized锁,防止线程抢占)
public static synchronized SingletonLazy getInstance() {//获取实例
//当没有对象时,进行创建
if(s==null){
s = new SingletonLazy();
}
return s;
}
}
2.3.final实现方式(了解)
通过final关键字,实现对象在创建之后不可被更改。
public class Main {
public static void main(String[] args) {
//获得唯一对象
Singletonfinal s1 = Singletonfinal.s;
Singletonfinal s2 = Singletonfinal.s;
System.out.println(s1 == s2);//结果为true
}
}
class Singletonfinal{
//1.私有构造方法,其他类不能访问该构造方法
private Singletonfinal() {}
//2.声明一个引用,final表示了地址不可被修改
public static final Singletonfinal s = new Singletonfinal();
}
3.双重校验锁(DCL,即double-checked locking)
3.1.DCL代码实现
是否 Lazy 初始化: 是
是否多线程安全: 是
实现难度: 较复杂
描述: 这种方式采用双锁机制,安全且在多线程情况下能保持高性能。getInstance() 的性能对应用程序很关键。
public class Main {
public static void main(String[] args) {
SingletonDCL s1 = SingletonDCL.getInstance();
SingletonDCL s2 = SingletonDCL.getInstance();
System.out.println(s1 == s2);//结果是true
}
}
class SingletonDCL {
//1.构造私有方法
private SingletonDCL() {}
//2.懒汉式Lazy初始化,使用volatile可以保证可见性,也禁止指令重排序
private volatile static SingletonDCL singletonDCL;
//3.提供外部获得实例的方法
public static SingletonDCL getInstance() {
//如果实例未创建,第一重
if (singletonDCL == null)
synchronized (SingletonDCL.class) {//通过字节码,反射机制获得对象,并上锁
//加上锁后再次判断,第二重
if (singletonDCL == null) {
singletonDCL = new SingletonDCL();
}
}
return singletonDCL;//返回对象
}
}
3.2.使用volatile的原因(禁止指令重排序)
指令重排序: 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
例如:
singletonDCL = new SingletonDCL();//实例化对象这行代码
这行代码实际上可以分解成三个步骤:
1.分配内存空间。
2.初始化对象。
3.将对象指向刚分配的内存空间。
但是有些编译器因为性能的原因,可能会改变2和3的顺序,就成了:
1.分配内存空间。
2.将对象指向刚分配的内存空间。
3.初始化对象。
在不使用volatile且发生重排序的情况下,调用顺序如下
线程一 | 线程二 |
---|---|
检查到singletonDCL为null | - |
获取锁 | - |
再次检查到singletonDCL为null | - |
为singletonDCL分配内存空间 | - |
将singletonDCL指向内存空间 | - |
- | 检查到singletonDCL不为空 |
- | 访问singletonDCL(此时线程一还未初始化完成对象) |
初始化singletonDCL | - |
在这种情况下,在线程二访问singletonDCL时,访问的是一个初始化未完成的对象。
4.如何通过反射破坏双重校验锁(抬杠)
4.1.1.通过反射无视私有构造器创建对象
public class Main {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
SingletonDCL s1 = SingletonDCL.getInstance();
SingletonDCL s2 = SingletonDCL.getInstance();
System.out.println(s1);
System.out.println(s2);
//---------------以下为添加的部分,SingletonDCL部分未改动--------------------
//通过字节码,获得SingletonDCL的空参构造器
Constructor<SingletonDCL> declaredConstructor = SingletonDCL.class.getDeclaredConstructor(null);
//无视了私有的构造器
declaredConstructor.setAccessible(true);
//通过反射创建SingletonDCL对象
SingletonDCL s3 = declaredConstructor.newInstance();
//输出对象
System.out.println(s3);
}
}
class SingletonDCL {
//1.构造私有方法
private SingletonDCL() {}
//2.懒汉式Lazy初始化,使用volatile可以保证可见性,也禁止指令重排序
private volatile static SingletonDCL singletonDCL;
//3.提供外部获得实例的方法
public static SingletonDCL getInstance() {
//如果实例未创建,第一重
if (singletonDCL == null)
synchronized (SingletonDCL.class) {//通过字节码,反射机制获得对象,并上锁
//加上锁后再次判断,第二重
if (singletonDCL == null) {
singletonDCL = new SingletonDCL();
}
}
return singletonDCL;//返回对象
}
}
运行结果:可见s1,s2为同一个对象,通过反射走对象空构造器创建的s3为不同的对象。
4.1.2.如何防止通过4.1.1反射机制创建对象
思路:如果对象已经走getInstance()进行创建,则可以在空构造器中加入对象判断,防止通过反射再次创建。
public class Main {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
SingletonDCL s1 = SingletonDCL.getInstance();
SingletonDCL s2 = SingletonDCL.getInstance();
System.out.println(s1);
System.out.println(s2);
//通过字节码,获得SingletonDCL的空参构造器
Constructor<SingletonDCL> declaredConstructor = SingletonDCL.class.getDeclaredConstructor(null);
//无视了私有的构造器
declaredConstructor.setAccessible(true);
//通过反射创建对象
SingletonDCL s3 = declaredConstructor.newInstance();
System.out.println(s3);
}
}
class SingletonDCL {
//1.构造私有方法
private SingletonDCL() {
synchronized (SingletonDCL.class){
//---------------以下为添加的部分,增加第三重检测--------------------
//第三重检测,如果对象singletonDCL已被创建,则可认为有人想通过反射进行创建
if(singletonDCL!=null){
throw new RuntimeException("使用反射创建对象失败");
}
}
}
//2.懒汉式Lazy初始化,使用volatile可以保证可见性,也禁止指令重排序
private volatile static SingletonDCL singletonDCL;
//3.提供外部获得实例的方法
public static SingletonDCL getInstance() {
//如果实例未创建,第一重
if (singletonDCL == null)
synchronized (SingletonDCL.class) {//通过字节码,反射机制获得对象,并上锁
//加上锁后再次判断,第二重
if (singletonDCL == null) {
singletonDCL = new SingletonDCL();
}
}
return singletonDCL;//返回对象
}
}
运行结果是这样的,原因是在一开始getInstance()的时候已经存在了singletonDCL对象,在第三重检测的时候,发现对象已经创建,所以就会走Exception。
4.1.3.使用多次反射破坏双重校验锁(杠到底)
在4.1.2的方法中,第三重校验能成功,是因为在一开始走了getInstance()方法,已经创建了singletonDCL对象,所以通过反射走空构造器的时候会失败。但是如果只用反射创建对象,singletonDCL对象并没有存储,得到的依旧是非单例的结果。
public class Main {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//通过字节码,获得SingletonDCL的空参构造器
Constructor<SingletonDCL> declaredConstructor = SingletonDCL.class.getDeclaredConstructor(null);
//无视了私有的构造器
declaredConstructor.setAccessible(true);
//通过反射创建对象
SingletonDCL s3 = declaredConstructor.newInstance();
SingletonDCL s4 = declaredConstructor.newInstance();
System.out.println(s3);
System.out.println(s4);
}
}
class SingletonDCL {
//1.构造私有方法
private SingletonDCL() {
synchronized (SingletonDCL.class){
//第三重检测,如果对象singletonDCL已被创建,则可认为有人想通过反射进行创建
if(singletonDCL!=null){
throw new RuntimeException("使用反射创建对象失败");
}
}
}
//2.懒汉式Lazy初始化,使用volatile可以保证可见性,也禁止指令重排序
private volatile static SingletonDCL singletonDCL;
//3.提供外部获得实例的方法
public static SingletonDCL getInstance() {
//如果实例未创建,第一重
if (singletonDCL == null)
synchronized (SingletonDCL.class) {//通过字节码,反射机制获得对象,并上锁
//加上锁后再次判断,第二重
if (singletonDCL == null) {
singletonDCL = new SingletonDCL();
}
}
return singletonDCL;//返回对象
}
}
运行结果:通过反射创建的s3,s4
4.1.4.通过信号量保证单例对象只创建一次
public class Main {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//通过字节码,获得SingletonDCL的空参构造器
Constructor<SingletonDCL> declaredConstructor = SingletonDCL.class.getDeclaredConstructor(null);
//无视了私有的构造器
declaredConstructor.setAccessible(true);
//通过反射创建对象
SingletonDCL s3 = declaredConstructor.newInstance();
System.out.println(s3);
SingletonDCL s4 = declaredConstructor.newInstance();
System.out.println(s4);
}
}
class SingletonDCL {
//---------------以下为添加的部分--------------------
//定义一个对象是否创建的判断信号
private static boolean charge= false;
//1.构造私有方法
private SingletonDCL() {
System.out.println("走空构造器");
synchronized (SingletonDCL.class){
//---------------以下为添加的部分--------------------
//无论走反射或者是get方法,创建一次对象之后,此信号就会置为true
if (charge ==false){
charge =true;
}else {
throw new RuntimeException("使用反射创建对象失败");
}
}
}
//2.懒汉式Lazy初始化,使用volatile可以保证可见性,也禁止指令重排序
private volatile static SingletonDCL singletonDCL;
//3.提供外部获得实例的方法
public static SingletonDCL getInstance() {
//如果实例未创建,第一重
if (singletonDCL == null)
synchronized (SingletonDCL.class) {//通过字节码,反射机制获得对象,并上锁
//加上锁后再次判断,第二重
if (singletonDCL == null) {
singletonDCL = new SingletonDCL();
}
}
return singletonDCL;//返回对象
}
}
运行结果:在第二次走空构造器的时候,由于charge为true,所以走else报错。
5.通过枚举实现单例模式
枚举是一种自带单例模式的类型,具体实现可以点进源码了解。
public enum Singleton {
//仅枚举INSTANCE
INSTANCE;
}
class Main2{
public static void main(String[] args) {
System.out.println(Singleton.INSTANCE);
System.out.println(Singleton.INSTANCE);
}
}
运行结果:显而易见,只能为单例
6.总结
在4.1.4.的方法可以通过反射破坏私有,获得charge变量,将其值改为false再次进行对象创建。因此看来似乎并没有绝对安全的代码。单例实现过程的话可以考虑看看枚举类型->enum的源码。另一方面,反序列化也可以破坏双重校验锁的单例实现。可谓道高一尺魔高一丈。