不过时的技术
2019-02-26
最近两天被极客时间的新课刷群刷屏。刷屏的标题大多是“学了这么多年 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 连接的通讯是双向的。单向的只需要一方取信于另外一方即可,而双向通讯需要两方都取信于对方。