单例模式

2021-04-06  本文已影响0人  lj72808up

1. 什么是单例模式?

创建单例类的方法叫单例模式. 单例类, 就是只能产生一个对象的类.

2. 为什么使用单例模型

场景一: 一个写日志的类 (资源访问冲突)

  1. 首先, 假设如下方法 FileWriter 的 write 方法本身没有锁. 此假设下设计一个Log类. 在多线程下写日志会冲突, 导致日志覆盖问题.
    首先想到加锁, 尝试方法上加 synchronized, 发现不管用, 因为这个加在对象上的锁, 对不同对象, 没有锁控制. 于是想到在类上加锁. synchronized(Log.class)
public class Logger { 
    private FileWriter writer; 
    public Logger() { 
        File file = new File("/Users/wangzheng/log.txt"); 
        writer = new FileWriter(file, true); //true表示追加写入 
    } 
    public void log(String message) { 
        // synchronized(this) {        // 加锁加载对象上 (1)
        // synchronized (Log.class){   //  加锁加在类上
            writer.write(mesasge); 
        } 
    }
}
  1. 在类上加锁是一种很通用的方法, 除此之外, 解决资源竞争的方法还有

    • 将日志发送到一个 BlockingQueue, 用一个线程 EventLoop 负责将队列中的内容写到文件 (可参考 org.apache.spark.util.EventLoop)
  2. 如果用单例模式呢?
    上面的解决办法中, 虽然在类上加了锁, 但因为能创建多个 Log 对象, 导致空间浪费. 如果只能产生一个对象, 就可以节省内存. 当然即使只创建一个对象, 仍要保证线程安全问题, 单例模式和线程安全无关, 因为同一个对象可以被多个线程使用

3. 单例模式的实现方式

实现单例模式, 有几个问题需要考虑在内:

1. 饿汉式

2. 懒汉式

3. 双重检测

饿汉式不支持延迟加载, 懒汉式不支持高并发. 因此出现第三种方式, 双重检测: 既能延迟加载, 又支持高并发.

4. 静态内部类

静态内部类的方式, 是饿汉式的改造, 将饿汉式单例类作为一个整体放在普通类内部, getInstance() 方法返回内部静态类的静态属性

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);

    private IdGenerator() {
    }

    public static IdGenerator getInstance() {
        return SingletonHolder.instance;
    }

    public long getId() {
        return id.incrementAndGet();
    }

    private static class SingletonHolder {
        private static final IdGenerator instance = new IdGenerator();
    }
}

当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 的类加载来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。

5. 枚举

  1. 上面4种方法的潜在问题?
    上面4中方法的问题在于, 我们是如何满足不让用户自己创建对象这一前提的? 是通过私有化构造函数, 避免用户访问构造函数. 可是即使不访问构造方法, 还有两种创建对象的方式:

    • 反序列化创建对象化:
      只要把单例对象序列化成字节流, 然后读取成新的对象, 就会创造出第二个对象. 因为反序列化是靠字节流和类模板实现, 不用通过构造函数
    • 反射:
      反射会通过 api 调用私有方法
      Constructor constructor = singleton.getClass().getDeclaredConstructor(new Class[0]);
      constructor.setAccessible(true);
      
  2. 用枚举实现单例, 解决上述所有问题

    1. jvm 如何实现枚举对象

      • 所有枚举编译后都是 Enum 的子类 .
      • Enum 类不支持序列化和反序列化. 对应方法直接抛异常
      private void readObject(ObjectInputStream in) throws IOException,
          ClassNotFoundException {
          throw new InvalidObjectException("can't deserialize enum");
      }
      
      private void readObjectNoData() throws ObjectStreamException {
          throw new InvalidObjectException("can't deserialize enum");
      }
      
      • enum 可以反射获取 value, 但不能反射调用构造函数
      • enum 的第一行, 是所有可能的, 不可变的枚举对象列表
      public enum Season {
          // enum 有一组不可变的常量集合 (常量不可变, 集合不可变)
          WINTER(5), SPRING(10), SUMMER(15), FALL(20);
      
          private int value;
      
          // compiler 限制 enum 的构造函数必须是 private
          private Season(int value) {
              this.value = value;
          }
      }
      

      枚举 Season 编译后生成的枚举类:

      final class Season extends Enum {
          public static Season[] values() {
              return (Season[]) $VALUES.clone();
          }
      
          public static Season valueOf(String s) {
              return (Season) Enum.valueOf(Season, s);
          }
      
          private Season(String s, int i, int j) {
              super(s, i);
              value = j;
          }
      
          public static final Season WINTER;
          public static final Season SUMMER;
          private int value;
          private static final Season $VALUES[];
      
          static {
              WINTER = new Season("WINTER", 0, 10);
              SUMMER = new Season("SUMMER", 1, 20);
              $VALUES = (new Season[]{
                      WINTER, SUMMER
              });
          }
      } 
      

      可见, 枚举第一行列出的所有可能的值(Enum类的name属性), 在编译后会变成静态属性, 初始化放到了静态代码块中, 与饿汉模式写法相同, 且其构造函数不能通过反射调用, 又不能序列化反序列化, 因此是实现单例的最佳模式.

    2. 基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。具体的代码如下所示:

    public enum IdGenerator {
        INSTANCE;
    
        private AtomicLong id = new AtomicLong(0);
    
        public long getId() {
            return id.incrementAndGet();
        }
    }
    
    
上一篇 下一篇

猜你喜欢

热点阅读