Javaのマルチスレッド排他制御を試してみました

複数のスレッドで更新処理を行う場合、
1つの変数を同時に更新してしまうと意図していない値になる可能性があります。
そのため排他制御をして、同時に更新を行わないようにする必要があります。





複数のスレッドで、abの値を更新する処理を作成してみました。

0からインクリメントして5までの結果を出力します。

下記はまだ排他制御をしていない状態です。




import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ParallelCount {
private static int a = 0;
private static int b = 0;

public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);

try {
for (int i = 0; i < 5; i++) {
executor.execute(() -> {
increment();
System.out.println("a:" + a + ", b:" + b);
});
}

} finally {
executor.shutdown();
}
}

static private void increment() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
a++;
b++;
}
}





この状態で実行してみます。「3」の値がなく「4」が重複してしまっています。



<実行結果>




a:1, b:1
a:2, b:2
a:4, b:4
a:4, b:4
a:5, b:5





これを排他制御することによって、1から5までの値が出力されるようにしてみます。
また、偶然想定通りの結果になっていたというようなことを防ぐため、
最低5回は実行して排他制御になっていることの確認をしました。






今回試した方法










Synchronized



クラスやインスタンスは「ロック」というものを持っていて、
修飾子にsynchronizedをつけることで
開始時にロックを取得し、終了時にロックを解放させることができます。

ロックされている間は他のスレッドは処理を開始することができず、解放されるまで待たされることになります。
そのため処理時間が長いメソッドでは使うべきではなく、
必要最低限の狭い範囲で使う必要があります。




synchronized static private void increment() {

try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
a++;
b++;
}





<実行結果>




a:1, b:1
a:2, b:2
a:3, b:3
a:4, b:4
a:5, b:5





staticメソッドの場合はクラスオブジェクトのロックを取得する必要があるので、
下記のような書き方をすることもできます。




static private void increment() {

try {
synchronized (Class.forName("ParallelCount")) {
Thread.sleep(100);
a++;
b++;
}
} catch (ClassNotFoundException | InterruptedException e) {
e.printStackTrace();
}
}







StampedLock



Java8から追加された、読取と書込のアクセスを制御する機能ベースのロックです。
ロックを取得するメソッドは、ロック状態に対するアクセス状態をLong型の値で返します。
書込モードで保持されているときは、読取ロックを取得することはできません。

ロックを解放するためには unLock〜 メソッドを使います。
引数を渡すことで時間指定で解放させることもできます。




  • 参考




StampedLock (Java Platform SE 8)




import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.StampedLock;

public class ParallelCountStampLock {
private static int a = 0;
private static int b = 0;
private static final StampedLock sl = new StampedLock();

public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);

try {
for (int i = 0; i < 5; i++) {
executor.execute(() -> {
increment();
System.out.println("a:" + a + ", b:" + b);
});
}

} finally {
executor.shutdown();
}
}

static private void increment() {
long stamp = sl.writeLock();

try {
Thread.sleep(100);
a++;
b++;

} catch (InterruptedException e) {
e.printStackTrace();
} finally {
sl.unlockWrite(stamp);
}
}
}





<実行結果>




a:1, b:1
a:2, b:2
a:3, b:3
a:4, b:4
a:5, b:5







Atomic変数



「アトミック」とは他のスレッドに割り込まれないことをいい、
Atomic変数を使うことでCAS(Compare And Swap)を利用して、
比較処理と代入処理をアトミックに実行することができます。
ロックをしないので高速な処理をすることができます。




  • 参考




https://docs.oracle.com/javase/jp/8/docs/api/java/util/concurrent/atomic/package-summary.html





変数に対して制御をするようで Thread.sleep() を使った確認ができなかったため、
1から100までのインクリメントを複数スレッドから行うプログラムを作成して、排他制御の確認をしました。

下記はまだ排他制御していない状態です。




import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ParallelCount2 {
private static int a = 0;
private static int b = 0;

public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);

try {
for (int i = 0; i < 100; i++) {
executor.execute(() -> {
increment();
System.out.println("a:" + a + ", b:" + b);
});
}
} finally {
executor.shutdown();
}
}

static private void increment() {
a++;
b++;
}
}





実行してみます。bの値で「2」が重複してしまっています。



<実行結果>




