最近两天被极客时间的新课刷群刷屏。刷屏的标题大多是“学了这么多年 Java,却连 singleton 都不会用”、“面试总被问高并发,你真的会么”这一类标题党。内容千篇一律是推荐极客时间打新的课程,《Java 并发编程实战》。

高并发哥又不是没做过,随手找了一下,发现陈皓在 2009 年的一篇文章就提到了正确的解法,以及背后的原因。《深入浅出单实例 SINGLETON 设计模式》。

文中给出几种功能上正确的 singleton 写法。

// version 1.4
public class Singleton {
    private volatile static Singleton singleton = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton== null) {
                    singleton= new Singleton();
                }
            }
        }
        return singleton;
    }
}

请留意私有变量的描述词 volatile,目的是不让编译器对指令进行重排序优化。

// version 1.5
public class Singleton {
    private volatile static Singleton singleton = new Singleton();
    private Singleton()  {}
    public static Singleton getInstance() {
        return singleton;
    }
}

这是自动加载版本。每次加载类的时候,实例就生成了。所以加载类的过程可能会很慢(特别是有很多继承、引用的情况)。

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

这是对上面 1.5 版本的修正。SingletonHolder 是个私有类,并且在 Singleton 加载的时候才会被调用,INSTANCE 才会被真正创建。

这段代码是即确保了线程安全,又实现了懒加载的较优办法。

还有一个所谓最优(优雅?代码最少?)的办法,不过不建议大家使用,可读性实在不太高。有点奇技淫巧的意思,大大牺牲了代码的可读性。

public enum Singleton{
   INSTANCE;
}

利用了 enum 的创建是线程安全这一特性。

PS:PHP 中没有 singleton 的困扰,因为 php 语言特点决定的。php-fpm 本身就是 accepter-worker 并发模式,程序员写的 PHP 程序其实只是 worker,worker 与 worker 之间由 fpm 完成资源隔离和协调,PHP 程序员并不需要从内存数据的层面考虑并发的情况。所以有句话讲得不错,singleton 在 PHP 语言中不是一个好实践(practice)。PHP 的 singleton 用简单的工厂模式就够了。


今天抽空找了下 MQTT 的 QoS2 实现方式,记录如下。原科普文链接《MQTT QoS 深度解读》。

无论是 QoS2 还是 transaction,原理都是一样的:通过一次代价非常小、成功概率足够高的操作,作为最后确认的依据。这样做并不是说绝对不出错,而是出错的概率足够低,实践中可以忽略。

sequenceDiagram
  participant Publisher
  participant Broker
  participant Subscriber
  Publisher->>Publisher: Store(Msg)
  Publisher->>Broker: PUBLISH(QoS2, Msg)
  Broker->>Broker: Store(Msg)
  Broker->>Publisher: PUBREC
  Publisher->>Broker: PUBREL
  Broker->>Subscriber: PUBLISH(QoS2, Msg)
  Broker->>Publisher: PUBCOMP
  Publisher->>Publisher: Delete(Msg)
  Subscriber->>Subscriber: Store(Msg)
  Subscriber->>Broker: PUBREC
  Broker->>Subscriber: PUBREL
  Subscriber->>Subscriber: Notify(Msg)
  Subscriber->>Broker: PUBCOMP
  Broker->>Broker: Delete(Msg)
  Subscriber->>Subscriber: Delete(Msg)

简单一点的模型,如果不需要中间的 broker,则流程如下。

sequenceDiagram
  participant Publisher
  participant Subscriber
  Publisher->>Publisher: Store(Msg)
  Publisher->>Subscriber: (1) PUBLISH(QoS2, Msg)
  Subscriber->>Subscriber: Store(Msg)
  Subscriber->>Publisher: (2) PUBREC
  Publisher->>Subscriber: (3) PUBREL
  Subscriber->>Subscriber: Notify(Msg)
  Subscriber->>Publisher: (4) PUBCOMP
  Subscriber->>Subscriber: Delete(Msg)
  Publisher->>Publisher: Delete(Msg)

从简化以后的模型可以看到,publisher 和 subscriber 有两次交互。第一次,publisher 把 msg 推送给 subscriber,对应 PUBLISH/PUBREC 指令。第二次,publisher 等于是询问 subscriber,“你是不是收到一次”,对应 PUBREL/PUBCOMP 指令。

如果没有第 (3)/(4)步,PUBREL/PUBCOMP 指令,实际就是 QoS1,*至少收到一次*。

再少一点,如果没有 (2)/(3)/(4) 步,只剩第 (1) 步,实际就是 QoS0,*至多只发送一次*。

科普文里面提问,为什么 MQTT QoS2 是两次“握手”,而不是像 TCP 一样,三次握手。我觉得这个问题太教条了。为什么 negotiate 就一定要想到 TCP 呢?当然,如果一定要回答,最本质的区别就是,MQTT QoS2 通讯是单向的,而 TCP 连接的通讯是双向的。单向的只需要一方取信于另外一方即可,而双向通讯需要两方都取信于对方。