闲聊·java中的单例模式

前言

        经常在一些面试题中遇到这样的面试官:请你手写下单例模式。面试者如果没有准备或者提前储备不足的话,就会陷入一种尴尬的境地,单例模式?记得好像有懒汉式和饿汉式,手写?糟糕...记得不太清了。

一、什么是单例模式?

        在Java中,单例模式是一种设计模式,用于确保一个类只有一个实例,并提供全局访问点以访问该实例。它通常用于需要共享资源的情况,例如数据库连接池、线程池以及常见的配置信息等。在实际应用中,我们常遇到的单例模式,有“懒汉式”,“饿汉式”,“枚举类”等

二、单例模式能解决什么问题?

1. 限制一个类只能有一个实例:通过单例模式,可以确保一个类只有一个实例对象被创建,并且提供一个全局访问点以供其他类使用。

2. 提供对实例的全局访问:单例模式可以使实例对象在程序的任何地方都可以被访问,方便了对象之间的通信和数据共享。

3. 控制资源的共享和访问:通过单例模式,可以对某些共享资源进行控制,避免资源的重复创建和浪费。

4. 避免全局变量的使用:通过单例模式,可以避免将变量定义为全局变量,减少了对全局变量的依赖和使用带来的问题。

5. 实现延迟加载:通过单例模式,可以在需要时才创建实例对象,避免了一开始就进行实例化的开销。

6. 简化代码:通过单例模式,可以将一些需要共享的方法和数据封装在单个类中,简化了代码的逻辑和结构。

三、常见的单例模式有哪几种?

        懒汉式

        饿汉式

        枚举类

四、先上菜:解锁最完美的单例模式

/**
 * 饿汉式
 */
class Demo {
    /**
     * 私有化构造器
     */
    private Demo() {}

    /**
     * 创建对象
     */
    private static class DemoIn{
        private static Demo INSTANCE = new Demo();
    }
    /**
     * 获取实例的方法
     */
    public static Demo getInstance() {
        return DemoIn.INSTANCE;
    }
}


这段Java代码定义了一个`Demo`类来实现单例模式。它通过将构造器私有化,阻止外部类直接实例化该类。然后,内部定义了一个静态内部类`DemoIn`,在这个静态内部类中创建了`Demo`类的唯一实例`INSTANCE`,最后通过`getInstance`方法对外提供获取这个唯一实例的途径。


1.类加载阶段的初始化保障:
在Java中,类的加载过程是由类加载器来完成的,并且类加载器会保证一个类在整个Java虚拟机中只会被加载一次。对于静态成员变量(如这里的`DemoIn`类中的`INSTANCE`)的初始化,是在类加载阶段完成的,并且Java的类加载机制会确保类的静态初始化在多线程环境下是线程安全的。

当一个类第一次被主动使用(比如通过`getInstance`方法第一次调用去访问`DemoIn.INSTANCE`时触发`DemoIn`类的加载和`INSTANCE`的初始化),JVM会保证这个过程的原子性,多个线程同时尝试访问这个单例实例时,只有一个线程会实际触发类的初始化过程(也就是创建`INSTANCE`这个实例),其他线程会阻塞等待这个初始化完成,然后获取到同一个已经初始化好的实例。

2.获取实例方法的线程安全性:
`getInstance`方法只是简单地返回已经初始化好的`DemoIn.INSTANCE`,不存在对共享变量的修改等并发操作,不存在多个线程同时修改导致数据不一致等问题。只要类的初始化过程(也就是实例的创建过程)是线程安全的,那么通过这个方法获取实例在多线程环境下就是安全的。

五、集中常见的单例写法

1、“饿汉式”

        所谓饿汉式,即,上来就创建对象,像饿了好几天一样。

public class SingletonHungry {
    /**
     * 单例比较关键的一句就是:先私有化构造器.这样外部就不可能通过new一个对象,来构造对象.
     */
    private SingletonHungry(){};

    private final static SingletonHungry INSTANCE = new SingletonHungry();

    public static SingletonHungry getInstance(){
        return INSTANCE;
    }

    /**
     * 线程运行100次,每次取到的值,都是一样的.
     */
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                SingletonHungry instance = SingletonHungry.getInstance();
                System.out.println(instance.hashCode());
            }).start();
        }
    }
}
1176009558
1176009558
1176009558

  这种单例模式,看起来的确没有我们常常担心的线程安全问题。只不过对于有些强迫症的coder而言,他们会认为,这段代码,先创建实例对象,假如我不用到呢,假如实例要占据的内存比较大,这样是否有浪费空间的嫌疑?额...你说的都对

        那我们先不创建,好叭

