CAS操作の概要
CAS(Compare-And-Swap)は並行処理における重要な非ブロッキングアルゴリズムであり、データ整合性を保ちながらスレッドの効率的な実行を実現します。この操作はCPU命令レベルで実装されており、アトミックな特性を持っています。
CASプロセスでは、メモリ上の値が期待値と一致するか確認し、一致すれば新しい値に置換します。不一致の場合は操作が失敗します。
boolean compareAndSwap(address, expected, newValue) {
if (*address == expected) {
*address = newValue;
return true;
}
return false;
}
主要なパラメータ:
- address:更新対象となるメモリ位置
- expected:期待される現在値
- newValue:設定したい新しい値
アトミッククラスの利用
CAS操作の主な応用としてアトミッククラスがあります。java.util.concurrent.atomicパッケージ配下にあるこれらのクラスはロックなしでスレッドセーフな操作を提供し、従来の同期メカニズムより優れたパフォーマンスを発揮します。
以下はアトミック整数の使用例です:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
public static void main(String[] args) throws InterruptedException {
SafeCounter counter = new SafeCounter();
Thread worker1 = new Thread(() -> {
for (int idx = 0; idx < 50000; idx++) {
counter.incrementValue();
}
});
Thread worker2 = new Thread(() -> {
for (int idx = 0; idx < 50000; idx++) {
counter.incrementValue();
}
});
worker1.start();
worker2.start();
worker1.join();
worker2.join();
System.out.println(counter.getValue());
}
}
class SafeCounter {
private final AtomicInteger atomicValue = new AtomicInteger(0);
public void incrementValue() {
atomicValue.incrementAndGet(); // 前置インクリメント相当
// atomicValue.getAndAdd(delta); // 加算操作も可能
}
public int getValue() {
return atomicValue.get();
}
}
アトミッククラスの内部動作
アトミック操作の基本的な実装パターン:
class CustomAtomicInteger {
private volatile int currentValue;
public int incrementOperation() {
int tempValue;
int updatedValue;
do {
tempValue = currentValue;
updatedValue = tempValue + 1;
} while (!performCAS(currentValue, tempValue, updatedValue));
return tempValue;
}
private boolean performCAS(int memoryAddress, int expected, int replacement) {
// JVM内部でのネイティブ実装
return unsafeCompareAndSwap(memoryAddress, expected, replacement);
}
}
スピンロックのような無限ループにより、競合状況でも正しく処理を完了させます。
スピンロックのCAS実装
以下はCASベースのスピンロック実装:
public class NonBlockingLock {
private volatile Thread holderThread = null;
public void acquireLock() {
Thread current = Thread.currentThread();
while (!compareAndSet(holderThread, null, current)) {
// ロック取得まで待機
}
}
public void releaseLock() {
if (Thread.currentThread().equals(holderThread)) {
holderThread = null;
}
}
private boolean compareAndSet(Thread expected, Thread newValue) {
// 実際にはUnsafeクラスを使用
return true; // シンプル化のため
}
}
ABA問題の詳細
CAS操作における潜在的な問題点としてABA問題が挙げられます。ある変数がA→B→Aと変化した場合、変化していないと誤認識され、予期せぬ動作を引き起こす可能性があります。
例えば銀行口座の残高処理において:
- 初期残高:1000円
- 引き出し要求:500円
- 同時アクセス中に他のスレッドが入金処理(+500円)
- 最終的に不正な残高計算が発生
この問題を解決するためにはバージョン番号やタイムスタンプの導入が必要です。
Callableインタフェース
Runnableに似た機能を持つCallableは戻り値を持つタスクを定義できます。FutureTaskを通じてThreadで実行可能です。
import java.util.concurrent.*;
public class CallableDemo {
public static void main(String[] args) throws Exception {
TaskWithResult task = new TaskWithResult();
FutureTask<Integer> executionWrapper = new FutureTask<>(task);
Thread executor = new Thread(executionWrapper);
executor.start();
// 結果取得(ブロッキング)
Integer result = executionWrapper.get();
System.out.println("計算結果: " + result);
}
}
class TaskWithResult implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int num = 1; num <= 100; num++) {
sum += num;
}
return sum;
}
}
ReentrantLockの特徴
Java標準ライブラリの再入可能ロック実装で、synchronizedキーワードに比べ柔軟な制御が可能です。
主な違い:
- synchronizedはJVM内部実装、ReentrantLockはJavaクラスとして提供
- 明示的なlock/unlock呼び出しが必要
- tryLockメソッドによるタイムアウト指定や即時返却が可能
- 公平ロック・非公平ロックの選択肢あり
- Conditionオブジェクトによる高度な同期制御
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private static int sharedCounter = 0;
private static final ReentrantLock mutex = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread processor1 = new Thread(() -> {
for (int iteration = 0; iteration < 50000; iteration++) {
mutex.lock();
try {
sharedCounter++;
} finally {
mutex.unlock();
}
}
});
Thread processor2 = new Thread(() -> {
for (int iteration = 0; iteration < 50000; iteration++) {
mutex.lock();
try {
sharedCounter++;
} finally {
mutex.unlock();
}
}
});
processor1.start();
processor2.start();
processor1.join();
processor2.join();
System.out.println("最終カウント: " + sharedCounter);
}
}