首页 > 试题广场 >

以下哪种方式实现的单例是线程安全的

[不定项选择题]
以下哪种方式实现的单例是线程安全的
  • 枚举
  • 静态内部类
  • 双检锁模式
  • 饿汉式

高频面试题:单列模式的6种实现方式。

整理分享不易,点个赞鸭~

目录:

一、单例模式的定义

定义: 确保一个类只有一个实例,并提供该实例的全局访问点。

这样做的好处是:有些实例,全局只需要一个就够了,使用单例模式就可以避免一个全局使用的类,频繁的创建与销毁,耗费系统资源。

二、单例模式的设计要素

  • 一个私有构造函数 (确保只能单例类自己创建实例)
  • 一个私有静态变量 (确保只有一个实例)
  • 一个公有静态函数 (给使用者提供调用方法)

简单来说就是,单例类的构造方法不让其他人修改和使用;并且单例类自己只创建一个实例,这个实例,其他人也无法修改和直接使用;然后单例类提供一个调用方法,想用这个实例,只能调用。这样就确保了全局只创建了一次实例。

三、单例模式的6种实现及各实现的优缺点

(一)懒汉式(线程不安全)

实现:

public class Singleton {
     private static Singleton uniqueInstance;

     private Singleton() {

    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

说明: 先不创建实例,当第一次被调用时,再创建实例,所以被称为懒汉式。

优点: 延迟了实例化,如果不需要使用该类,就不会被实例化,节约了系统资源。

缺点: 线程不安全,多线程环境下,如果多个线程同时进入了 if (uniqueInstance == null) ,若此时还未实例化,也就是uniqueInstance == null,那么就会有多个线程执行 uniqueInstance = new Singleton(); ,就会实例化多个实例;

(二)饿汉式(线程安全)

实现:

public class Singleton {

    private static Singleton uniqueInstance = new Singleton();

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        return uniqueInstance;
    }

}

说明: 先不管需不需要使用这个实例,直接先实例化好实例 (饿死鬼一样,所以称为饿汉式),然后当需要使用的时候,直接调方法就可以使用了。

优点: 提前实例化好了一个实例,避免了线程不安全问题的出现。

缺点: 直接实例化好了实例,不再延迟实例化;若系统没有使用这个实例,或者系统运行很久之后才需要使用这个实例,都会操作系统的资源浪费。

(三)懒汉式(线程安全)

实现:

public class Singleton {
    private static Singleton uniqueInstance;

    private static singleton() {
    }

    private static synchronized Singleton getUinqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }

}

说明: 实现和 线程不安全的懒汉式 几乎一样,唯一不同的点是,在get方法上 加了一把 锁。如此一来,多个线程访问,每次只有拿到锁的的线程能够进入该方法,避免了多线程不安全问题的出现。

优点: 延迟实例化,节约了资源,并且是线程安全的。

缺点: 虽然解决了线程安全问题,但是性能降低了。因为,即使实例已经实例化了,既后续不会再出现线程安全问题了,但是锁还在,每次还是只能拿到锁的线程进入该方***使线程阻塞,等待时间过长。

(四)双重检查锁实现(线程安全)

实现:

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }  
}

说明: 双重检查数相当于是改进了 线程安全的懒汉式。线程安全的懒汉式 的缺点是性能降低了,造成的原因是因为即使实例已经实例化,依然每次都会有锁。而现在,我们将锁的位置变了,并且多加了一个检查。 也就是,先判断实例是否已经存在,若已经存在了,则不会执行判断方法内的有锁方法了。 而如果,还没有实例化的时候,多个线程进去了,也没有事,因为里面的方法有锁,只会让一个线程进入最内层方法并实例化实例。如此一来,最多最多,也就是第一次实例化的时候,会有线程阻塞的情况,后续便不会再有线程阻塞的问题。

为什么使用 volatile 关键字修饰了 uniqueInstance 实例变量 ?

uniqueInstance = new Singleton(); 这段代码执行时分为三步:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

正常的执行顺序当然是 1>2>3 ,但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。
单线程环境时,指令重排并没有什么问题;多线程环境时,会导致有些线程可能会获取到还没初始化的实例。
例如:线程A 只执行了 1 和 3 ,此时线程B来调用 getUniqueInstance(),发现 uniqueInstance 不为空,便获取 uniqueInstance 实例,但是其实此时的 uniqueInstance 还没有初始化。

