Javaのシングルトンパターンとシングルトン

angryTom
リリース: 2019-11-29 13:46:45
転載
2232 人が閲覧しました

まえがき

この記事は私の公式アカウントからのものです。まだ読んでいない方はぜひご覧ください。読んでいる方はもう一度ご覧ください。一部の内容、今日説明する内容 は次のとおりです :

1. シングルトン パターンとは何ですか

[シングルトン パターン]、英語名: Singleton Pattern、このパターンは非常に単純で、タイプに必要なインスタンスは 1 つだけです。作成タイプに属する一般的に使用されるソフトウェア デザイン パターンです。シングルトン モード メソッドを通じて作成されたクラスは、現在のプロセス内にインスタンスを 1 つだけ持ちます (必要に応じて、スレッド内のシングルトンにすることもできます。たとえば、スレッド コンテキスト内では同じインスタンスのみが使用されます)。

(推奨ビデオ: Java ビデオ チュートリアル)

1. シングルインスタンス クラスはインスタンスを 1 つだけ持つことができます。

2. シングルトン クラスは、独自の一意のインスタンスを作成する必要があります。

3. シングルトン クラスは、このインスタンスを他のすべてのオブジェクトに提供する必要があります。

そうすればおそらくわかります。実際、率直に言うと、プロジェクト サイクル全体でインスタンスは 1 つだけです。プロジェクトが停止すると、インスタンスは破棄されます。再起動すると、インスタンスは破棄されます。製品を再度インスタンスします。

上記では名詞[作成型]のデザインパターンについて触れましたが、作成型のデザインパターンとは何でしょうか?

作成モード: オブジェクトの作成を担当します。このモードを使用して、必要なオブジェクト インスタンスを作成します。

#作成に加えて、他に 2 つのタイプのパターンがあります:

構造 (構造) パターン: クラスとオブジェクトの組み合わせを処理します

動作モード: クラスとオブジェクト間の対話における責任は、2 つの設計モードに分割されます:

. それらについては後ほど説明しますが、ここではリストしません。

シングルトン モードでオブジェクト インスタンスを 0 から作成する方法の分析に焦点を当てましょう。

2. シングルトン モードの作成方法

シングルトン モードを実装するには、さまざまな方法があります:

「怠け者スタイル」から「腹ペコスタイル」まで、最後に「 ダブルチェックロック」モードです。 ここでは、シングルトンの作成方法をステップバイステップでゆっくり説明します。

1. 通常の論理的な思考順序

単一のインスタンスを作成したいので、まずインスタンスの作成方法を学ぶ必要があります。これは非常に簡単です。誰もがインスタンスを作成できると思います。たとえば、次のように言ってください:

/// <summary>
/// 定义一个天气类
/// </summary>
public class WeatherForecast
{
    public WeatherForecast()
    {
        Date = DateTime.Now;
    }
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string Summary { get; set; }
}


 [HttpGet]
 public WeatherForecast Get()
 {
     // 实例化一个对象实例
     WeatherForecast weather = new WeatherForecast();
     return weather;
 }
ログイン後にコピー
訪問するたびに時間が変わるため、インスタンスは常に作成され、変更されます:


## このコードが何を意味するかは誰もが理解できると思います。これ以上の苦労はせずに、すぐに進みましょう。シングルトン モードの中心的な目的は次のとおりであることがわかります:

