ホームページ  >  記事  >  コンピューターのチュートリアル  >  シングルトン モード、それほど複雑にする必要はありませんね。

シングルトン モード、それほど複雑にする必要はありませんね。

WBOY
WBOY転載
2024-02-22 18:25:02469ブラウズ

Laomaoのデザインパターンコラムをこっそり公開しています。汚い少年になりたくないですか?何度読んでもデザインパターンをまだ思い出せないですか?それを忘れずに、老猫のことを追い続け、職場の興味深い話を通じてデザイン パターンの本質を理解してください。何を待っていますか?急いで車に乗りましょう

システム ソフトウェアを川や湖にたとえると、設計原則は OO プログラマーの武道の精神であり、設計パターンは動きです。精神的なスキルだけでは十分ではなく、動きと組み合わせる必要があります。精神的なスキルと動きが相互に補完し合う場合にのみ、強力な敵(「チート システム」など)に柔軟に対処し、無敵になることができます。

シングルトン モード、それほど複雑にする必要はありませんね。

###話###

以前Mao Maoによって整理されていたビジネスプロセスとコードプロセスは、基本的に整理されました[システム分類方法とコード分類方法]。コード側からも、システムが肥大化していた(設計原則に違反した)理由がわかりました。マオマオは徐々に正しい軌道に乗り、いくつかの簡単なビジネス シナリオから始めてシステム コードの最適化を開始することにしました。では、変更後の影響が最も少ないビジネスコードは何でしょうか? Mao Mao はそれを見て、ランダムに作成されたスレッド プールから始めることにし、シングルトン モードを使用して再構築することを計画しました。

Mao Mao が引き継ぐシステムでは、通常、スレッド プールの作成は、特定のクラスにマルチスレッド関数を直接実装する必要性に基づいています。そのため、多くのサービスのサービスクラスにスレッドプール作成の痕跡が残ります。

前に書きます

上記の子猫の問題を解決するために、シングルトン モードを使用して共有スレッド プール エグゼキュータを作成し、ファクトリ モードと組み合わせて業種に応じた分類管理を行う予定です。

次に、シングルトン モードから始めましょう。

###まとめ### シングルトン モード、それほど複雑にする必要はありませんね。単一ケースのパターン定義

シングルトンは、システム内に特定のクラスのインスタンスが 1 つだけ存在することを保証し、外部アクセス用のグローバル アクセス ポイントを提供することを目的とした設計パターンです。このモードは、システム内の特定のクラスのインスタンスの数を制御し、システム リソースを節約し、アクセスを容易にするようです。シングルトン モードを使用すると、システム内のオブジェクト インスタンスを効果的に管理し、同じ種類のオブジェクトを複数回作成することを回避し、システムのパフォーマンスとリソースの使用率を向上させることができます。シングルトン パターンを実装するには、怠け者スタイル、ハングリーマン スタイル、ダブル チェック ロックなど、さまざまな方法があります。ソフトウェア開発では、シングルトン パターンは、構成情報、ロギング、スレッド プールなど、一意のインスタンスを必要とするシナリオでよく使用されます。シングルトンモードにより、システムアーキテクチャを簡素化し、結合度を低減し、コードの保守性や拡張性を向上させることができます。

シングルトンモードの簡単な図

シングルトン モード、それほど複雑にする必要はありませんね。飢えた中国のシングルトンモード

お腹を空かせた中国人シングルトンとは何ですか?記憶を促進するために、老猫はこれを理解しています お腹を空かせた人間のイメージは、食べ物があると熱心に食べようとするものです。したがって、Hungry スタイルのシングルトン パターンのイメージは、クラスが作成されると、シングルトン オブジェクトの作成が待ちきれないということです。このシングルトン パターンは、スレッドの前に既にシングルトン オブジェクトを作成しているため、完全にスレッドセーフです。が生成されます。

次の例を見てください:

リーリー

上記のケースの長所と短所を見てみましょう:

利点: スレッド セーフ、クラスのロード時に初期化が完了し、オブジェクトの取得が高速になります。

欠点: クラスのロード時にオブジェクトの作成が完了するため、オブジェクトが呼び出されなくてもすでに存在している場合があり、メモリの無駄が発生します。
  • 現在のハードウェアとサーバーの開発はソフトウェアの開発よりも速いです。さらに、マイクロサービスとクラスターの展開により、水平拡張の敷居とコストが大幅に削減されました。したがって、Laomao は現在のメモリには実際には価値がないと感じているため、上記のシングルトン モデルに重大な欠点があるかというとそうではありませんが、個人的にはこのモデルを実際の開発プロセスで使用することに問題はないと考えています。
  • 実際、私たちが日常的に使用している Spring フレームワークでは、IOC コンテナ自体は Hungry スタイルのシングルトン モードです。Spring が開始されると、オブジェクトはメモリにロードされます。ここでは展開しません。 Spring ソースについては後ほど説明します。コードに関してはさらに詳しく説明します。