解决办法就是加一个 volatile 关键字修饰 uniqueInstance ,volatile 会禁止 JVM 的指令重排,就可以保证多线程环境下的安全运行。

优点: 延迟实例化,节约了资源;线程安全;并且相对于 线程安全的懒汉式,性能提高了。

缺点: volatile 关键字,对性能也有一些影响。

(五)静态内部类实现(线程安全)

实现:

public class Singleton {

    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE;
    }

}

说明: 首先,当外部类 Singleton 被加载时,静态内部类 SingletonHolder 并没有被加载进内存。当调用 getUniqueInstance() 方法时,会运行 return SingletonHolder.INSTANCE; ,触发了 SingletonHolder.INSTANCE ,此时静态内部类 SingletonHolder 才会被加载进内存,并且初始化 INSTANCE 实例,而且 JVM 会确保 INSTANCE 只被实例化一次。

优点: 延迟实例化,节约了资源;且线程安全;性能也提高了。

(六)枚举类实现(线程安全)

实现:

public enum Singleton {

    INSTANCE;

    //添加自己需要的操作
    public void doSomeThing() {

    }

}

说明: 默认枚举实例的创建就是线程安全的,且在任何情况下都是单例。

优点: 写法简单,线程安全,天然防止反射和反序列化调用。

  • 防止反序列化
    序列化:把java对象转换为字节序列的过程;
    反序列化: 通过这些字节序列在内存中新建java对象的过程;
    说明: 反序列化 将一个单例实例对象写到磁盘再读回来,从而获得了一个新的实例。
    我们要防止反序列化,避免得到多个实例。
    枚举类天然防止反序列化。
    其他单例模式 可以通过 重写 readResolve() 方法,从而防止反序列化,使实例唯一重写 readResolve() :
private Object readResolve() throws ObjectStreamException{
        return singleton;
}

四、单例模式的应用场景

应用场景举例:

  • 网站计数器。
  • 应用程序的日志应用。
  • Web项目中的配置对象的读取。
  • 数据库连接池。
  • 多线程池。
  • ......

使用场景总结:

  • 频繁实例化然后又销毁的对象,使用单例模式可以提高性能。
  • 经常使用的对象,但实例化时耗费时间或者资源多,如数据库连接池,使用单例模式,可以提高性能,降低资源损坏。
  • 使用线程池之类的控制资源时,使用单例模式,可以方便资源之间的通信。

看完之后,如果还有什么不懂的,可以在评论区留言,会及时回答更新。

最后,记得点赞支持一波鸭~

编辑于 2021-03-19 15:17:02 回复(250)
枚举,接口变量,双重检索(懒汉式),饿汉式.
发表于 2020-02-18 17:40:51 回复(0)
我能问个可能比较愚蠢的问题吗?枚举跟单例有什么关系?
发表于 2019-10-16 17:42:11 回复(1)

Java中四种线程安全的单例模式实现方式

第一种:饿汉模式(线程安全)

public class Single2 {

    private static Single2 instance = new Single2();
    
    private Single2(){
        System.out.println("Single2: " + System.nanoTime());
    }
    
    public static Single2 getInstance(){
        return instance;
    }
}


第二种:懒汉模式 (如果方法没有synchronized,则线程不安全)

public class Single3 {

    private static Single3 instance = null;
    
    private Single3(){
        System.out.println("Single3: " + System.nanoTime());
    }
    
    public static synchronized Single3 getInstance(){
        if(instance == null){
            instance = new Single3();
        }
        return instance;
    }
}


第三种:懒汉模式改良版(线程安全,使用了double-check,即check-加锁-check,目的是为了减少同步的开销)

public class Single4 {

    private volatile static Single4 instance = null;
    
    private Single4(){
        System.out.println("Single4: " + System.nanoTime());
    }
    
    public static Single4 getInstance(){
        if(instance == null){
            synchronized (Single4.class) {
                if(instance == null){
                    instance = new Single4();
                }
            }
        }
        return instance;
    }
}


第四种:利用私有的内部工厂类(线程安全,内部类也可以换成内部接口,不过工厂类变量的作用于要改为public了。)

public class Singleton {
    
    private Singleton(){
        System.out.println("Singleton: " + System.nanoTime());
    }
    