a:1, b:2
a:2, b:2
a:3, b:3
a:4, b:4
a:5, b:5
a:6, b:6
a:7, b:7
a:8, b:8
a:9, b:9
a:10, b:10
a:11, b:11
a:12, b:12
a:13, b:13
a:14, b:14
a:15, b:15
a:16, b:16
a:17, b:17
a:18, b:18
a:19, b:19
a:20, b:20
a:21, b:21
a:22, b:22
a:23, b:23
a:24, b:24
a:25, b:25
a:26, b:26
a:27, b:27
a:28, b:28
a:29, b:29
a:30, b:30
a:31, b:31
a:32, b:32
a:33, b:33
a:34, b:34
a:35, b:35
a:36, b:36
a:37, b:37
a:38, b:38
a:39, b:39
a:40, b:40
a:41, b:41
a:42, b:42
a:43, b:43
a:44, b:44
a:45, b:45
a:46, b:46
a:47, b:47
a:48, b:48
a:49, b:49
a:50, b:50
a:51, b:51
a:52, b:52
a:53, b:53
a:55, b:55
a:54, b:54
a:56, b:56
a:57, b:57
a:58, b:58
a:59, b:59
a:60, b:60
a:61, b:61
a:62, b:62
a:63, b:63
a:64, b:64
a:65, b:65
a:66, b:66
a:67, b:67
a:68, b:68
a:69, b:69
a:70, b:70
a:71, b:71
a:72, b:72
a:74, b:74
a:75, b:75
a:73, b:73
a:76, b:76
a:77, b:77
a:78, b:78
a:79, b:79
a:80, b:80
a:81, b:81
a:82, b:82
a:83, b:83
a:84, b:84
a:85, b:85
a:86, b:86
a:87, b:87
a:88, b:88
a:89, b:89
a:90, b:90
a:91, b:91
a:92, b:92
a:93, b:93
a:94, b:94
a:95, b:95
a:97, b:97
a:98, b:98
a:99, b:99
a:100, b:100
a:96, b:96





AtomicIntegerを使って整数のインクリメントをアトミックにしてみます。

引数を指定しないと初期値0、引数を指定するとその値のAtomicIntegerを作成します。
incrementAndGet()を使って値をインクリメントしていきます。




  • 参考




https://docs.oracle.com/javase/jp/8/docs/api/java/util/concurrent/atomic/AtomicInteger.html



ちなみにAtomicInteger以外にもAtomicLongやAtomicBooleanなどもあります。




import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class ParallelCountAtomic {
private static AtomicInteger a = new AtomicInteger();
private static AtomicInteger b = new AtomicInteger();

public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);

try {
for (int i = 0; i < 100; i++) {
executor.execute(() -> {
increment();
System.out.println("a:" + a + ", b:" + b);
});
}
} finally {
executor.shutdown();
}
}

static private void increment() {
a.incrementAndGet();
b.incrementAndGet();
}
}





値の重複や抜けがなく出力されるようになりました。



<実行結果>




a:1, b:1
a:2, b:2
a:3, b:3
a:4, b:4
a:5, b:5
a:6, b:6
a:7, b:7
a:8, b:8
a:9, b:9
a:10, b:10
a:11, b:11
a:12, b:12
a:13, b:13
a:14, b:14
a:15, b:15
a:16, b:16
a:17, b:17
a:18, b:18
a:19, b:19
a:20, b:20
a:21, b:21
a:22, b:22
a:23, b:23
a:24, b:24
a:25, b:25
a:26, b:26
a:27, b:27
a:28, b:28
a:29, b:29
a:30, b:30
a:31, b:31
a:32, b:32
a:33, b:33
a:34, b:34
a:35, b:35
a:36, b:36
a:37, b:37
a:38, b:38
a:39, b:39
a:40, b:40
a:41, b:41
a:42, b:42
a:43, b:43
a:44, b:44
a:45, b:45
a:46, b:46
a:47, b:47
a:48, b:48
a:49, b:49
a:50, b:50
a:51, b:51
a:53, b:53
a:52, b:52
a:55, b:54
a:55, b:55
a:56, b:56
a:57, b:57
a:58, b:58
a:59, b:59
a:60, b:60
a:61, b:61
a:62, b:62
a:64, b:64
a:65, b:65
a:63, b:63
a:66, b:66
a:67, b:67
a:68, b:68
a:69, b:69
a:70, b:70
a:71, b:71
a:72, b:72
a:73, b:73
a:74, b:74
a:75, b:75
a:76, b:76
a:78, b:78
a:77, b:77
a:79, b:79
a:80, b:80
a:81, b:81
a:82, b:82
a:83, b:83
a:84, b:84
a:85, b:85
a:86, b:86
a:87, b:87
a:88, b:88
a:89, b:89
a:90, b:90
a:91, b:91
a:92, b:92
a:93, b:93
a:94, b:94
a:95, b:95
a:96, b:96
a:97, b:97
a:98, b:98
a:99, b:99
a:100, b:100






所感



volatile変数(変数の修飾子にvolatileを付ける)で排他制御になるとの記事もあったのですが、
私のプログラムで a と b の値をvolatile変数にしても重複があったりして意図した値になりませんでした。
スレッドからアクセスするたびに、共有メモリ上の変数の値と作業コピー上の共有メモリの値を一致させる機能のようですが、
使い方や使い所がよくわからなかったので、理解できるまでは使うのをやめておこうと思います。



Atomic変数を使う方法でも、私のプログラムだとたまに動作が不安定になることがあったので、
SynchronizedやStampedLockを使ったロックをする方法が確実かと思いました。
ネットの情報だとSynchronizedばかり出てきたのですが、StampedLockを使う方法も便利でした。



今回処理速度は測っていないのですが、処理速度を意識しつつ実装方法を選定するようにしていきたいです。




github.com