设计模式-单例模式

文章目录

单例模式

  1. 单例类只能有一个实例
  2. 单例类必须自己创建自己的唯一实例
  3. 单例类必须给所有其它对象提供这一实例

  • 懒汉:在初始化类的时候,不创建唯一的实例,而是等到真正需要用到的时候才创建。必须加上同步,否则有可能依然创建多个实例。
  • 饿汉:在初始化的时候,就创建了唯一的实例,不管是否需要用到。不需要自己加同步,一定产生唯一的实例。

如何保证单例不能被new出来?

单例类的构造方法必须是私有的,这样确保是不能被new出来的

懒汉式 + 非同步

java 复制代码
final class Singleton{
    private byte[] data = new byte[1024];

    private static Singleton instance = null;

    public static Singleton getInstance(){
        if (null == instance) {
            System.out.println("new Singleton()");
            instance = new Singleton();
        }
        return instance;
    }

}

Singleton.class初始化的时候,instance不会实例化,getInstance()方法内部会实例化,但是多线程下会出现new Singleton()多次的情况,因为某个时间可能多个线程看到的instance都是null, 这使得实例并不唯一

java 复制代码
// 20个线程简单测试
for(int i=0;i<20;i++){
    new Thread(()-> Singleton.getInstance()).start();
}

懒汉式 + synchronized同步

getInstance同一时刻只能被一个线程访问,效率很低

java 复制代码
final class Singleton{
    private byte[] data = new byte[1024];

    private static Singleton instance = null;

    public static synchronized Singleton getInstance(){
        if(null == instance){
            System.out.println("new Singleton");
            instance = new Singleton();
        }
        return instance;
    }

}

double-check(仍然线程不安全)

首次初始化的时候加锁,之后多线程调用getInstance

java 复制代码
final class Singleton{
    private byte[] data = new byte[1024];

    private static Singleton instance = null;
    String conn;
    Socket socket;

    private Singleton(){
        System.out.println("Singleton constructor init");
        try {
            TimeUnit.SECONDS.sleep(2);
        }catch (Exception e){
            e.printStackTrace();
        }
        this.conn = new String();
        this.socket = new Socket();
    }

    public static Singleton getInstance(){
        if(null == instance){
            synchronized (Singleton.class) {
                if(null == instance) {
                    System.out.println("new Singleton");
                    instance = new Singleton(); // new 对象 可能会指令重排
                }
            }
        }
        return instance;
    }
}

创建一个变量需要:一个是申请一块内存,调用构造方法进行初始化操作,另一个是分配一个指针指向这块内存。这两个操作谁在前,谁在后呢?JVM规范并没有规定。那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。

double-check + volatile(实现 懒汉 且 线程安全)

volatile保证顺序性、可见性,不保证原子性(是非线程安全的)

java 复制代码
private volatile static Singleton instance = null;

public static Singleton getInstance(){
    if(null == instance){  // check 1
        synchronized (Singleton.class) {
            if(null == instance) { // check 2
                System.out.println("new Singleton");
                instance = new Singleton(); // new 对象 可能会指令重排, 获取到的实例是个半成品
            }
        }
    }
    return instance;
}

double check 单例模式需要 volatile 吗?

正确答案:需要

Object o = new Object();的汇编指令

java 复制代码
0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>
7 astore_1
8 return

隐含一个对象创建的过程:(记住3步)

  1. 堆内存中申请了一块内存(new指令)【半初始化状态,成员变量初始化为默认值】
  2. 这块内存的构造方法执行(invokespecial指令)
  3. 栈中变量建立连接到这块内存(astore_1指令)

问题:由于指令重排和半初始化状态,导致多线程会使用半初始化的对象

Holder方式(静态内部类:线程安全 & 懒汉式)

java 复制代码
final class Singleton {

    private Singleton() {
    }

    private static class Holder {
        private static Singleton singleton = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.singleton;
    }

}

