龙空技术网

万字长文带你搞懂Java中的单例模式

Java机械师 122

前言:

当前同学们对“java中单例模式具体作用”大致比较关切,看官们都需要了解一些“java中单例模式具体作用”的相关资讯。那么小编在网络上收集了一些关于“java中单例模式具体作用””的相关内容,希望各位老铁们能喜欢,同学们一起来了解一下吧!

一、前言

本文旨在通过由浅入深的方式带大家深入的了解各种单例模式,接下来我会先简单介绍一下单例模式,给出相应单例类的代码,然后通过一些问题来介绍各个单例模式需要注意的地方,还会给出相应的测试代码,希望各位读者看完能有所收获,有任何问题都可以在评论区提出或私信我,由于本人水平有限,所以可能存在错漏之处,望指正。

二、什么是单例模式?

单例模式(Singleton Pattern)属于创建型设计模式,单例指的就是单实例,我们通常都是使用 new 来创建对象的,直接 new 的话创建出来的是不同的对象,如果我们想要确保一个类只能有一个对象,那么我们就可以采用单例模式的方式来获取对象。

三、单例模式的核心特点单例类只能有一个实例,即每次获取该类的对象都是同一个对象单例类必须自己创建自己的唯一实例,即 new 操作必须在单例类中实现,不能在外部 new 单例类单例类必须提供获取该唯一实例的方法,如Singleton.getInstance();四、单例模式的多种实现方式

1、饿汉式

简单来说就是不管用不用得上,在项目启动时就先 new 出该实例对象,这种方法比较简单,但可能会造成资源浪费,不过一般我们创建了这个类肯定就会使用,所以其实项目中直接用这种方式也是可以的

/** * 饿汉式单例模式 - 静态变量方式 */public class Singleton {    private final static Singleton INSTANCE = new Singleton();    private Singleton() {    }    public static Singleton getInstance() {        return INSTANCE;    }    }复制代码

接下来请先思考以下几个问题,请务必认真思考后再看我的讲解

问题一:为什么要私有化构造方法?问题二:为什么实例对象 INSTANCE 要加 static ?问题三:为什么实例对象 INSTANCE 要加 final ?问题四:除了以上这种写法,你能否给出与它功能一致的写法?

答案如下:

解答一:根据单例模式的特点,单例类必须自己创建自己的唯一实例,所以只能私有化构造方法,防止用户从外部 new 出新实例对象解答二:首先我们要知道我们的目的是获取该单例类的实例对象,由于不能 new 出该实例对象,所以我们只能通过类名直接调用类中的静态方法来获取该实例对象,即 Singleton.getInstance() ,而静态方法是不能访问非静态成员变量的(因为非静态成员变量是随着对象的创建而被实例化的,而调用静态方法时,可能还没有实例化好对象,所以是无法访问非静态成员变量的),因此实例变量也必须是静态的,静态实例变量会在类的初次加载时初始化解答三:首先 final 保证了实例初始化过程的顺序性(这个我也不是很了解,感兴趣的可以去查阅 final 的相关资料),还有就是禁止通过反射方式改变实例对象,通过反射方式修改实例对象的方法请看我下面给出的SingletonTest 中的 testNotFinal() 方法解答四:上面我们是通过静态变量的方式实现饿汉式单例模式,还有一种方式的通过静态代码块的方式实现,具体看下面的 Singleton1,其实和上面这种方式没啥区别,主要就是把类实例化的过程放在了静态代码块中