    public static Singleton getInstance(){
        return SingletonFactory.singletonInstance;
    }
    
    private static class SingletonFactory{
        private static Singleton singletonInstance = new Singleton();
    }
}
发表于 2019-09-17 22:58:15 回复(15)
1.饿汉式(线程安全,调用效率高,但是不能延时加载); 2.懒汉式(线程安全,调用效率不高,但是能延时加载); 3.Double CheckLock实现单例:DCL也就是双重锁判断机制(由于JVM底层模型原因,偶尔会出问题,不建议使用); 4.静态内部类实现模式(线程安全,调用效率高,可以延时加载); 5.枚举类(线程安全,调用效率高,不能延时加载,可以天然的防止反射和反序列化调用)。
发表于 2019-08-21 12:30:39 回复(11)
以下方法实现的单例是线程安全的:枚举;静态内部类;双检索模式;饿汉式。
发表于 2021-08-19 19:53:53 回复(0)

单例 线程安全

饿汉式

懒汉式

枚举

静态内部类

双检锁模式

编辑于 2019-09-25 15:53:16 回复(0)

通过单例模式可以保证系统中一个类只有一个实例而且该实例易于被外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决办法。

饿汉式单例类

public class EagerSingleton {
    //可以由名知意,太饥饿了,一大早就准备好了实例等待使用,但很有可能一直没人使用
    private static final EagerSingleton instance = new EagerSingleton();
    private EagerSingleton() {}
    public static EagerSingleton getInstance() {
        return instance;
    }
}

懒汉式单例类

public class LazySingleton {
       //等到人家使用才生产一个实例,所以叫懒汉模式
    private static LazySingleton instance = null;
    private LazySingleton() {}
    public static LazySingleton getInstance() {
        if(instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

懒汉式单例类在实例化时,必须处理好多个线程同时首次引用此类时的访问限制问题。

为了实现懒汉式单例类的线程安全,必须加锁 synchronized 实现单例,但加锁会影响效率。

public class LazySingleton {
    private static LazySingleton instance = null;
    private LazySingleton() {}
    public static synchronized LazySingleton getInstance() {
        if(instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

双重校验锁懒汉模式(DCL)

在保证安全的前提下保持高性能的实现方式

public class LazySingleton {
    // 防止指令重排序以及保证变量在多线程运行时的可见性
    private volatile static LazySingleton instance = null;
    private LazySingleton() {}
    public static LazySingleton getInstance() {
        //    由于单例模式只需创建一次实例即可
        //    所有当已经创建一个实例之后,再次调用就不需要进入同步代码块中了
        //    避免线程之间互相竞争锁,提高了性能
        if(instance == null) {
            synchronized(LazySingleton.class) {
                //    防止二次创建实例
                if(instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

静态内部类模式

内部类都是在第一次使用时才会被加载,而且静态内部类的加载不需要依附外部类,在使用时才加载。多个线程用到同一个类,而这个类还未被加载,则只有一个线程去加载类,其他线程等待。因此静态内部类模式的单例可以保证线程安全。

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton(){}
    private static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

枚举模式

枚举天然解决了多线程同步执行的问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。

public enum Singleton {
    INSTANCE;
    public void method() {}
}

编辑于 2020-08-14 15:55:29 回复(1)
ABCD都是线程安全的,懒汉式如果加了synchronized也是线程安全的
发表于 2019-08-22 22:38:40 回复(0)
<p>双锁检测需要加volatile关键字,这里没说啊,是不是默认就添加的?</p>
发表于 2020-11-27 17:44:55 回复(1)
反射注入不算是线程不安全吗?
发表于 2020-05-05 08:33:01 回复(0)
看高赞第一的评论,自己补充单例应用场景

下面是一些常见的具体应用场景:

  1. 日志记录器:在许多应用程序中,需要使用单例模式创建一个全局的日志记录器,以便记录系统的操作、错误和调试信息。单例模式确保只有一个日志记录器实例,并且可以在整个应用程序中使用。

  2. 数据库连接池:在具有多线程环境的应用程序中,使用单例模式创建一个数据库连接池可以提高性能和效率。单例模式可以确保只有一个连接池实例,并且多个线程可以共享这个实例来获取数据库连接。

  3. 配置信息管理器:在应用程序中,通常需要读取和管理配置信息,例如数据库连接参数、系统设置等。使用单例模式创建一个配置信息管理器可以确保只有一个实例来加载和保存配置信息,并且可以在整个应用程序***享。

  4. 线程池:在多线程环境中,线程池用于管理和控制线程的执行。使用单例模式创建一个全局的线程池可以避免多个线程池实例的创建和管理,从而提高线程的复用和性能。

  5. 缓存系统:在许多应用程序中,需要使用缓存来存储经常使用的数据,以提高访问速度。使用单例模式创建一个全局的缓存系统可以确保只有一个实例来管理缓存数据,并且可以在整个应用程序***享。

  6. GUI应用程序中的窗口管理器:在GUI应用程序中,窗口管理器负责创建、管理和控制窗口的显示和交互。使用单例模式创建一个窗口管理器可以确保只有一个实例来管理窗口,并且可以在整个应用程序中统一管理。


发表于 2023-06-05 21:36:00 回复(0)
mark
发表于 2022-10-07 14:26:58 回复(0)
除了懒汉式,其他都是线程安全的。
懒汉式即你要用我再创建。
发表于 2022-08-12 13:02:18 回复(0)
双重检索用反射是可以破坏的吧,如果要防止利用反射破坏,要加上标志位
发表于 2022-05-08 23:27:05 回复(0)
这题有毛病,细节都不给,怎么知道是否线程安全,每一种的实现方式有好几种。
发表于 2021-11-30 09:15:42 回复(0)
面试让手写单利模式的面试官都是傻子 哈哈😄
发表于 2021-05-24 20:03:21 回复(0)
以下方法实现的单例是线程安全的:枚举;静态内部类;双检索模式;饿汉式。
发表于 2021-05-20 10:25:54 回复(0)
一、单例模式的定义 定义:确保一个类只有一个实例,并提供该实例的全局访问点。 这样做的好处是:有些实例,全局只需要一个就够了,使用单例模式就可以避免一个全局使用的类,频繁的创建与销毁,耗费系统资源。 二、单例模式的设计要素 一个私有构造函数 (确保只能单例类自己创建实例) 一个私有静态变量 (确保只有一个实例) 一个公有静态函数 (给使用者提供调用方法) 简单来说就是,单例类的 构造方法不让其他人修改和使用;并且单例类自己只创建一个实例,这个实例,其他人也无法修改和直接使用;然后单例类提供一个调用方法,想用这个实例,只能调用。这样就确保了全局只创建了一次实例。 三、单例模式的6种实现及各实现的优缺点 (一)懒汉式(线程不安全) 实现: public class Singleton { private static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } } 点击并拖拽以移动 说明:先不创建实例,当第一次被调用时,再创建实例,所以被称为懒汉式。 优点:延迟了实例化,如果不需要使用该类,就不会被实例化,节约了系统资源。 缺点:线程不安全,多线程环境下,如果多个线程同时进入了 if (uniqueInstance == null) ,若此时还未实例化,也就是uniqueInstance == null,那么就会有多个线程执行 uniqueInstance = new Singleton(); ,就会实例化多个实例; (二)饿汉式(线程安全) 实现: public class Singleton { private static Singleton uniqueInstance = new Singleton(); private Singleton() { } public static Singleton getUniqueInstance() { return uniqueInstance; } } 点击并拖拽以移动 说明:先不管需不需要使用这个实例,直接先实例化好实例 (饿死鬼一样,所以称为饿汉式),然后当需要使用的时候,直接调方法就可以使用了。 优点:提前实例化好了一个实例,避免了线程不安全问题的出现。 缺点:直接实例化好了实例,不再延迟实例化;若系统没有使用这个实例,或者系统运行很久之后才需要使用这个实例,都会操作系统的资源浪费。 (三)懒汉式(线程安全) 实现: public class Singleton { private static Singleton uniqueInstance; private static singleton() { } private static synchronized Singleton getUinqueInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } } 点击并拖拽以移动 说明:实现和 线程不安全的懒汉式 几乎一样,唯一不同的点是,在get方法上 加了一把 锁。如此一来,多个线程访问,每次只有拿到锁的的线程能够进入该方法,避免了多线程不安全问题的出现。 优点:延迟实例化,节约了资源,并且是线程安全的。 缺点:虽然解决了线程安全问题,但是性能降低了。因为,即使实例已经实例化了,既后续不会再出现线程安全问题了,但是锁还在,每次还是只能拿到锁的线程进入该方***使线程阻塞,等待时间过长。 (四)双重检查锁实现(线程安全) 实现: public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { if (uniqueInstance == null) { synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } } 点击并拖拽以移动 说明: 双重检查数相当于是改进了 线程安全的懒汉式。线程安全的懒汉式 的缺点是性能降低了,造成的原因是因为即使实例已经实例化,依然每次都会有锁。而现在,我们将锁的位置变了,并且多加了一个检查。 也就是,先判断实例是否已经存在,若已经存在了,则不会执行判断方法内的有锁方法了。 而如果,还没有实例化的时候,多个线程进去了,也没有事,因为里面的方法有锁,只会让一个线程进入最内层方法并实例化实例。如此一来,最多最多,也就是第一次实例化的时候,会有线程阻塞的情况,后续便不会再有线程阻塞的问题。 为什么使用 volatile 关键字修饰了 uniqueInstance 实例变量 ? uniqueInstance = new Singleton(); 这段代码执行时分为三步: 为 uniqueInstance 分配内存空间 初始化 uniqueInstance 将 uniqueInstance 指向分配的内存地址 正常的执行顺序当然是 1>2>3 ,但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。 单线程环境时,指令重排并没有什么问题;多线程环境时,会导致有些线程可能会获取到还没初始化的实例。 例如:线程A 只执行了 1 和 3 ,此时线程B来调用 getUniqueInstance(),发现 uniqueInstance 不为空,便获取 uniqueInstance 实例,但是其实此时的 uniqueInstance 还没有初始化。 解决办法就是加一个 volatile 关键字修饰 uniqueInstance ,volatile 会禁止 JVM 的指令重排,就可以保证多线程环境下的安全运行。 优点:延迟实例化,节约了资源;线程安全;并且相对于 线程安全的懒汉式,性能提高了。 缺点:volatile 关键字,对性能也有一些影响。 (五)静态内部类实现(线程安全) 实现: public class Singleton { private Singleton() { } private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getUniqueInstance() { return SingletonHolder.INSTANCE; } } 点击并拖拽以移动 说明:首先,当外部类 Singleton 被加载时,静态内部类 SingletonHolder 并没有被加载进内存。当调用 getUniqueInstance() 方法时,会运行 return SingletonHolder.INSTANCE; ,触发了 SingletonHolder.INSTANCE ,此时静态内部类 SingletonHolder 才会被加载进内存,并且初始化 INSTANCE 实例,而且 JVM 会确保 INSTANCE 只被实例化一次。 优点:延迟实例化,节约了资源;且线程安全;性能也提高了。 (六)枚举类实现(线程安全) 实现: public enum Singleton { INSTANCE; //添加自己需要的操作 public void doSomeThin***击并拖拽以移动 说明:默认枚举实例的创建就是线程安全的,且在任何情况下都是单例。 优点:写法简单,线程安全,天然防止反射和反序列化调用。 防止反序列化 序列化:把java对象转换为字节序列的过程; 反序列化:通过这些字节序列在内存中新建java对象的过程; 也就是说,反序列化 将一个单例实例对象写到磁盘再读回来,从而获得了一个新的实例。 我们要防止反序列化,避免得到多个实例。 枚举类天然防止反序列化。 其他单例模式 可以通过 重写 readResolve() 方法,从而防止反序列化,使实例唯一 重写 readResolve() : private Object readResolve() throws ObjectStreamException{ return singleton; } 点击并拖拽以移动 四、单例模式的应用场景 应用场景举例: 网站计数器。 应用程序的日志应用。 Web项目中的配置对象的读取。 数据库连接池。 多线程池。 ...... 使用场景总结: 频繁实例化然后又销毁的对象,使用单例模式可以提高性能。 经常使用的对象,但实例化时耗费时间或者资源多,如数据库连接池,使用单例模式,可以提高性能,降低资源损坏。 使用线程池之类的控制资源时,使用单例模式,可以方便资源之间的通信。
发表于 2021-02-24 12:34:35 回复(0)
单例 线程安全 饿汉式 懒汉式 枚举 静态内部类 双检锁模式
发表于 2020-12-23 20:36:42 回复(0)