回顾:什么时候需要对类进行初始化

  1. 使用new该类实例化对象的时候;
  2. 读取或设置类静态字段的时候(但被final修饰的字段,在编译器时就被放入常量池(static final)的静态字段除外);
  3. 调用类静态方法的时候;
  4. 使用反射Class.forName("xxx")对类进行反射调用的时候,该类需要初始化;
  5. 初始化一个类的时候,有父类则先初始化父类(注:1.接口除外,父接口在调用的时候才会被初始化;2.子类引用父类静态字段,只会引发父类初始化);
  6. 被标明为启动类的类(即包含main()方法的类)要初始化;
  7. 当使用JDK1.7的动态语言支持时,如果一个java.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上情况称为对一个类进行主动引用,且有且只要以上几种情况是需要对类进行初始化:

  • 所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是<clinit>方法,即类/接口初始化方法,该方法只能在类加载的过程中由JVM调用。

  • 编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量;

  • 如果超类还没有被初始化,那么优先对超类初始化,但在<clinit>方法内部不会显示调用超类的<clinit>方法,由JVM负责保证一个类的<clinit>方法执行之前,它的超类<clinit>方法已经被执行。

  • JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其它线程。(所以可以利用静态内部类实现线程安全的单例模式)。

  • 如果一个类没有声明任何的类变量,也没有静态代码块,那么可以没有类<clinit>方法。

懒加载+线程安全的说明以及缺点

  • Singleton构造函数是私有的,其初始化的时候必须调用getInstance方法,否则不会被类加载

  • 当调用getInstance时,会初始化Holder实例;而在Java程序类加载的编译时期<clinit>()方法中,该方法是一个同步方法;类里面的静态变量是初始化一次后放在常量池中的

缺点:

  1. 可能会增加类的数量,因为需要一个静态内部类来保存单例对象
  2. 第一次加载时可能会略微增加启动时间
  3. 无法传递参数到静态内部类中

枚举

枚举类型不允许被继承,同样是线程安全的且只能被实例化一次,但是枚举类型不能够实现懒加载

补充:防止反射

单例模式还要考虑的点:防止反射

java 复制代码
public enum Singleton {
    /**
     * 单例
     */
    INSTANCE;

    public void doSome(){
        System.out.println(INSTANCE.hashCode());
    }
}

单例模式测试代码

java 复制代码
import java.lang.reflect.Constructor;

final class Singleton {

    private Singleton() {
    }

    private static class Holder {
        private static Singleton singleton = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.singleton;
    }

    public String say(String what){
        return "single:" + what;
    }

}

public class TestSingle {

    public static void main(String[] args) throws Exception{
        test3();
    }

    public static void test1() throws Exception{
        Singleton singleton = Singleton.getInstance();
        Singleton singleton1 = Singleton.getInstance();
        System.out.println(singleton + ":" + singleton.say("hello"));
        System.out.println(singleton1 + ":" + singleton1.say("hello1"));
    }

    public static void test2() throws Exception{
        new Thread(()->{
            Singleton singleton = Singleton.getInstance();
            System.out.println(singleton + ":" + singleton.say("hello"));
        }, "tA").start();

        new Thread(()->{
            Singleton singleton2 = Singleton.getInstance();
            System.out.println(singleton2 + ":" + singleton2.say("hello"));
        }, "tB").start();
    }

    public static void test3() throws Exception{
        Class clazz = Singleton.class;
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton singleton = (Singleton) constructor.newInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton + ":" + singleton.say("hello"));
        System.out.println(singleton2 + ":" + singleton2.say("hello"));
    }

}
相关推荐
会编程的吕洞宾10 分钟前
Spring Boot多环境配置实战 配置文件加载顺序与切换不再翻车
java·后端
额鹅恶饿呃13 分钟前
随着CentOS官方停服的时间越来越久,大量仍在使用CentOS7的企业和运维从业者
java·python·算法·c#·ruby
秋名RG18 分钟前
Java 异常处理全攻略:从入门到实战(JDK 21 版)
java·开发语言
SQL-First布道者27 分钟前
我为什么把 MyBatis 从项目中删了?
java·spring·tomcat·mybatis·mybatis plus·spring jdbc
公爵爱学习30 分钟前
无人机多点导航笔记
java·前端·笔记
萧瑟余晖1 小时前
Java深入解析篇五十五之Unsafe 机制详解
java
kakawzw1 小时前
mybatis源码笔记1——JDBC和整体架构
java·mybatis
sunshine22 girl1 小时前
Java学习一 环境配置1 安装JDK,配置环境变量
java·开发语言·学习
秋饼1 小时前
Spring AI Session API 深度实战:从 ChatMemory 平滑迁移到事件溯源的企业级短期记忆
java·ai·技术分享·后端开发
QCodingDev1 小时前
Spring AI 2.0企业级RAG实战:引用校验、无依据拒答与知识治理怎么做?
java·人工智能·spring·ai