Java の基本: アトミック参照に関する ABA の問題
1.ABAの問題
Q:アトミック クラス AtomicInteger の ABA 問題について教えてください。アトミック更新参照とは何か知っていますか? ABA 問題の解決方法
回答:
1. CAS は「ABA 問題」を引き起こします。
CAS アルゴリズムを実装するための重要な前提条件は、メモリ内の特定の時刻のデータを取り出し、比較して交換することです。マルチスレッドの場合、データの取得後に他のスレッドによってデータが変更される場合があります。
1) たとえば、スレッド 1 がメイン メモリからフェッチしたデータは A であり、その後、いくつかのビジネス操作を実行し (たとえば、5 秒かかります)、最終的にデータを C に変更する準備をします; 2) この時点で、スレッド2 は CPU リソースを占有し、
いくつかのビジネス オペレーションを実行し (たとえば、2 秒かかります)、共有データを B に変更します;
3) この時点で、スレッド 1 はまだビジネス オペレーションを実行しているため、スレッド 3 が CPU を占有します。共有データは A に変更されます;
この時点でスレッド 1 の業務は終了します. cas になると、データはAです。比較して交換した後、Cに変更されます。
上記の手順は、CAS の ABA 問題として理解できます。
説明すると、データの最終結果が A であることだけが必要で、途中で他の変更があるかどうかを気にしない場合、この問題はほとんど、またはまったく影響しません。
ただし、たとえば、データはユーザーのお金、または倉庫内の商品の量です。誰かが金銭を横領したり、商品を横領して販売したとは言えません。誰かがチェックに来たとき、データは追加されました。最終的なデータは正しいですが、これには問題があるはずです。
上記の問題を解決する技術が必要です。
2. ABA の問題に加えて、マルチスレッドの場合、AtomicInteger クラスを使用して、n++ の原子性の問題を解決しました。では、特定のカスタム クラスの原子性の問題を解決したい場合はどうでしょうか。この時点で、アトミック更新参照を使用できます: AtomicReference
3. ABA の問題を解決するには、タイムスタンプ付きのアトミック参照を使用できます: AtomicStampedReference
1. アトミック参照コードで ABA の問題を確認する
前の 2 つのブログ投稿では、アトミック性を説明するときに、n++ を例として使用しました。
しかし、実際の開発では多くのカスタム クラスが存在する必要があり、このときアトミックな参照も必要になります。次に、並行パッケージでアトミック参照を使用できます: AtomicReference.
最初に倉庫商品クラスを作成します: WareHouseGoods。
package com.koping.test;
public class WareHouseGoods {
public String name;
public String type;
public int number;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public WareHouseGoods(String name, String type, int number) {
this.name = name;
this.type = type;
this.number = number;
}
@Override
public String toString() {
return "WareHouseGoods{" +
"name='" + name + '\'' +
", type='" + type + '\'' +
", number=" + number +
'}';
}
}
次に、コードを使用して ABA 問題を検証します. 検証コードは次のとおりで、実行結果は次の図に示すとおりです.
スレッド 1 が (5 秒以内に) ロジックを処理すると、スレッド 2 がそれ自体を販売し、その後、一部の商品を倉庫に戻すことがわかります。
このとき、スレッド 1 は倉庫にまだ 100 個残っていることを確認すると、それらを取り出して直接販売します。
途中でABA問題が発生していることも知りません.あなたの商品を取り上げて高値で売って安値で買った人は、すでにお金を稼いでいるかもしれませんし、倉庫に入れているかもしれません.あなた。これらのスレッド 1 はそれを知らず、成功しました。
package com.koping.test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class AbaDemo {
static WareHouseGoods wareHouseGoods1 = new WareHouseGoods("裙子", "女性", 100);
static WareHouseGoods wareHouseGoods2 = new WareHouseGoods("裙子", "女性", 80);
static WareHouseGoods wareHouseGoods3 = new WareHouseGoods("裙子", "女性", 50);
static AtomicReference<WareHouseGoods> atomicReference = new AtomicReference<>(wareHouseGoods1);
public static void main(String[] args) {
// 线程1需要取50件货物进行销售,剩余50件。但是在之前,线程1会先做一些业务操作,假设5s.
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("线程1是否成功拿出50件: " + atomicReference.compareAndSet(wareHouseGoods1, wareHouseGoods3));
System.out.println("现在仓库货物的总数为:" + atomicReference.get());
},"线程1").start();
new Thread(() -> {
// 当前程1做业务逻辑操作时,线程2可以先取出20件进行销售,剩余80件;
// 然后再自己补回20件到仓库中,因此剩余还是100件。
atomicReference.compareAndSet(wareHouseGoods1, wareHouseGoods2);
System.out.println("线程2已拿出20件进行销售,现在仓库货物的总数为:" + atomicReference.get());
atomicReference.compareAndSet(wareHouseGoods2, wareHouseGoods1);
System.out.println("线程2已放回20件到仓库中,现在仓库货物的总数为:" + atomicReference.get());
},"线程2").start();
}
}
2. タイムスタンプ付きのアトミック参照によって ABA 問題を解決する
前のセクションの ABA 問題を解決するには、タイムスタンプ付きのアトミック参照 AtomicStampedReference を使用できます。
1) 商品の各バッチにはバージョン番号があり、商品には最初のバージョン番号: 1.
2) スレッド 1 がデータベースから取得したバージョン番号が 1 の場合、スレッド 1 はまだ業務を行っている;
3) この時点で、スレッド 2 は個人的に商品を取り出して戻しているが、最終的な商品はまだ 100 個であり、単数形であるバージョン番号が 3 に変更されました;
実行結果は下の図に示すように、バージョン番号が以前のバージョン番号 1 ではないことが判明したため、この時点で 50 アイテムを取得したときにスレッド 1 が失敗したことがわかります。 . この時点で、システムは誰かが倉庫内の商品を移動したことを認識しているため、追跡できます。
package com.koping.test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class AbaDemo {
static WareHouseGoods wareHouseGoods1 = new WareHouseGoods("裙子", "女性", 100);
static WareHouseGoods wareHouseGoods2 = new WareHouseGoods("裙子", "女性", 80);
static WareHouseGoods wareHouseGoods3 = new WareHouseGoods("裙子", "女性", 50);
// 初始版本号是1
static AtomicStampedReference<WareHouseGoods> atomicReference = new AtomicStampedReference<>(wareHouseGoods1, 1);
public static void main(String[] args) {
// 线程1需要取50件货物进行销售,剩余50件。但是在之前,线程1会先做一些业务操作,假设5s.
new Thread(() -> {
// 先从数据库中获取之前的版本号,再进行业务操作
int stamp = atomicReference.getStamp();
try {
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("\n线程1是否成功拿出50件: " + atomicReference.compareAndSet(wareHouseGoods1, wareHouseGoods3, stamp, stamp+1));
System.out.println("现在仓库货物的总数为:" + atomicReference.getReference());
},"线程1").start();
new Thread(() -> {
// 当前程1做业务逻辑操作时,线程2可以先取出20件进行销售,剩余80件;
// 然后再自己补回20件到仓库中,因此剩余还是100件。
atomicReference.compareAndSet(wareHouseGoods1, wareHouseGoods2, atomicReference.getStamp(), atomicReference.getStamp()+1);
System.out.println("线程2已拿出20件进行销售,现在仓库货物的总数为:" + atomicReference.getReference());
System.out.println("此时仓库货物的版本号为: " + atomicReference.getStamp());
atomicReference.compareAndSet(wareHouseGoods2, wareHouseGoods1, atomicReference.getStamp(), atomicReference.getStamp()+1);
System.out.println("线程2已放回20件到仓库中,现在仓库货物的总数为:" + atomicReference.getReference());
System.out.println("此时仓库货物的版本号为: " + atomicReference.getStamp());
},"线程2").start();
}
}