遅延シングルトン パターン

上記のハングリー シングルトン モードの欠点は、クラスのロード時にオブジェクトが作成されるため、メモリが浪費されることであると述べましたが、この種のメモリの浪費を解決するために、「lazy モード」があります。老毛はこのタイプのシングルトン パターンを次のように理解しています。怠惰な人の定義は、人々に怠惰と先延ばしの直感的な感覚を与えます。対応するモデルに関して言えば、このスキームにおけるオブジェクトの作成方法は、プログラムがオブジェクトを使用する前に、まずそのオブジェクトがインスタンス化されているかどうか (判定が空であるか) を判定し、インスタンス化されている場合には、そのオブジェクトを直接返します。それ以外の場合は、インスタンス化が最初に実行されます。

次の例を見てください:

リーリー

上記のシングルトン モードでオブジェクトを作成するとメモリの問題は解決されたように見えますが、この作成方法は本当にスレッドセーフなのでしょうか?次に簡単なテスト デモを作成しましょう:

リーリー

実行出力結果は以下のとおりです。

リーリー

上記の出力から、2 つのスレッドで取得されたオブジェクトが異なることが簡単にわかりますが、これには一定の確率があります。したがって、このマルチスレッド要求シナリオでは、スレッドの安全性の問題が発生します。

聊到共享变量访问线程安全性的问题,我们往往就想到了锁,所以,咱们在原有的代码块上加上锁对其优化试试,我们首先想到的是给方法代码块加上锁。

加锁后代码如下:

public class LazySingleton {

private LazySingleton() {
}

private static LazySingleton lazySingleton = null;
public synchronized static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton =new LazySingleton();
}
return lazySingleton;
}
}

经过上述同样的测试类运行之后,我们发现问题似乎解决了,每次运行之后得到的结果,两个线程对象的输出都是一致的。

我们用线程debug的方式看一下具体的运行情况,如下图:

シングルトン モード、それほど複雑にする必要はありませんね。

线程输出

我们可以发现,当一个线程进行初始化实例时,另一个线程就会从Running状态自动变成了Monitor状态。试想一下,如果有大量的线程同时访问的时候,在这样一个锁的争夺过程中就会有很多的线程被挂起为Monitor状态。CPU压力随着线程数的增加而持续增加,显然这种实现对性能还是很有影响的。

那还有优化的空间么?当然有,那就是大家经常听到的“DCL”即“Double Check Lock” 实现如下:

/**
 * 公众号:程序员老猫
 * 懒汉式单例模式(DCL)
 * Double Check Lock
 */
public class LazySingleton {

private LazySingleton() {
}
//使用volatile防止指令重排
private volatile static LazySingleton lazySingleton = null;
public static LazySingleton getInstance() {
if (lazySingleton == null) {
synchronized (LazySingleton.class) {
if(lazySingleton == null){
lazySingleton =new LazySingleton();
}
}
}
return lazySingleton;
}
}

通过DEBUG,我们来看一下下图:

シングルトン モード、それほど複雑にする必要はありませんね。

双重校验锁

这里引申一个常见的问题,大家在面试的时候估计也会碰到。问题:为什么要double check?去掉第二次check行不行?

回答:当2个线程同时执行getInstance方法时,都会执行第一个if判断,由于锁机制的存在,会有一个线程先进入同步语句,而另一个线程等待,当第一个线程执行了new Singleton()之后,就会退出synchronized的保护区域,这时如果没有第二重if判断,那么第二个线程也会创建一个实例,这就破坏了单例。

问题:这里为什么要加上volatile修饰关键字?回答:这里加上该关键字主要是为了防止”指令重排”。关于“指令重排”具体产生的原因我们这里不做细究,有兴趣的小伙伴可以自己去研究一下,我们这里只是去分析一下,“指令重排”所带来的影响。

lazySingleton =new LazySingleton();

这样一个看似简单的动作,其实从JVM层来看并不是一个原子性的行为,这里其实发生了三件事:

  • 给LazySingleton分配内存空间。
  • 调用LazySingleton的构造函数,初始化成员字段。
  • 将LazySingleton指向分配的内存空间(注意此时的LazySingleton就不是null了)

在此期间存在着指令重排序的优化,第2、3步的顺序是不能保证的,最后的执行顺序可能是1-2-3,也可能是1-3-2,假如执行顺序是1-3-2,我们看看会出现什么问题。看一下下图:

シングルトン モード、それほど複雑にする必要はありませんね。

指令重排执行

从上图中我们看到虽然LazySingleton不是null,但是指向的空间并没有初始化,最终被业务使用的时候还是会报错,这就是DCL失效的问题,这种问题难以跟踪难以重现可能会隐藏很久。

JDK1.5之前JMM(Java Memory Model,即Java内存模型)中的Cache、寄存器到主存的回写规定,上面第二第三的顺序无法保证。JDK1.5之后,SUN官方调整了JVM,具体化了volatile关键字,private volatile static LazySingleton lazySingleton;只要加上volatile,就可以保证每次从主存中读取(这涉及到CPU缓存一致性问题,感兴趣的小伙伴可以研究研究),也可以防止指令重排序的发生,避免拿到未完成初始化的对象。

上面这种方式可以有效降低锁的竞争,锁不会将整个方法全部锁定,而是锁定了某个代码块。其实完全做完调试之后我们还是会发现锁争夺的问题并没有完全解决,用到了锁肯定会对整个代码的执行效率带来一定的影响。所以是否存在保证线程的安全,并且能够不浪费内存完美的解决方案呢?一起看下下面的解决方案。

内部静态类单例模式

这种方式其实是利用了静态对象创建的特性来解决上述内存浪费以及线程不安全的问题。在这里我们要弄清楚,被static修饰的属性,类加载的时候,基本属性就已经加载完毕,但是静态方法却不会加载的时候自动执行,而是等到被调用之后才会执行。并且被STATIC修饰的变量JVM只为静态分配一次内存。(这里老猫不展开去聊static相关知识点,有兴趣的小伙伴也可以自行去了解一下更多JAVA中static关键字修饰之后的类、属性、方法的加载机制以及存储机制)

所以综合这一特性,我们就有了下面这样的写法:

public class LazyInnerClassSingleton {
private LazyInnerClassSingleton () {
}

public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY;
}

private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}

上面这种写法,其实也属于“懒汉式单例模式”,并且这种模式相对于“无脑加锁”以及“DCL”以及“饿汉式单例模式”来说无疑是最优的一种实现方式。

但是深度去追究的话,其实这种方式也会有问题,这种写法并不能防止反序列化和反射生成多个实例。我们简单看一下反射的破坏的测试类:

public class DestructionSingletonTest {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class enumSingletonClass = LazyInnerClassSingleton.class;
//枚举默认有个String 和 int 类型的构造器
Constructor constructor = enumSingletonClass.getDeclaredConstructor();
constructor.setAccessible(true);
//利用反射调用构造方法两次直接创建两个对象,直接破坏单例模式
LazyInnerClassSingleton singleton1 = (LazyInnerClassSingleton) constructor.newInstance();
LazyInnerClassSingleton singleton2 = (LazyInnerClassSingleton) constructor.newInstance();
}
}

这里序列化反序列化单例模式破坏老猫偷个懒,因为下面会有写到,有兴趣的小伙伴继续看下文,老猫觉得这种破坏场景在真实的业务使用场景比较极端,如果不涉及底层框架变动,光从业务角度来看,上面这些单例模式的实现已经管够了。当然如果硬是要防止上面的反射创建单例两次问题也能解决,如下:

public class LazyInnerClassSingleton {
private LazyInnerClassSingleton () {
if(LazyHolder.LAZY != null) {
throw new RuntimeException("不允许创建多个实例");
}
}

public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY;
}

private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}

写到这里,可能大家都很疑惑了,咋还没提及用单例模式优化线程池创建。下面这不来了么,老猫个人觉得上面的这种方式进行创建单例还是比较好的,所以就用这种方式重构一下线程池的创建,具体代码如下:

public class InnerClassLazyThreadPoolHelper {
public static void execute(Runnable runnable) {
ThreadPoolExecutor threadPoolExecutor = ThreadPoolHelperHolder.THREAD_POOL_EXECUTOR;
threadPoolExecutor.execute(runnable);
}
/**
 * 静态内部类创建实例(单例).
 * 优点:被调用时才会创建一次实例
 */
public static class ThreadPoolHelperHolder {
private static final int CPU = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU + 1;
private static final int MAXIMUM_POOL_SIZE = CPU * 2 + 1;
private static final long KEEP_ALIVE_TIME = 1L;
private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS;
private static final int MAX_QUEUE_NUM = 1024;

private ThreadPoolHelperHolder() {
}

private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TIME_UNIT,
new LinkedBlockingQueue(MAX_QUEUE_NUM),
new ThreadPoolExecutor.AbortPolicy());
}
}

以上がシングルトン モード、それほど複雑にする必要はありませんね。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はmryunwei.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。