Java 同期変数
この記事では、Java で変数を同期またはロックする方法について説明します。
マルチスレッド プログラミングでは、同期またはロックが不可欠です。 並行して実行されているスレッドが、同じ変数または他のリソースにアクセスしようとすると、予期しない結果が生じる場合があります。
同期またはロックは、このようなエラー ケースを回避するためのソリューションです。
同期
キーワード
同期は Java の伝統的な方法です。 同期ブロックは、synchronized
キーワードを使用して実装されます。
スレッドが同期
コードに入ると、他の Java スレッドはブロック状態になります。 現在の Java スレッドが synchronized
ブロックを終了すると、他の Java スレッドが synchronized
ブロックにアクセスしようとする可能性があります。
この方法には欠点があります。 synchronized
ブロックでは、現在の Java スレッドが作業を終了した後、スレッドがキューで待機して変数にアクセスすることは許可されないため、Java スレッドは長時間待機する必要があります。
メソッドまたはブロックで Synchronized
キーワードを使用する
以下の例では、MultiThreadList
クラスの run()
メソッドで MenuObj
を同期します。 listItem()
コード ブロックを synchronized
で定義することもできます。
どちらの関数も同じ結果になります。
同じプログラムは、メソッドの同期により次のようになります。
public synchronized void listItem(String item) {
{ /*Code to Synchronize*/
}
}
synchronized (this) {
System.out.println("\nMenu Item - \t" + item);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Listed - " + item);
}
ここでは、synchronized
ブロックを使用した完全なサンプル コードを確認できます。
コード例:
import java.io.*;
import java.util.*;
// Menu class
class Menu {
// List an item
public void listItem(String item) {
System.out.println("\nMenu Item - " + item);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Listed - " + item);
}
}
// Multi-thread menu listing
class MultiThreadList extends Thread {
private String item;
Menu MenuObj;
/* Gets menu object and a string item*/
MultiThreadList(String m, Menu obj) {
item = m;
MenuObj = obj;
}
public void run() {
/* Only one Java thread can list an item at a time.*/
synchronized (MenuObj) {
// Menu object synchronized
MenuObj.listItem(item);
}
}
}
// Main
class MainSync {
public static void main(String args[]) {
Menu listItem = new Menu();
MultiThreadList M1 = new MultiThreadList("Rice", listItem);
MultiThreadList M2 = new MultiThreadList("Icecream", listItem);
// Start two threads
M1.start();
M2.start();
// Wait for thread completion
try {
M1.join();
M2.join();
} catch (Exception e) {
e.printStackTrace();
}
}
}
出力:
Menu Item - Rice
Listed - Rice
Menu Item - Icecream
Listed - Icecream
上記の出力では、1つのスレッドが MenuObj
にアクセスすると、リストされた項目の後にメニュー項目が出力されるため、他のスレッドはアクセスできないことがわかります。 複数のスレッドが同じブロックに同時にアクセスしようとすると、メニュー項目とリストされた項目を異なる順序で出力する必要があります。
ReentrantLock
メソッドを使用する
Java の ReentrantLock
クラスは、変数をロックする柔軟な方法です。 ReentrantLock
クラスは、共通のリソースまたは変数にアクセスするときに同期を提供します。
lock()
および unlock()
メソッドがプロセスを実行します。 一度に 1つの Java スレッドがロックを取得します。 この間、他のスレッドはブロック状態になります。
スレッドはロックに何度も入ることができます。 したがって、名前は再入可能です。 Java スレッドがロックを取得すると、保持カウントは 1 になり、再入力時にカウントが加算されます。
unlock()
が実行されると、ロック保持カウントが 1つ減少します。 ReentrantLock
は、待機時間に基づいてスレッドを提供します。
待ち時間の長い Java スレッドが優先されます。
いくつかの例外が発生した場合にロックを解放するために、lock()
は try
ブロックの前に実行されます。 unblock()
は finally
ブロック内で実行されます。
ReentrantLock
関数
lock() |
ホールド カウントを 1つ増やします。 変数が空いている場合は、Java スレッドにロックを割り当てます。 |
unlock() |
保留カウントを 1 減らします。 ホールドカウントがゼロになるとロックが解除されます。 |
tryLock() |
リソースが空いている場合、この関数は true を返します。 それ以外の場合、スレッドは終了します。 |
lockInterruptibly() |
1つの Java スレッドがロックを使用すると、他の Java スレッドがこの Java スレッドに割り込むことができます。 そのため、現在の Java スレッドはすぐに戻る必要があります。 |
getHoldCount() |
リソースのロック数を返します。 |
isHeldByCurrentThread() |
現在の Java スレッドがロックを使用している場合、メソッドは true を返します。 |
ReentrantLock
を使用する
このプログラムでは、ReentrantLock
のオブジェクトを作成します。 実行可能なクラス business
が実行され、ロックが ReentrantLock
に渡されます。
結果を観察するために 2つのプールが利用可能です。 プログラムは lock()
を使用してロックを取得し、unlock()
を使用して最終的にロックを解放します。
ブール値の finished
と available
は、ロックの可用性とタスクの完了を追跡します。
2つのスレッドが実行されています。 2 番目のショップはロックを取得せず、キューで待機します。
ショップ 1 は最初に外側のロックを取得し、次に内側のロックを取得します。 ショップ2は現在待機中です。
ショップ 1 のロック保留カウントは 2 です。 ショップ 1 が内側のロックを解除し、次に外側のロックを解除すると、ロック保持カウントが減少し、ショップ 1 が閉じます。
ショップ 2 は、キューを介して自動的にロックを取得します。 ショップ 2 についてもこのプロセスが繰り返されます。
コード例:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;
class business implements Runnable {
String name;
ReentrantLock re;
public business(ReentrantLock rl, String n) {
re = rl;
name = n;
}
public void run() {
boolean finished = false;
while (!finished) {
// Get Outer Lock
boolean isLockAvailable = re.tryLock();
// If the lock is available
if (isLockAvailable) {
try {
Date d = new Date();
SimpleDateFormat ft = new SimpleDateFormat("hh:mm:ss");
System.out.println("Shop - " + name + " got outside lock at " + ft.format(d));
Thread.sleep(1500);
// Get Inner Lock
re.lock();
try {
d = new Date();
ft = new SimpleDateFormat("hh:mm:ss");
System.out.println("Shop - " + name + " got inside lock at " + ft.format(d));
System.out.println("Lock Hold Count - " + re.getHoldCount());
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// Inner lock release
System.out.println("Shop - " + name + " releasing inside lock");
re.unlock();
}
System.out.println("Lock Hold Count - " + re.getHoldCount());
System.out.println("Shop - " + name + " closed");
finished = true;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// Outer lock release
System.out.println("Shop - " + name + " releasing outside lock");
re.unlock();
System.out.println("Lock Hold Count - " + re.getHoldCount());
}
} else {
System.out.println("Shop - " + name + " waiting for lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class sample {
static final int POOL_MAX = 2;
public static void main(String[] args) {
ReentrantLock rel = new ReentrantLock();
ExecutorService pool = Executors.newFixedThreadPool(POOL_MAX);
Runnable p1 = new business(rel, "Shop 1");
Runnable p2 = new business(rel, "Shop 2");
System.out.println("Running Pool 1");
pool.execute(p1);
System.out.println("Running Pool 2");
pool.execute(p2);
pool.shutdown();
}
}
出力:
Running Pool 1
Running Pool 2
Shop - Shop 2 waiting for the lock
Shop - Shop 1 got the outside lock at 11:05:47
Shop - Shop 2 waiting for the lock
Shop - Shop 1 got the inside 'lock' at 11:05:48
Lock Hold Count - 2
Shop - Shop 2 waiting for the lock
Shop - Shop 2 waiting for the lock
Shop - Shop 1 releasing the inside lock
Lock Hold Count - 1
Shop - Shop 1 closed
Shop - Shop 1 releasing the outside lock
Lock Hold Count - 0
Shop - Shop 2 got the outside lock at 11:05:51
Shop - Shop 2 got the inside 'lock' at 11:05:52
Lock Hold Count - 2
Shop - Shop 2 releasing the inside lock
Lock Hold Count - 1
Shop - Shop 2 closed
Shop - Shop 2 releasing the outside lock
Lock Hold Count - 0
バイナリまたはカウンティング セマフォを使用する
Java のセマフォは、リソースに同時にアクセスするスレッドの数を決定します。 バイナリ セマフォは、マルチスレッド プログラミングでリソースにアクセスするためのパスまたは許可を与えます。
Java スレッドは、ロック許可が使用可能になるまで待機する必要があります。 acquire()
メソッドはパスを提供し、release()
関数はロック パスを解放します。
acquire()
関数は、ロック許可が使用可能になるまでスレッドをブロックします。
Semaphore
で使用できる 2つの状態は、permit available
と permit not available
です。
この例では、Semaphore
オブジェクトが commonResource()
関数にアクセスします。 プログラム内で 2つのスレッドが実行されています。
acquire()
メソッドはスレッドに許可を与え、release()
は可用性に基づいてロック許可を解放します。
スレッド 0 はパスを取得し、ビジー スペースに入ります。 そして、許可証を解放するとフリースペースに出てきます。
次に、スレッド 1 はロック許可を取得し、クリティカル領域に入り、許可解放後に空き領域に到達します。
コード例:
import java.util.concurrent.Semaphore;
public class SemaphoreCounter {
Semaphore binary = new Semaphore(1);
public static void main(String args[]) {
final SemaphoreCounter semObj = new SemaphoreCounter();
new Thread() {
@Override
public void run() {
semObj.commonResource();
}
}.start();
new Thread() {
@Override
public void run() {
semObj.commonResource();
}
}.start();
}
private void commonResource() {
try {
binary.acquire();
// mutual sharing resource
System.out.println(Thread.currentThread().getName() + " busy space");
} catch (InterruptedException ie) {
ie.printStackTrace();
} finally {
binary.release();
System.out.println(Thread.currentThread().getName() + " free space");
}
}
}
出力:
Thread-0 busy space
Thread-0 free space
Thread-1 busy space
Thread-1 free space
アトミック変数を使用する
アトミック変数は、Java マルチスレッド プログラミングで組み込み関数との同期を提供します。
プログラムは、タイマー制限とスレッド サイズを設定します。 executorService
は、スレッド制限までループしてタイマーを送信し、サービスをシャットダウンします。
Atomic 変数の timer
オブジェクトはタイマーを増やします。 プロセス全体が同期して行われます。
各 Java スレッドは、中断することなく一意のカウントを取得します。
コード例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
class Timer implements Runnable {
private static AtomicInteger timer;
private static final int timerLimit = 10;
private static final int totThreads = 5;
public static void main(String[] args) {
timer = new AtomicInteger(0);
ExecutorService executorService = Executors.newFixedThreadPool(totThreads);
for (int i = 0; i < totThreads; i++) {
executorService.submit(new Timer());
}
executorService.shutdown();
}
@Override
public void run() {
while (timer.get() < timerLimit) {
increaseTimer();
}
}
private void increaseTimer() {
System.out.println(Thread.currentThread().getName() + " : " + timer.getAndIncrement());
}
}
出力:
pool-1-thread-2 : 4
pool-1-thread-2 : 5
pool-1-thread-4 : 1
pool-1-thread-4 : 7
pool-1-thread-4 : 8
pool-1-thread-4 : 9
pool-1-thread-3 : 3
pool-1-thread-5 : 2
pool-1-thread-1 : 0
pool-1-thread-2 : 6
この記事では、マルチスレッド プログラミングで Java のリソースを同期する 4つの方法を説明しました。 同期
メソッドは従来のメソッドです。
柔軟性を利用するために ReentrantLock
メソッドを使用します。 ReentrantLock
にも欠点があります。プログラマーは lock()
と unlock()
を書くときに try-catch-finally
ブロックを追跡する必要があります。
バイナリ セマフォは ReentrantLock
に似ていますが、ReentrantLock
には基本レベルの同期と固定ロック メカニズムしかありません。 バイナリ セマフォは、高レベルの同期、カスタム ロック、およびデッドロック防止を提供します。
アトミック変数は、単純なデータの読み取り/書き込みアクティビティの同期を実現する方法でもあります。