途中で問題が発生しないように、システムの実行サイクル全体でこのインスタンスが一意であることを確認する必要があります。 じゃあ、もっと改善していきましょう、オンリーワンになりたいって言ったじゃないですか?言うのは簡単です!直接返すことはできますか:

 /// <summary>
 /// 定义一个天气类
 /// </summary>
 public class WeatherForecast
 {
     // 定义一个静态变量来保存类的唯一实例
     private static WeatherForecast uniqueInstance;

     // 定义私有构造函数,使外界不能创建该类实例
     private WeatherForecast()
     {
         Date = DateTime.Now;
     }
     /// <summary>
     /// 静态方法,来返回唯一实例
     /// 如果存在,则返回
     /// </summary>
     /// <returns></returns>
     public static WeatherForecast GetInstance()
     {
         // 如果类的实例不存在则创建,否则直接返回
         // 其实严格意义上来说,这个不属于【单例】
         if (uniqueInstance == null)
         {
             uniqueInstance = new WeatherForecast();
         }
         return uniqueInstance;
     }
     public DateTime Date { get; set; }public int TemperatureC { get; set; }
     public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
     public string Summary { get; set; }
 }
ログイン後にコピー

次に、呼び出しメソッドを変更します。デフォルトのコンストラクターはプライベート化されており、これ以上インスタンスを作成することはできないため、メソッドを直接呼び出します:

[HttpGet]
 public WeatherForecast Get()
 {
     // 实例化一个对象实例
     WeatherForecast weather = WeatherForecast.GetInstance();
     return weather;
 }
ログイン後にコピー

最後に効果を見てみましょう:


現時点では、時間が変わっていないことがわかります。これは、インスタンスが一意であることを意味します。もう終わりです!すごく嬉しいじゃないですか!


しかし、心配しないでください。ここで問題が発生します。現在はシングルスレッドなので、スレッドは 1 つだけです。複数のスレッドがある場合はどうなりますか? 複数のスレッドが同時にアクセスすると、それは普通だろうか?

ここでテストを行います。プロジェクトを開始するとき、マルチスレッドを使用して次の呼び出しを行います:

[HttpGet]
 public WeatherForecast Get()
 {
     // 实例化一个对象实例
     //WeatherForecast weather = WeatherForecast.GetInstance();

     // 多线程去调用
     for (int i = 0; i < 3; i++)
     {
         var th = new Thread(
         new ParameterizedThreadStart((state) =>
         {
             WeatherForecast.GetInstance();
         })
         );
         th.Start(i);
     }
     return null;
 }
ログイン後にコピー

次に、その効果がどのようなものかを見てみましょう。私たちの考え方によると、それは次のとおりです。コンストラクターを確認してみると、実際には次のようにはなりません:

3个线程在第一次访问GetInstance方法时,同时判断(uniqueInstance ==null)这个条件时都返回真,然后都去创建了实例,这个肯定是不对的。那怎么办呢,只要让GetInstance方法只运行一个线程运行就好了,我们可以加一个锁来控制他,代码如下:

public class WeatherForecast
{
    // 定义一个静态变量来保存类的唯一实例
    private static WeatherForecast uniqueInstance;
    // 定义一个锁,防止多线程
    private static readonly object locker = new object();

    // 定义私有构造函数,使外界不能创建该类实例
    private WeatherForecast()
    {
        Date = DateTime.Now;
    }
    /// <summary>
    /// 静态方法,来返回唯一实例
    /// 如果存在,则返回
    /// </summary>
    /// <returns></returns>
    public static WeatherForecast GetInstance()
    {
        // 当第一个线程执行的时候,会对locker对象 "加锁",
        // 当其他线程执行的时候,会等待 locker 执行完解锁
        lock (locker)
        {
            // 如果类的实例不存在则创建,否则直接返回
            if (uniqueInstance == null)
            {
                uniqueInstance = new WeatherForecast();
            }
        }

        return uniqueInstance;
    }
    public DateTime Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

    public string Summary { get; set; }
}
ログイン後にコピー

这个时候,我们再并发测试,发现已经都一样了,这样就达到了我们想要的效果,但是这样真的是最完美的么,其实不是的,因为我们加锁,只是第一次判断是否为空,如果创建好了以后,以后就不用去管这个 lock 锁了,我们只关心的是 uniqueInstance 是否为空,那我们再完善一下:

/// <summary>
/// 定义一个天气类
/// </summary>
public class WeatherForecast
{
    // 定义一个静态变量来保存类的唯一实例
    private static WeatherForecast uniqueInstance;
    // 定义一个锁,防止多线程
    private static readonly object locker = new object();