2、“懒汉式”
public class SingletonLazy {
    // 1-私有化构造器
    private SingletonLazy() {}

    // 2-先声明一个实例对象
    private volatile static SingletonLazy INSTANCE = null;
    
    // 3-提供获取实例的方法
    public static SingletonLazy getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SingletonLazy();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        // 多线程,看看是否都是同一个对象
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                SingletonLazy instance = SingletonLazy.getInstance();
                System.out.println(instance.hashCode());
            }).start();
        }
    }
}

       这种写法,先声明了一个实例对象,但是并没有给它分配内存空间,很好的避免了“饿汉式”所突出的内存浪费问题。

        不过,多线程环境下,这么写会有问题:

1145699847
1120021779
186050359
186050359
186050359

 我们分析下:

        假如有2个线程A,B,当A执行到:instance == null 的时候,刚要往下走(说明它已经判定 instance == null,为true),B线程拿到了CPU的执行权,也到了这里(instance == null ),此时,instance没有实例过,程序往下走,会创建对象。A线程恰好这时也拿到了,继续往下走,也创建了一个对象。这下,就有了两个实例对象。

        常规思路,考虑给实例化的方法,加个锁:

public class SingletonLazy {
    // 1-私有化构造器
    private SingletonLazy() {}

    // 2-先声明一个实例对象
    private volatile static SingletonLazy INSTANCE = null;

    // 3-提供获取实例的方法
    public static synchronized SingletonLazy getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SingletonLazy();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        // 多线程,看看是否都是同一个对象
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                SingletonLazy instance = SingletonLazy.getInstance();
                System.out.println(instance.hashCode());
            }).start();
        }
    }
}

        这样写,是不是就没有问题呢?

        我想,大家应该知道,同步锁会增加锁竞争,带来系统性能开销,从而导致系统性能下降。

        再仔细看,没有加“synchronized”时,由于多线程判断 instance == null 时,造成了创建多个实例的情况,看前面的多线程结果,只有前面的两个hashCode是不同的,后面拿到的都是同一个实例对象,基于这种情形,是不是可以考虑,将同步锁挪个位置,减少同步锁资源竞争呢?

        看代码:

public static SingletonLazy getInstance() {
        if (INSTANCE == null) {
            synchronized (SingletonLazy.class) {
                INSTANCE = new SingletonLazy();
            }
        }
        return INSTANCE;
    }

        这样写,是不是就完美了呢?答案是否定的。这是因为,当多个线程进入到if条件判断里,虽然有同步锁,但是,线程会依次拿到锁资源,创建对象,再释放锁对象。这么说的话,我们考虑再同步锁里面,再加一层判断:

public static SingletonLazy getInstance() {
        if (INSTANCE == null) {
            synchronized (SingletonLazy.class) {
                if(INSTANCE == null) {
                    INSTANCE = new SingletonLazy();
                }
            }
        }
        return INSTANCE;
    }

        如上,就是典型的单例模式中常被提到的“双重校验锁”写法。这种写法,多数时候,程序不会进入到同步锁,从而减少竞争,提高效率,又不浪费空间,几乎完美。总结好处就是:

  • 实现了延迟加载
  • 解决了线程并发
  • 提高了执行效率

        不过这种写法,已然存在潜在的风险。

        这里就不得不提到Java中的指令重排优化和 Happens-Before 规则。所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化

        这个问题的关键就在于由于指令重排优化的存在,导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。

        这个问题,在JDK1.5解决了,官方引入了关键字:volatile

        

// 懒汉模式 + synchronized 同步锁 + double-check
public final class SingletonLazy {
    private volatile static SingletonLazy instance= null;// 不实例化
    public List<String> list = null;//list 属性
    private SingletonLazy(){
      list = new ArrayList<String>();
    }// 构造函数
    public static SingletonLazy getInstance(){// 加同步锁,通过该函数向整个系统提供实例
        if(null == instance){// 第一次判断,当 instance 为 null 时,则实例化对象,否则直接返回对象
          synchronized (SingletonLazy.class){// 同步锁
             if(null == instance){// 第二次判断
                instance = new SingletonLazy();// 实例化对象
             }
          } 
        }
        return instance;// 返回已存在的对象
    }
}

问题:有没有比较简单的写法呢?

答案:有,请看第四部分。

总结:

  • 饿汉式:线程安全
  • 懒汉式:非线程安全
  • 双检锁:线程安全
  • 静态内部类:线程安全
  • 枚举:线程安全
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘书书-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值