单例还是多例,如何选择?

导读:本篇文章讲解 01-单例还是多例,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

目录

一 什么是单例模式

二 使用单例模式的优点

三 单例模式的实现方式

四 实现线程唯一的单例

五 实现一个多例模式


一 什么是单例模式

       保证一个类仅有一个实例,并提供一个访问它的全局访问点,一般来说是指进程内只允许创建一个对象

二 使用单例模式的优点

  • 减少了类的频繁创建,降低了系统资源开销。
  • 类的实例变少,减少内存开销,降低了 GC 操作。
  • 使用单例来表示全局唯一类和处理资源访问冲突

三 单例模式的实现方式

单例实现指导原则:

  • 构造函数 private 访问权限,避免外部通过 new 创建实例。
  • 对象创建必须考虑是否线程安全。
  • 对象创建是否支持懒加载。
  • 获取对象函数是否高性能。

3.1 懒汉式-线程不安全版

       核心是先不初始化单例,等第一次使用的时候再初始化,也就是“懒加载”模式。

public class Singleton {
  private static Singleton singleton = null;

  private Singleton() {
  }

  public static Singleton getInstance() {
    if (singleton == null) {
        singleton = new Singleton();
    }
    return singleton;
  }
}

       基础版中,if 语句存在竞态条件,适用于单线程,多线程出现线程不安全的问题。

3.2 懒汉式-线程安全版

public class Singleton {
  private static Singleton singleton = null;

  private Singleton() {
  }

  public synchronized static Singleton getInstance() {
    if (singleton == null) {
        singleton = new Singleton();
    }
    return singleton;
  }
}

       加上 synchronized 之后能保证线程安全,但并发性能极差,事实上完全退化到了串行。

3.3 懒汉式-双重检查加锁版

public class Singleton {
  private static volatile Singleton singleton = null;

  private Singleton() {
  }

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

       针对线程安全版的性能问题,我们采用 volatile 来保证可见性,防止指令重排序问题;双重检查来保证性能。

3.4 饿汉式

public class Singleton {
    private static final Singleton singleton = new Singleton();

    private Singleton() {
    }

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

       线程安全(得益于类加载机制),写起来超级简单,使用时没有延迟;坏处是有可能造成资源浪费(如果类加载后就一直不使用单例的话)。

       值得注意的时,单线程环境下,饿汉与饱汉在性能上没什么差别;但多线程环境下,由于懒汉需要加锁,饿汉的性能反而更优。

饿汉模式能解决以下两种情况:

  • 耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。
  • 更早的暴露因为对象实例占用资源多,而出现的 OOM 问题,提高系统可用性。

3.5 Holder模式

       Holder 既能实现线程安全,又能通过懒加载规避资源浪费。

public class Singleton {
  
    private Singleton() {
    }

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

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

3.6 枚举

public enum Singleton {
    uniqueInstance;
}

       使用枚举来实现单例控制会更加简洁,并且无偿地提供了序列化的机制,并由 JVM 从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式

四 实现线程唯一的单例

  • 一般来说单例类对象是进程唯一的,一个进程只能有一个单例对象 A,不同进程可以有相同的单例对象 A。
  • 线程唯一的单例是说,在一个线程里面只能有一个单例对象 B,不同线程可以有相同的单例对象 B

       我们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。不过,ThreadLocal 底层实现原理也是基于下面代码中所示的 HashMap。

import java.util.concurrent.ConcurrentHashMap;
public class Singleton {

  private static final ConcurrentHashMap<Long, Singleton> instances = new ConcurrentHashMap<>();

  public Singleton() {
  }

  public static Singleton getInstance() {
    Long currentThreadId = Thread.currentThread().getId();
    instances.putIfAbsent(currentThreadId, new Singleton());
    return instances.get(currentThreadId);
  }

}

五 实现一个多例模式

       “单例”指的是,一个类只能创建一个对象。对应地,“多例”指的就是,一个类可以创建多个对象。

import java.util.HashMap;
import java.util.Map;

public class Multi {
  private long serverNo;
  private String serverAddress;

  private static final Map<Long, Multi> serverInstances = new HashMap<>();

  static {
    serverInstances.put(1L, new Multi(1L, "127.0.0.1:6379"));
    serverInstances.put(2L, new Multi(2L, "127.0.0.1:6380"));
    serverInstances.put(3L, new Multi(3L, "127.0.0.1:6381"));
  }

  private Multi(long serverNo, String serverAddress) {
    this.serverNo = serverNo;
    this.serverAddress = serverAddress;
  }

  public Multi getInstance(long serverNo) {
    return serverInstances.get(serverNo);
  }
}

对于多例模式,还有一种理解方式:同一类型的只能创建一个对象,不同类型的可以创建多个对象。这里的“类型”如何理解呢?


public class Logger {
  private static final ConcurrentHashMap<String, Logger> instances = new ConcurrentHashMap<>();

  private Logger() {}

  public static Logger getInstance(String loggerName) {
    instances.putIfAbsent(loggerName, new Logger());
    return instances.get(loggerName);
  }

  public void log() {
    //...
  }
}

//l1==l2, l1!=l3
Logger l1 = Logger.getInstance("User.class");
Logger l2 = Logger.getInstance("User.class");
Logger l3 = Logger.getInstance("Order.class");

       这种多例模式的理解方式有点类似工厂模式。它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/9582.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!