单例模式

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. 单例模式的实现方式

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

  • 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;
  • 考虑对象创建时的线程安全问题;
  • 考虑是否支持延迟加载;
  • 考虑 getInstance() 性能是否高(是否加锁)。

1. 饿汉式

  • 饿汉式的单例, 在类加载时, instance 静态实例就已经创建并初始化好了.
    • 实例初始化是和类加载绑定的
    • 用类的静态属性的方式保证只有一个实例
    // 一个单例的 ID 递增生成器
    public class IdGenerator {
        private static final IdGenerator instance = new IdGenerator();
        private AtomicLong id = new AtomicLong(0);
    
        private IdGenerator() {}
    
        public static IdGenerator getInstance() {
            return instance;
        }
    
        public long getId() {
            return id.incrementAndGet();
        }
    }
    
    
  • 争议点: 不能延迟加载, 对象随类初始化
    因为饿汉式的单利对象是在类加载时初始化的, 不能懒加载, 导致提前初始化. 所以其报表不已, 有人认为提前初始化是一种资源浪费, 应该真正使用时再去初始化; 而另一些人认为, 提前初始化满足 fail-fast 的设计原则(有问题及早暴露), 而且如果资源不够,就会在程序启动的时候触发报错

2. 懒汉式

  • 懒汉式相当于延迟加载版的饿汉式, 单例实例也是静态属性, 但实例是在 getInstance() 获取时创建, 也因此需要一把类级别的锁防止对象重复初始化.
    public class IdGenerator {
        private static IdGenerator instance;
        private AtomicLong id = new AtomicLong(0);
    
        private IdGenerator() {}
    
        public static synchronized IdGenerator getInstance() {   // 一把类级别的大锁
            if (instance == null) {
                instance = new IdGenerator();
            }
    
            return instance;
        }
    
        public long getId() {
            return id.incrementAndGet();
        }
    }
    
  • 缺点: 无法面对高并发场景
    懒汉式的缺点十分明显: 由于给 getInstance() 方法加了一把类级别的大锁(synchronzed), 导致函数的并发度为1, 相当于串行操作. 如果这个单例类偶尔被用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,

3. 双重检测

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

  • 基于懒汉式的改造点

    • 如果实例已存在, 就不要先获得锁才能获取对象
      因此, 加锁操作的锁竞争放在判断 instance 为空后进行, 还是类级别的大锁 (因为确保静态方法的锁)
    public class IdGenerator { 
        private AtomicLong id = new AtomicLong(0); 
        private volatile static IdGenerator instance; 
        private IdGenerator() {} 
        public static IdGenerator getInstance() { 
            if (instance == null) { 
                synchronized(IdGenerator.class) { 
                    // 此处为类级别的锁 
                    if (instance == null) { 
                        instance = new IdGenerator(); 
                    } 
                } 
            } 
            return instance; 
        } 
        public long getId() { 
            return id.incrementAndGet(); 
        }
    }
    
  • 为什么是双重检测? 只检测一遍 instance == null 不行吗
    因为为了支持 getInstance() 的高并发, 锁没有加载方法上, 而是加在 if (instance == null) 这个条件的判断后. 即判断条件本身没有加锁, 所以在进入 synchronized 代码块后, 判断条件可能已经不成立, 需要再次判断. 第二次判断因为加了锁, 所以是安全的

  • 为什么 instance 实例加 volatile?
    在低版本的 jvm 中, 对象初始化instance = new IdGenerator() 这句其实是2个动做, 分为 new IdGenerator() 创建动作 和 instance= 赋值操作. CPU 的指令重排, 导致赋值语句和不依赖此变量的计算语句重排.(参考volatile), 即在释放锁指令可能先于赋值语句执行. 即同步块退出后, 可能其它线程看到的 instance 仍然是 null, 导致对象重复创建.
    高版本的 jvm 已不存在此问题, 解决办法很简单, 让对象的new和赋值成为原子操作即可.

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();
        }
    }
    
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容