Java 单例模式
什么是单例模式?
单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。
许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
单例模式有以下特点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
饿汉式(静态变量)
package com.example.demo.module;
/**
* 饿汉式单例模式 静态变量
*/
public class SingleHungryVariable {
private static SingleHungryVariable singleHungryVariable = new SingleHungryVariable();
private SingleHungryVariable(){}
public static SingleHungryVariable getInstance(){
return singleHungryVariable;
}
}
饿汉式(静态代码块)
package com.example.demo.module;
/**
* 饿汉式单例模式 静态代码块
*/
public class SingleHungryConstruction {
private static SingleHungryConstruction singleHungryConstruction;
static {
singleHungryConstruction = new SingleHungryConstruction();
}
private SingleHungryConstruction(){}
public static SingleHungryConstruction getInstance(){
return singleHungryConstruction;
}
}
懒汉式
package com.example.demo.module;
/**
* 懒汉式单例模式
*/
public class SingleLazy {
private SingleLazy(){}
private static SingleLazy singleLazy;
public static SingleLazy getInstance(){
if(singleLazy == null){ //(1)
singleLazy = new SingleLazy();
}
return singleLazy;
}
}
从代码可以看到,懒汉式单例实例是延迟加载,起到了Lazy Loading的效果,只有在真正使用的时候才会实例化一个对象。
这是最简单的懒汉式单例写法,是线程不安全的,只能在单线程下使用,多线程下会出现创建多个实例的情况。
测试代码
package com.example.demo.module.user;
import com.example.demo.module.SingleLazy;
public class SingleTest {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(SingleLazy.getInstance().hashCode());
}).start();
}
}
}
运行结果
可以看到,多线程情况下获取到的实例存在不一样的情况。
分析:
多线程运行下,会出现 线程1执行到(1)处的时候,线程2抢到cpu执行权,执行getInstance()
方法此时实例并未进行初始化,所以线程2执行了singleLazy = new SingleLazy();
进行创建实例,但是线程1并不知道,等线程1重新夺得cpu执行权,继续从(1)处继续执行,线程1又执行了一次singleLazy = new SingleLazy();
实例创建,所以出现了多个线程下获取到的实例不一样的情况。
对上面懒汉式进行修改,加上double check lock(DCL) 双重校验锁。
package com.example.demo.module;
/**
* 懒汉式单例模式
*/
public class SingleLazy {
private SingleLazy(){}
private static SingleLazy singleLazy;
public static SingleLazy getInstance(){
if(singleLazy == null){
synchronized (SingleLazy.class){
if(singleLazy == null){
singleLazy = new SingleLazy();
}
}
}
return singleLazy;
}
}
这种写法还存在一个问题,就是执行singleLazy = new SingleLazy();
时会出现指令重排的问题。
什么是指令重排?
在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。
执行singleLazy = new SingleLazy();
时究竟做了哪些事情呢?
1.为对象分配内存空间:然后执行new SingleLazy() ,这里会根据SingleLazy类元信息先确定对象的大小,向JVM堆中申请一块内存区域并构建对象。
2.初始化对象:执行对象内部生成的init方法,初始化成员变量值,同时执行搜集到的{}代码块逻辑,最后执行对象构造方法。
3.引用对象:对象实例化完毕后,再把栈中的Person对象引用地址指向Person对象在堆内存中的地址。
其中步骤 2,3 不存在依赖关系,执行顺序发生变更都不会影响到最终对象的创建,所以步骤 2,3 执行顺序是可以发生改变的,执行顺序可能是 1-2-3,也可以是 1-3-2。
分析多线程懒汉式单例指令重排不安全问题:
public static SingleLazy getInstance(){
if(singleLazy == null){//(2)
synchronized (SingleLazy.class){
if(singleLazy == null){
singleLazy = new SingleLazy();
}
}
}
return singleLazy;
}
多线程运行下,会出现 线程1执行singleLazy = new SingleLazy();
执行步骤为 1-3-2 ,当执行完步骤3还没执行步骤2时,singleLazy
对象已经不为空但并未进行初始化,此时线程2抢到cpu执行权,执行到(2)非空判断为false,指向返回一个没初始化的对象。
为了避免指令重排,我们可以使用 volatile
关键字。
懒汉式单例最终版
package com.example.demo.module;
/**
* 懒汉式单例模式
*/
public class SingleLazy {
private SingleLazy(){}
private static volatile SingleLazy singleLazy;
public static SingleLazy getInstance(){
if(singleLazy == null){
synchronized (SingleLazy.class){
if(singleLazy == null){
singleLazy = new SingleLazy();
}
}
}
return singleLazy;
}
}
静态内部类方式
package com.example.demo.module;
public class SingleStatic {
private SingleStatic(){}
private static class SingleHoler{
private static SingleStatic instance = new SingleStatic();
}
public static SingleStatic getInstance(){
return SingleHoler.instance;
}
}
静态内部类的优点是:
外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化instance,故而不占内存。具体来说当SingleStatic第一次被加载时,并不需要去加载SingleHoler,只有当getInstance()方法第一次被调用时,使用instance的时候,才会导致虚拟机加载SingleHoler类。这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
那么他是如何实现线程安全的?
首先要了解类加载过程中的最后一个阶段:即类的初始化,类的初始化阶本质就是执行类构造器的 <clinit>
方法。
<clinit>
方法:这不是由程序员写的程序,而是根据代码由javac编译器生成的。它是由类里面所有的类变量的赋值动作和静态代码块组成的。JVM内部会保证一个类的<clinit>
方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的<clinit>
方法,其他的线程都要阻塞等待,直到这个线程执行完<clinit>
方法。然后执行完<clinit>
方法后,其他线程唤醒,但是不会再进入<clinit>()
方法。也就是说同一个加载器下,一个类型只会初始化一次。
那么回到这个代码中,这里的静态变量的赋值操作进行编译之后实际上就是一个<clinit>
代码,当我们执行getInstance方法的时候,会导致SingleHoler类的加载,类加载的最后会执行类的初始化,但是即使在多线程情况下,这个类的初始化的<clinit>
代码也只会被执行一次,所以他只会有一个实例。
那么再增加一句,之所以这里变量定义的时候不需要volatile,因为只有一个线程会执行具体的类的初始化代码<clinit>
,也就是即使有指令重排序,因为根本没有第二个线程给你去影响,所以无所谓。
以上无论是饿汉式还是懒汉式,都可以用反射机制破坏创建多个对象。
反射破坏单例
测试代码
package com.example.demo.module;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 懒汉式单例模式
*/
public class SingleLazy {
private SingleLazy(){}
private static volatile SingleLazy singleLazy;
public static SingleLazy getInstance(){
if(singleLazy == null){
synchronized (SingleLazy.class){
if(singleLazy == null){
singleLazy = new SingleLazy();
}
}
}
return singleLazy;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<SingleLazy> constructor = SingleLazy.class.getDeclaredConstructor();
constructor.setAccessible(true);
SingleLazy test1 = constructor.newInstance();
SingleLazy test2 = constructor.newInstance();
System.out.println(test1);
System.out.println(test2);
System.out.println(test1 == test2);
}
}
运行结果
可以看到,通过反射机制成功破坏了单例,创建出的两个对象不一样。
序列化反序列化方式破坏单例
测试代码
package com.example.demo.module;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 懒汉式单例模式
*/
public class SingleLazy implements Serializable {
private SingleLazy(){}
private static volatile SingleLazy singleLazy;
public static SingleLazy getInstance(){
if(singleLazy == null){
synchronized (SingleLazy.class){
if(singleLazy == null){
singleLazy = new SingleLazy();
}
}
}
return singleLazy;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
SingleLazy singleLazy = SingleLazy.getInstance();
System.out.println(singleLazy);
//将得到的实例序列化到磁盘
FileOutputStream fileOutputStream = new FileOutputStream("SingleLazy.obj");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(singleLazy);
objectOutputStream.flush();
objectOutputStream.close();
//从磁盘反序列化得到实例
FileInputStream fileInputStream = new FileInputStream("SingleLazy.obj");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
SingleLazy singleLazy1 = (SingleLazy) objectInputStream.readObject();
System.out.println(singleLazy1);
}
}
运行结果
可以看到,通过序列化方式成功破坏了单例,创建出的两个对象不一样。
枚举单例
由于枚举类先天就是线程安全,且每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,正好可以用来完成单例模式。
package com.example.demo.module;
public enum SingleEnum {
INSTANCE;
}
测试反射破坏枚举
查看枚举构造方法
package com.example.demo.module;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum SingleEnum {
INSTANCE;
public static void main(String[] args) {
Constructor<?>[] declaredConstructors = SingleEnum.class.getDeclaredConstructors();
for (Constructor<?> declaredConstructor : declaredConstructors) {
System.out.println(declaredConstructor);
}
}
}
运行结果显示有2个参数,这是父类构造函数参数,因为enum类默认继承Enum,它要帮父类构造函数。
通过反射构建枚举实例
package com.example.demo.module;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum SingleEnum {
INSTANCE;
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<SingleEnum> constructor = SingleEnum.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
SingleEnum test1 = constructor.newInstance("a",1);
System.out.println(test1);
}
}
运行结果
构建失败,抛出了java.lang.IllegalArgumentException: Cannot reflectively create enum objects
异常。
查看反射源码
可以发现,只要类为枚举类,反射构建实例时会抛出异常。
总结
在饿汉式和懒汉式使用时需要注意构造器私有化,防止外部访问。
懒汉式中需要注意的是,volatile关键字的使用。
在单例模式中,由于反射机制和多线程的存在枚举是最安全的,但是懒汉式和饿汉式也不是不能用,需要根据实际的情况来灵活的选择。
内容如有帮助,记得点赞收藏哦~