    // 定义私有构造函数,使外界不能创建该类实例
    private WeatherForecast()
    {
        Date = DateTime.Now;
    }
    /// <summary>
    /// 静态方法,来返回唯一实例
    /// 如果存在,则返回
    /// </summary>
    /// <returns></returns>
    public static WeatherForecast GetInstance()
    {
        // 当第一个线程执行的时候,会对locker对象 "加锁",
        // 当其他线程执行的时候,会等待 locker 执行完解锁
        if (uniqueInstance == null)
        {
            lock (locker)
            {
                // 如果类的实例不存在则创建,否则直接返回
                if (uniqueInstance == null)
                {
                    uniqueInstance = new WeatherForecast();
                }
            }
        }

        return uniqueInstance;
    }
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string Summary { get; set; }
}
ログイン後にコピー

这样才最终的完美实现我们的单例模式!搞定。

2、幽灵事件:指令重排

当然,如果你看完了上边的那四步已经可以出师了,平时我们就是这么使用的,也是这么想的,但是真的就是万无一失么,有一个 JAVA 的朋友提出了这个问题,C# 中我没有听说过,是我孤陋寡闻了么:

单例模式的幽灵事件,时令重排会偶尔导致单例模式失效。

是不是听起来感觉很高大上,而不知所云,没关系,咱们平时用不到,但是可以了解了解:

为何要指令重排?       

指令重排是指的 volatile,现在的CPU一般采用流水线来执行指令。一个指令的执行被分成:取指、译码、访存、执行、写回、等若干个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。
指令流水线并不是串行的,并不会因为一个耗时很长的指令在“执行”阶段呆很长时间,而导致后续的指令都卡在“执行”之前的阶段上。
相反,流水线是并行的,多个指令可以同时处于同一个阶段,只要CPU内部相应的处理部件未被占满即可。比如说CPU有一个加法器和一个除法器,那么一条加法指令和一条除法指令就可能同时处于“执行”阶段, 而两条加法指令在“执行”阶段就只能串行工作。
相比于串行+阻塞的方式,流水线像这样并行的工作,效率是非常高的。

然而,这样一来,乱序可能就产生了。比如一条加法指令原本出现在一条除法指令的后面,但是由于除法的执行时间很长,在它执行完之前,加法可能先执行完了。再比如两条访存指令,可能由于第二条指令命中了cache而导致它先于第一条指令完成。
一般情况下,指令乱序并不是CPU在执行指令之前刻意去调整顺序。CPU总是顺序的去内存里面取指令,然后将其顺序的放入指令流水线。但是指令执行时的各种条件,指令与指令之间的相互影响,可能导致顺序放入流水线的指令,最终乱序执行完成。这就是所谓的“顺序流入,乱序流出”。

 这个是从网上摘录的,大概意思看看就行,理解双检锁失效原因有两个重点

1、编译器的写操作重排问题.

例 : B b = new B();

上面这一句并不是原子性的操作,一部分是new一个B对象,一部分是将new出来的对象赋值给b.

直觉来说我们可能认为是先构造对象再赋值.但是很遗憾,这个顺序并不是固定的.再编译器的重排作用下,可能会出现先赋值再构造对象的情况.

2、结合上下文,结合使用情景.

理解了1中的写操作重排以后,我卡住了一下.因为我真不知道这种重排到底会带来什么影响.实际上是因为我看代码看的不够仔细,没有意识到使用场景.双检锁的一种常见使用场景就是在单例模式下初始化一个单例并返回,然后调用初始化方法的方法体内使用初始化完成的单例对象.

三、Singleton = 单例 ?

 上边我们说了很多,也介绍了很多单例的原理和步骤,那这里问题来了,我们在学习依赖注入的时候,用到的 Singleton 的单例注入,是不是和上边说的一回事儿呢,这里咱们直接多多线程测试一下就行:

/// <summary>
/// 定义一个心情类
/// </summary>
public class Feeling
{
    public Feeling()
    {
        Date = DateTime.Now;
    }
    public DateTime Date { get; set; }
}


 // 单例注册到容器内
 services.AddSingleton<Feeling>();
ログイン後にコピー

这里重点表扬下评论区的@我是你帅哥 小伙伴,及时的发现了我文章的漏洞,笔芯!

紧接着我们就控制器注入服务,然后多线程测试:

 private readonly ILogger<WeatherForecastController> _logger;
 private readonly Feeling _feeling;

 public WeatherForecastController(ILogger<WeatherForecastController> logger, Feeling feeling)
 {
     _logger = logger;
     _feeling = feeling;
 }


 [HttpGet]
 public WeatherForecast Get()
 {
     // 实例化一个对象实例
     //WeatherForecast weather = WeatherForecast.GetInstance();

     // 多线程去调用
     for (int i = 0; i < 3; i++)
     {
         var th = new Thread(
         new ParameterizedThreadStart((state) =>
         {
             //WeatherForecast.GetInstance();

             // 此刻的心情
             Console.WriteLine(_feeling.Date);
         })
         );
         th.Start(i);
     }
     return null;
 }
ログイン後にコピー

测试的结果,情理之中,只在我们项目初始化服务的时候,进入了一次构造函数:

これは上で述べたことと同じです。Singleton は シングルトンであり、二重チェック ロックでもあります。なぜなら、シングルトン モードを使用すると、次のことが直接使用できるという結論がわかるからです。依存性注入は Sigleton で十分で非常に便利です。

4. シングルトン モードのメリットとデメリット

[優れた] シングルトン モードのメリット:

(1). 一意性の保証: 他のオブジェクトの防止インスタンス化を防止し、インスタンスの一意性を確保します。

(2) グローバル性: データが定義された後、現在のインスタンスとデータはプロジェクト全体のどこでも使用できます。

[劣っている], シングルトンモードのデメリット:

(1), メモリ常駐: シングルトンはライフサイクルが最も長く、開発システム全体に存在するため、データが常に追加されたり、常駐したりすると、一定量のメモリ消費が発生します。

次の内容は Baidu Encyclopedia からのものです:

利点

1. インスタンス制御

シングルトン モードは、オブジェクトはシングルトン オブジェクトの独自のコピーをインスタンス化し、すべてのオブジェクトが確実に一意のインスタンスにアクセスできるようにします。

2. 柔軟性

クラスがインスタンス化プロセスを制御するため、クラスはインスタンス化プロセスを柔軟に変更できます。

欠点

1. オーバーヘッド

数は少ないですが、オブジェクトが参照を要求するたびにクラスのインスタンスが存在すると、それでもある程度のオーバーヘッドが必要になります。この問題は、静的初期化を使用することで解決できます。

2. 開発上の混乱の可能性

シングルトン オブジェクト (特にクラス ライブラリで定義されたオブジェクト) を使用する場合、開発者は new を使用できないことを覚えておく必要があります。キーワードはオブジェクトをインスタンス化します。ライブラリのソース コードにアクセスできない可能性があるため、アプリケーション開発者は予期せずこのクラスを直接インスタンス化できない場合があります。

3. オブジェクトの有効期間

では、単一オブジェクトの削除の問題を解決できません。メモリ管理を提供する言語 (.NET Framework ベースの言語など) では、インスタンスへのプライベート参照が含まれるため、シングルトン クラスのみがインスタンスの割り当てを解除できます。一部の言語 (C など) では、他のクラスがオブジェクト インスタンスを削除できますが、これによりシングルトン クラスでの未解決参照が発生します。

この記事は、php 中国語 Web サイトの java チュートリアル コラムから引用したものです。学習へようこそ!

以上がJavaのシングルトンパターンとシングルトンの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:cnblogs.com
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート