楽観的ロックは非常に重要です。2つのステップで手動で実装する方法をご覧ください(非常に重要です。面接を依頼する必要があります)

Javaマルチスレッドには多くの種類のロックがあります。主な分類方法の1つは、楽観的および悲観的です。この記事では、主に楽観的ロックコードを自分で作成する方法を紹介します。ただし、完全性を確保するために、記事は基本から始めます。

1.楽観的ロックの概念

楽観的ロックの概念を書くと言われていますが、通常、楽観的ロックと悲観的ロックの概念は一緒に書かれなければなりません。比較する方が理にかなっています。

1.悲観的ロックの概念

悲観的ロック:常に最悪のケースを想定します。データを取得するたびに、他の人がデータを変更すると思うので、データを取得するたびにロックして、他の人がデータを取得したい場合にブロックするようにします。それをロックします。

たとえば、同期は悲観的なロックです。メソッドが同期変更を使用する場合、他のスレッドは、このメソッドを取得する場合、他のスレッドが解放されるまで待機する必要があります。

この悲観的なロックメカニズムは、データベースでも使用されます。たとえば、行ロック、テーブルロックなど、読み取りロック、書き込みロックなどはすべて、操作を実行する前にロックされます。このように、他のスレッドは操作を同期できず、解放されるまで待機する必要があります。

2.楽観的ロックの概念

楽観的ロック:常に最良の状況を想定します。データを取得するたびに、他の人がデータを変更しないと考えているため、ロックされません。更新中にのみ判断されます。この期間中、他の人がデータを更新します。 。

なお、「この期間中」とは、データを受信して​​から更新するまでの期間をいいます。ロックがないため、他のスレッドが変更される可能性があります。もう1つのポイントは、楽観的ロックが実際にロック解除されていることです。

概念を理解した後、例を見てみましょう。JavaのAtomicパッケージの下にあるクラスは、楽観的ロックメカニズムを使用します。公式の実装がどのようになっているのかを確認するために1つを選び、この実装メカニズムに従って自分で実装できます。

3.楽観的ロックの実装ケース

Javaの同時実行メカニズムには、アトミック性、可視性、順序という3つの主要な特性を考慮する必要があります。AtomicIntegerの役割は、原子性を確保することです。このデモを使用してください:

public class Test {
    //一个变量a
    private static volatile int a = 0;
    public static void main(String[] args) {
        Test test = new Test();
        Thread[] threads = new Thread[5];
        //定义5个线程,每个线程加10
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        System.out.println(a++);
                        Thread.sleep(500);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }
    }
}

この例は非常に単純です。変数aを定義し、初期値を0にしてから、5つのスレッドを使用して増加します。各スレッドは10ずつ増加し、論理的に言えば、合計5つのスレッドが50増加し、50未満で実行されます。理由それは内部のプラスワン操作にあります:a ++;

a ++の操作は、実際には3つのステップに分けることができます。

(1)メインメモリからaの値を読み取ります

(2)に1を追加します

(3)メインメモリにaを再フラッシュします

スレッド1はに1を追加しますが、メインメモリに再フラッシュする前に、スレッド2がそれを読み取ります。このとき、スレッド2はメモリにフラッシュされていない古い値を読み取る必要があります。これによりエラーが発生しました。解決策は、AtomicIntegerを使用することです。

public class Test3 {
    //使用AtomicInteger定义a
    static AtomicInteger a = new AtomicInteger();
    public static void main(String[] args) {
        Test3 test = new Test3();
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        //使用getAndIncrement函数进行自增操作
                        System.out.println(a.incrementAndGet());        
                        Thread.sleep(500);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }
    }
}

ここで、AtomicIntegerを使用してaを定義し、incrementAndGetを使用してインクリメント操作を実行すると、最終結果は常に50になります。それを分析しましょう:

4.楽観的ロックのケーススタディ

調べるには、AtomicIntegerのincrementAndGetメソッドから始める必要があります。この方法はロックと同じ機能を実現するためです。ここではjdk1.8バージョンが使用されており、バージョンが異なる場合があります。

/**
* Atomically increments by one the current value.
* @return the updated value
*/
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

ここでは、自動インクリメント操作が主に安全でないgetAndAddIntメソッドを使用していることがわかります。AtomicIntegerは特に導入されていないため、ソースコードは詳細に分析されません。

  • Unsafe:Unsafeは、sun.miscパッケージにあるクラスです。Unsafeクラスは、Java言語にC言語ポインタと同様のメモリ空間を操作する機能を提供します。つまり、メモリスペースを直接操作し、1ずつ増やしました。
  • unsafe.getAndAddInt:Unsafe.compareAndSwapIntメソッドが内部で呼び出されます。このメカニズムはCASメカニズムと呼ばれます。