public class SingletonTest {    /**     * 参考文章:     * Singleton 单例类不加 final 的时候可以通过反射方式修改实例对象,加上 final 则会抛出异常     */    private static void testNotFinal() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {        Singleton firstInstance = Singleton.getInstance();        System.out.println("第一次拿到单例模式创建的对象:" + firstInstance);        // 1.获得 Singleton 类        Class<Singleton> clazz = Singleton.class;        // 2.获得 Singleton 类的私有无参构造方法,并通过 setAccessible(true) 允许我们通过反射的方式调用该私有构造方法        Constructor<Singleton> constructor = clazz.getDeclaredConstructor();        constructor.setAccessible(true);        // 3.创建新的实例对象        Singleton clazzSingleton = constructor.newInstance();        System.out.println("反射创建出来的对象: " + clazzSingleton);        // 4.获取 Singleton 类中所有声明的字段,即包括public、private和 protected,但是不包括父类的字段,目前只有 INSTANCE        Field[] fields = clazz.getDeclaredFields();        for (Field field : fields) {            // 设置 true:允许通过反射访问该字段            field.setAccessible(true);            // 向 Singleton 对象(firstInstance)的这个 Field 属性(即:INSTANCE)设置新值 clazzSingleton            field.set(firstInstance, clazzSingleton);            Singleton secondInstance = Singleton.getInstance();            System.out.println("第二次拿到单例模式创建的对象: " + secondInstance);        }    }    /**     * 简单测试通过单例类(静态变量方式)获取的是否是同个对象     */    public static void testSingleton() {        Singleton singletonOne = Singleton.getInstance();        Singleton singletonTwo = Singleton.getInstance();        // 输出结果: true        System.out.println("两次获取的都是同一个对象:" + (singletonOne == singletonTwo));    }    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {        // 测试 Singleton 获取的是否是同个对象        // testSingleton();        // 测试不加 final 是否能成功修改 Singleton 类的实例对象        testNotFinal();    }}复制代码
/** * 饿汉式单例模式 - 静态代码块方式 */public class Singleton1 {    // 注意:这里同样需要加 final    private final static Singleton1 INSTANCE;    static {        INSTANCE = new Singleton1();    }    private Singleton1() {    }    public static Singleton1 getInstance() {        return INSTANCE;    }}复制代码

2、懒汉式

这里我们采用双重校验锁(DCL)的方式来实现懒汉式单例模式,这里看不懂没有关系,往下看,下面会给出一些问题以及懒汉式的其他错误写法,相信看完之后你就知道为什么要用DCL来实现懒汉式了

/** * 懒汉式单例模式:双重校验锁(DCL,double-checked locking)方式 */public class Singleton {    private static volatile Singleton INSTANCE;    private Singleton() {    }    public static Singleton getInstance() {        if (INSTANCE == null) {            synchronized (Singleton.class) {                if (INSTANCE == null) {                    INSTANCE = new Singleton();                }            }        }        return INSTANCE;    }}复制代码

接下来我们先分析几种错误的懒汉式写法,通过以下这些分析和问题,相信读者就能明白上述代码

第一种错误的懒汉式单例模式

public class SingletonErrorOne {    private static SingletonErrorOne INSTANCE;    private SingletonErrorOne() {    }    // 第一种错误的懒汉式单例模式,多线程下线程不安全,会产生多个实例    public static SingletonErrorOne getInstance() {        if (INSTANCE == null) {            System.out.println(Thread.currentThread().getName() + ":开始生成新实例");            INSTANCE = new SingletonErrorOne();        }        return INSTANCE;    }}复制代码

这种是我们最容易想到的懒汉式方式,即调用 getInstance() 方法的时候才创建实例对象,然后通过 if 判断让该实例对象只创建一次,但是只能在单线程下使用,在多线程下就会创建多个实例对象出来,这里举个例子说明一下:假设我们有线程一和线程二同时调用 getInstance() 方法,这时候两个线程的 INSTANCE 可能都是为 null 的,所以一定会生成新实例,我们可以通过下面代码测试一下

public class SingletonTest {    public static void main(String[] args) {        // 通过模拟多线程环境,我们可以看到有多个线程在生成新实例        for (int i = 0; i < 100; i++) {            new Thread(() -> {                System.out.println(SingletonErrorOne.getInstance().hashCode());            }).start();        }    }}复制代码
第二种错误的懒汉式单例模式

第一种错误写法的问题是线程不安全,所以我们自然会想到通过加锁(synchronized )的方式来进行线程同步,防止产生多实例对象,代码如下:

public class SingletonErrorTwo {    private static SingletonErrorTwo INSTANCE;    private SingletonErrorTwo() {    }    // 这种方式虽然线程安全了但是性能太差了,已经退化为单线程,而且整个实例方法都被阻塞了    // 如果该实例方法中存在耗时代码,将会大大降低接口性能,这时候我们可以降低锁粒度,只锁定部分代码    public static synchronized SingletonErrorTwo getInstance() {        try {            // 耗时代码            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        if (INSTANCE == null) {            System.out.println(Thread.currentThread().getName() + ":开始生成新实例");            INSTANCE = new SingletonErrorTwo();        }        return INSTANCE;    }}复制代码

这种方式其实不算错误,只是我们不推荐使用,因为效率太低了,每次调用 getInstance 方法都会加锁,我们在实例方法里面加入一段耗时代码,这时候调用下面的测试方法,你将会发现效率特别的低下,这种就是锁粒度太大了,我们可以降低一下锁粒度,具体看第三种错误的懒汉式单例模式

public class SingletonTest {        public static void main(String[] args) {        // 通过模拟多线程环境,我们可以看到获取的都是同一个实例,但是执行效率特别的低下        for (int i = 0; i < 100; i++) {            new Thread(() -> {                System.out.println(SingletonErrorTwo.getInstance().hashCode());            }).start();        }    }}复制代码
第三种错误的懒汉式单例模式

我们通过只锁定生成实例对象的这部分代码,让其他耗时代码并行执行,效率提高了,但是会产生多个实例对象,造成这种现象的原因是我们多个线程可能都通过了 if 判断,然后开始阻塞等待持有锁的线程释放锁,第一个持有锁的线程生成实例对象后释放锁,此时其他线程获得锁仍会继续生成新实例对象,因为已经通过了 if 判断,改进方法就是通过双重校验锁(DCL)来避免这种问题,我们获得锁之后可以再判断一次 INSTANCE 是否为空,这时候如果线程一是第一个获得锁的线程,那么线程一就会生成实例对象,如果线程二是第二个获得锁的线程,那么此时线程一已经生成完对象了,线程二就不会继续生成新对象,需要特别注意的是必须加上 volatile 字段,我们在下面进行讲解

注:这里由于我们加了生成新实例的打印输出,释放锁的速度比较慢,所以导致基本每个线程都会创建新实例,你可以去掉打印输出,你会发现虽然大部分输出的都是同个对象,但是还是会产生一些新对象,

public class SingletonErrorThree {    private static SingletonErrorThree INSTANCE;    private SingletonErrorThree() {    }    public static SingletonErrorThree getInstance() {        try {            // 耗时代码            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        if (INSTANCE == null) {            synchronized (SingletonErrorThree.class) {                System.out.println(Thread.currentThread().getName() + ":开始生成新实例");                INSTANCE = new SingletonErrorThree();            }        }        return INSTANCE;    }}复制代码
public class SingletonTest {    public static void main(String[] args) {        // 通过模拟多线程环境,我们可以看到执行效率大幅提升了,但是有多个线程在生成新实例        for (int i = 0; i < 100; i++) {            new Thread(() -> {                System.out.println(SingletonErrorThree.getInstance().hashCode());            }).start();        }    }}复制代码

这里再给出双重校验锁实现懒汉式单例模式的代码,看完代码看下面给出的问题,看是否都能答得上来

/** * 懒汉式单例模式:双重校验锁(DCL,double-checked locking)方式 */public class Singleton {    private static volatile Singleton INSTANCE;    private Singleton() {    }    public static Singleton getInstance() {        if (INSTANCE == null) {            synchronized (Singleton.class) {                if (INSTANCE == null) {                    INSTANCE = new Singleton();                }            }        }        return INSTANCE;    }}复制代码
问题一:为什么要私有化构造方法?问题二:为什么实例对象 INSTANCE 要加 static ?问题三:为什么要加 synchronized ?问题四:为什么要有第一个 if 判断 ?问题五:为什么要有第二个 if 判断 ?问题六:为什么实例对象 INSTANCE 要加 volatile ?

答案如下:

解答一:同饿汉式解答二:同饿汉式解答三:为了让线程同步,每次创建实例前先加锁,防止产生多实例解答四:为了提高效率,如果我们不加第一个 if 判断的话,那么每个线程都要加锁释放锁,但是其实我们只要第一个线程创建了实例对象后,后面的线程直接返回对象就好了,不需要再进行加锁操作解答五:没有第二个 if 判断会产生多实例的情况,具体看第三种错误的懒汉式单例模式的讲解解答六:我们知道 volatile 关键字有两大特性:可见性(变量修改后,所有线程都能立即实时地看到它的最新值)和禁止指令重排序;这里一开始我理解错了,我以为用到了可见性和指令重排序,可见性是因为第一个持有锁的线程创建完对象后,必须让其他线程知道,这样第二个 if 判断才会不为 null ,但是其实 synchronized 已经帮我们实现了线程的可见性,所以这里的 volatile 主要是起到禁止指令重排序的作用。下面我们来详细讲解为什么要禁止指令重排序我们要知道对象的创建其实不是一步到位的,它是分三步进行的,分别是①、在堆中给对象分配内存空间②、初始化赋值③、建立关联(将引用指向分配的内存空间)现在我们的单例类中有个成员变量 a,代码如下:/** * 懒汉式单例模式:双重校验锁(DCL,double-checked locking)方式 */ public class Singleton { private static volatile Singleton INSTANCE; int a = 1; private Singleton() { } public static Singleton getInstance() { ... } } 复制代码当我们第一个持有锁的线程执行到 INSTANCE = new Singleton(); 的时候正常是先分配内存空间,然后初始化给 a 赋值为 1,接着建立关联,让 INSTANCE 指向刚分配的内存空间,如果这个期间发生指令重排序,比如二三步骤调换顺序,这时候是先分配内存空间,然后就直接建立关联了,此时 a 还是默认值 0,其他线程会直接通过第二个 if 判断,因为此时已经建立关联了,所以 INSTANCE 已经不为空了,只是还没赋值为 1,这时候其他线程拿到的 a 可能为0,然后第一个持有锁的线程才将 a 赋值为1,所以我们必须禁止指令重排序,避免其他线程拿到半初始化状态的实例对象

3、静态内部类

这种方式的效果其实和饿汉式有点类似,都采用类加载机制确保创建的是单实例;但是饿汉式没有延迟初始化的功能,简单来说就是饿汉式不管你有没有调用,对象在类加载时就已经初始化了,而静态内部类方式只有调用getInstance时才会初始化静态内部类,进而初始化实例对象

public class Singleton {    private Singleton() {    }    private static class SingletonInstance {        private final static Singleton INSTANCE = new Singleton();    }    public static Singleton getInstance() {        return SingletonInstance.INSTANCE;    }}复制代码

4、枚举

这种实现方式我没用过,这里就不做过多解释了,网上说这是实现单例模式的最佳方法,因为它更简洁,自动支持序列化机制,绝对防止多次实例化,不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

public enum Singleton {    INSTANCE;    public void add(int x, int y) {        System.out.println(x + y);    }    // 其他各种方法    // ...}复制代码

测试方法如下:

public class SingletonTest {    public static void main(String[] args) {        for (int i = 0; i < 100; i++) {            new Thread(() -> {                System.out.println(Singleton.INSTANCE.hashCode());            }).start();        }    }}复制代码
五、如何解决序列化反序列化导致单例模式失效问题

首先我们先实现 Serializable 接口,让单例类的对象可以序列化,代码如下:

public class Singleton implements Serializable {    private final static Singleton INSTANCE = new Singleton();    private Singleton() {    }    public static Singleton getInstance() {        return INSTANCE;    }}复制代码

然后通过以下代码对实例对象序列化再反序列化,我们可以发现此时已经不是同个对象了

public class SingletonTest {    public static void main(String[] args) throws IOException, ClassNotFoundException {        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));        oos.writeObject(Singleton.getInstance());        File file = new File("tempFile");        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));        Singleton singleton = (Singleton) ois.readObject();        System.out.println(singleton);        System.out.println(Singleton.getInstance());        // 结果为:false        System.out.println(Singleton.getInstance() == singleton);    }}复制代码

解决方法如下:在单例对象代码中添加public Object readResolve()方法

public class Singleton implements Serializable {    private final static Singleton INSTANCE = new Singleton();    private Singleton() {    }    public static Singleton getInstance() {        return INSTANCE;    }        public Object readResolve() {        return getInstance();    }}

标签: #java中单例模式具体作用