单例模式大杂烩

以下哪种方式实现的单例是线程安全的:(A B C D)

A、枚举
B、静态内部类
C、双检锁模式
D、饿汉式

1、单例模式的定义

  • 定义:确保一个类只有一个实例,并提供该实例的全局访问点。
  • 这样做的好处是:有些时候,只需要一个实例就够了,使用单例模式就可以避免创建多个实例;同时,频繁的创建与销毁,会耗费系统资源。

2、单例模式的设计要素

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

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

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

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

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;
    }
}
  • 说明:实现和线程不安全的懒汉式 几乎一样,唯一不同的点是,在getInstance() 方法上加了一把 锁。如此一来,多个线程访问,每次只有拿到锁的的线程能够进入该方法,避免了多线程不安全问题的出现。
  • 优点:延迟实例化,节约了资源,并且是线程安全的。
  • 缺点:虽然解决了线程安全问题,但是性能降低了。因为,即使实例已经实例化了,既后续不会再出现线程安全问题了,但是锁还在,每次还是只能拿到锁的线程进入该方法,使得线程阻塞,等待时间过长。

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

  • 说明:先不创建实例,当第一次被调用时,再创建实例,所以被称为懒汉式。
  • 优点:延迟了实例化,如果不需要使用该类,就不会被实例化,节约了系统资源。
  • 缺点:线程不安全,多线程环境下,如果多个线程同时进入了 if (instance == null) ,若此时还未实例化,也就是instance == null,那么就会有多个线程执行 instance = new Single3();,就会实例化多个实例;

第三种:懒汉模式改良版(线程安全,使用了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;
    }
}
  • 说明: 双重检查锁模式相当于是改进为线程安全的懒汉式。线程安全的懒汉式的缺点是性能降低了,造成的原因是因为即使已经实例化,依然每次都会有锁。而现在,我们将锁的位置变了,并且多加了一个检查。 也就是,先判断实例是否存在,若已经存在了,则不会执行"判断方法内的有锁方法"了。 而如果,还没有实例化的时候,多个线程进去了,也没有事,因为里面的方法有锁,只会让一个线程进入最内层方法并实例化实例。如此一来,最多最多,也就是第一次实例化的时候,会有线程阻塞的情况,后续便不会再有线程阻塞的问题。
  • 为什么使用 volatile 关键字修饰了 instance 实例变量 ?instance = new Single4();这段代码执行时分为三步:

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

  • 正常的执行顺序当然是 1>2>3 ,但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。
  • 单线程环境时,指令重排并没有什么问题;多线程环境时,会导致有些线程可能会获取到还没初始化的实例。
    例如:线程A 只执行了 1 和 3 ,此时线程B来调用 getInstance(),发现 instance不为空,便获取 instance实例,但是其实此时的 instance 还没有初始化。解决办法就是加一个 volatile 关键字修饰 instance,volatile 会禁止 JVM 的指令重排,就可以保证多线程环境下的安全运行。
  • 优点:延迟实例化,节约了资源;线程安全;并且相对于 线程安全的懒汉式,性能提高了。
  • 缺点:volatile 关键字,对性能也有一些影响。

第四种:利用私有的内部工厂类(线程安全,内部类也可以换成内部接口,不过工厂类变量的作用于要改为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();
    }
}

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

public class Singleton { 
	private Singleton() {
    } 
    private static class SingletonHolder { 
    	private static final Singleton instance= new Singleton();
    } 
    public static Singleton getInstance() { 
    	return SingletonHolder.instance;
    }
     
}
  • 说明:首先,当外部类 Singleton 被加载时,静态内部类 SingletonHolder 并没有被加载进内存。当调用 getInstance() 方法时,会运行 return SingletonHolder.instance ,触发了 SingletonHolder.instance,此时静态内部类 SingletonHolder 才会被加载进内存,并且初始化 instance实例,而且 JVM 会确保 instance只被实例化一次。
  • 优点:延迟实例化,节约了资源;且线程安全;性能也提高了。

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

public enum Singleton {
    
    INSTANCE; //添加自己需要的操作 
    public void doSomeThing() {
    }
    
}
  • 说明:枚举实例的创建是线程安全的,且在任何情况下都是单例。
  • 优点:写法简单,线程安全,天然防止反射和反序列化调用。

防止反序列化

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

4、单例模式的应用场景

4.1 应用场景举例

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

4.2 使用场景总结:

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

https://www.nowcoder.com/profile/5457254/test/41138049/304933#summary

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

还能坚持

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

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

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

打赏作者

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

抵扣说明:

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

余额充值