CASは、並行アルゴリズムを比較、置換、および実装するときに一般的に使用されるテクノロジーです。CAS操作には、メモリ位置、予想される元の値、および新しい値の3つのオペランドが含まれます。CAS操作を実行すると、メモリ位置の値が予想される元の値と比較されます。一致する場合、プロセッサは位置値を新しい値に自動的に更新します。一致しない場合、プロセッサは何もしません。

例を使って説明しますが、もっと明確になると思います。

これは、あなたのお父さんがあなたに張さんと結婚することを望んでいることを意味します。実際の結婚の日まで待ってください。あなたのお父さんがあなたが実際に花嫁を手に入れたのと同じ花嫁(張さん)を期待している場合、結婚式はあなたのために開催されます、そうでなければ、結婚式は開催されません。

ただし、このようなCASメカニズムは、より一般的な問題を引き起こします。それがABA問題です。100元をテーブルに置いて戻ってきたときはまだ100元ですが、あなたが去ったとき、他の誰かがすでに100元を取り、後で戻ってきました。これがABA問題です。

楽観的ロックは非常に重要です。2つのステップで手動で実装する方法をご覧ください(非常に重要です。面接を依頼する必要があります)

ABA問題は問題ないように見えますが、実際には金融業界に隠れた危険を引き起こします。他の人が100万ドルを受け取って返還することは容認しますが、わかりませんか。

ABA問題を解決するためのアイデアは、データにバージョン番号を追加することです。

5.楽観的ロックの考え方

さて、上記で多くのことが言われましたが、実際、楽観的ロックはCASメカニズム+バージョンメカニズムによって実現できると言いたいだけです。

  • CASメカニズム:複数のスレッドがCASを使用して同じ変数を同時に更新しようとすると、そのうちの1つだけが変数の値を更新でき、他のすべてのスレッドは失敗します。CASは、「位置Vには値Aが含まれている必要があると思います。この値が含まれている場合は、Bをこの位置に配置します。それ以外の場合は、位置を変更せず、この位置の現在の値を教えてください」と効果的に述べています。
  • バージョンメカニズム:CASメカニズムは、データが更新されたときに他のデータの同期メカニズムが変更されないことを保証し、バージョンメカニズムは、同期メカニズムが変更されていないことを保証します(上記のABA問題を意味します)。

この考えに基づいて、楽観的ロックを実装できます。以下のコードを書いてみましょう。このコードは自分のコンピューターでのテストに合格しました。

次に、楽観的ロックを実装します

最初のステップ:操作するデータを定義する

public class Data {
    //数据版本号
    static int version = 1;
    //真实数据
    static String data = "java的架构师技术栈";
    public static int getVersion(){
        return version;
    }
    public static void updateVersion(){
        version = version + 1;
    }
}

ステップ2:楽観的ロックを定義する

public class OptimThread extends Thread { 
    public int version;
    public String data;
    //构造方法和getter、setter方法
    public void run() {
        // 1.读数据
        String text = Data.data;
        println("线程"+ getName() + ",获得的数据版本号为:" + Data.getVersion());
        println("线程"+ getName() + ",预期的数据版本号为:" + getVersion());
        System.out.println("线程"+ getName()+"读数据完成=========data = " + text);
        // 2.写数据:预期的版本号和数据版本号一致,那就更新
        if(Data.getVersion() == getVersion()){
            println("线程" + getName() + ",版本号为:" + version + ",正在操作数据");
            synchronized(OptimThread.class){
                if(Data.getVersion() == this.version){
                    Data.data = this.data;
                    Data.updateVersion();
                    System.out.println("线程" + getName() + "写数据完成=========data = " + this.data);
                    return ;
                }
            }
        }else{
             // 3\. 版本号不正确的线程,需要重新读取,重新执行
            println("线程"+ getName() + ",获得的数据版本号为:" + Data.getVersion());
            println("线程"+ getName() + ",预期的版本号为:" + getVersion());
            System.err.println("线程"+ getName() + ",需要重新执行。==============");
        }  
    }
}

ステップ3:テスト

public class Test {
    public static void main(String[] args) {
        for (int i = 1; i <= 2; i++) {
            new OptimThread(String.valueOf(i), 1, "fdd").start();
        }
    }
}

2つのスレッドが定義され、読み取りおよび書き込み操作が実行されます

ステップ4:結果を出力する

この結果は、データの読み取り時に変化がない限り確認できますが、データを更新する際には、現在のバージョン番号が期待されるバージョン番号と一致しているかどうかを判断する必要があります。一致している場合は更新してください。それらは一貫性がなく、更新は失敗します。

image.png

OK、今日の記事はここで終わります。何か問題があれば、批判して訂正してください。

おすすめ

転載: blog.csdn.net/doubututou/article/details/113105141