第14章 コンカレンシー
スレッド同期
マルチスレッド環境における実行の不確定性問題を解決するため、スレッド同期メカニズムが導入されます。これにより、あるスレッドが特定のメソッドを使用中は、他のスレッドは待機状態になります。
Javaでマルチスレッド同期問題を解決する方法は主に2つあります:
- Java SE 5.0で導入されたReentrantLockクラス
- 共有メモリのクラスメソッドにsynchronized修飾子を付ける方法
例:
public synchronized static void sub(int m)
解決策1:ロックオブジェクトと条件オブジェクト
ReentrantLockを使用してコードブロックを保護する基本的な構造は以下の通りです:
myLock.lock();
try {
// クリティカルセクション
} finally {
myLock.unlock();
}
ロックオブジェクトと条件オブジェクトの重要なポイント:
- ロックはコードセグメントを保護し、任意の時点で1つのスレッドのみが保護されたコードを実行できるようにします。
- ロックは保護されたコードセグメントに入ろうとするスレッドを管理します。
- ロックは1つ以上の関連する条件オブジェクトを持つことができます。
- 各条件オブジェクトは、保護されたコードセグメントに入ったがまだ実行できないスレッドを管理します。
解決策2:synchronizedキーワード
synchronizedキーワードの作用:
- クラス内のメソッドにsynchronized修飾子が付けられると、そのメソッドは同期メソッドと呼ばれます。
- あるスレッドが同期メソッドをアクセスしている間、他のスレッドが同じ同期メソッドにアクセスしようとするとブロックされ、現在のスレッドがメソッドから戻るまで待機します。
- スレッドが同期メソッドを使用中に、問題の要件に応じてwait()メソッドを使用して一時的に待機し、CPUの使用権を一時的に解放し、他のスレッドがこの同期メソッドを使用できるようにする必要がある場合があります。
- スレッドが同期メソッドの使用を終えると、notifyAll()メソッドを実行して、この同期メソッドを使用して待機状態にあるすべてのスレッドに待機を終了する通知を送るべきです。
実験:スレッド同期制御
実験1:テストプログラムとコードコメント
ReentrantLockと条件オブジェクトを使用したマルチスレッド同期技術の実装例:
package bank_system;
import java.util.*;
import java.util.concurrent.locks.*;
/**
* 複数の銀行口座を持つ銀行クラス、ロックを使用してアクセスをシリアル化
*/
public class Bank {
private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;
/**
* 銀行を初期化します。
* @param n 口座数
* @param initialBalance 各口座の初期残高
*/
public Bank(int n, double initialBalance) {
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
}
/**
* 指定された金額をある口座から別の口座に転送します。
* @param from 転送元の口座
* @param to 転送先の口座
* @param amount 転送金額
*/
public void transfer(int from, int to, double amount) throws InterruptedException {
bankLock.lock();
try {
while (accounts[from] < amount)
sufficientFunds.await();
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll();
} finally {
bankLock.unlock();
}
}
/**
* すべての口座の残高の合計を取得します。
* @return 総残高
*/
public double getTotalBalance() {
bankLock.lock();
try {
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
} finally {
bankLock.unlock();
}
}
/**
* 銀行内の口座数を取得します。
* @return 口座数
*/
public int size() {
return accounts.length;
}
}
package bank_system;
/**
* このプログラムは、複数のスレッドがデータ構造を安全にアクセスする方法を示します。
*/
public class BankTest {
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static final double MAX_AMOUNT = 1000;
public static final int DELAY = 10;
public static void main(String[] args) {
Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
for (int i = 0; i < NACCOUNTS; i++) {
int fromAccount = i;
Runnable r = () -> {
try {
while (true) {
int toAccount = (int) (bank.size() * Math.random());
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
} catch (InterruptedException e) {
}
};
Thread t = new Thread(r);
t.start();
}
}
}
synchronizedを使用した同期の実装例:
package sync_example;
import java.util.*;
/**
* 複数の同期プリミティブを使用する銀行口座を持つ銀行クラス
*/
public class Bank {
private final double[] accounts;
/**
* 銀行を初期化します。
* @param n 口座数
* @param initialBalance 各口座の初期残高
*/
public Bank(int n, double initialBalance) {
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
}
/**
* 指定された金額をある口座から別の口座に転送します。
* @param from 転送元の口座
* @param to 転送先の口座
* @param amount 転送金額
*/
public synchronized void transfer(int from, int to, double amount) throws InterruptedException {
while (accounts[from] < amount)
wait();
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
notifyAll();
}
/**
* すべての口座の残高の合計を取得します。
* @return 総残高
*/
public synchronized double getTotalBalance() {
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
/**
* 銀行内の口座数を取得します。
* @return 口座数
*/
public int size() {
return accounts.length;
}
}
package sync_example;
/**
* このプログラムは、複数のスレッドが同期メソッドを使用してデータ構造を安全にアクセスする方法を示します。
*/
public class BankTest {
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static final double MAX_AMOUNT = 1000;
public static final int DELAY = 10;
public static void main(String[] args) {
Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
for (int i = 0; i < NACCOUNTS; i++) {
int fromAccount = i;
Runnable r = () -> {
try {
while (true) {
int toAccount = (int) (bank.size() * Math.random());
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
} catch (InterruptedException e) {
}
};
Thread t = new Thread(r);
t.start();
}
}
}
競合状態の問題と解決策
以下のプログラムは競合状態を含んでおり、修正が必要です:
package race_condition;
class BankAccount {
private static int balance = 2000;
public static void withdraw(int amount) {
int temp = balance;
temp = temp - amount;
try {
Thread.sleep((int)(1000 * Math.random()));
} catch (InterruptedException e) { }
balance = temp;
System.out.println("残高=" + balance);
}
}
class Customer extends Thread {
public void run() {
for(int i = 1; i <= 4; i++) {
BankAccount.withdraw(100);
}
}
}
public class RaceConditionDemo {
public static void main(String args[]) {
Customer customer1 = new Customer();
Customer customer2 = new Customer();
customer1.start();
customer2.start();
}
}
競合状態を解決する修正版:
package race_condition;
class BankAccount {
private static int balance = 2000;
public static synchronized void withdraw(int amount) {
int temp = balance;
temp = temp - amount;
try {
Thread.sleep((int)(1000 * Math.random()));
} catch (InterruptedException e) { }
balance = temp;
System.out.println("残高=" + balance);
}
}
class Customer extends Thread {
public void run() {
for(int i = 1; i <= 4; i++) {
BankAccount.withdraw(100);
}
}
}
public class RaceConditionSolution {
public static void main(String args[]) {
Customer customer1 = new Customer();
Customer customer2 = new Customer();
customer1.start();
customer2.start();
}
}
実験2:プログラミング演習
マルチスレッドと同期メソッドを使用して、3つの窓口で10枚のチケットを販売するプログラムを作成してください。
public class TicketSystem {
public static void main(String[] args) {
TicketSeller seller = new TicketSeller();
Thread window1 = new Thread(seller);
Thread window2 = new Thread(seller);
Thread window3 = new Thread(seller);
window1.start();
window2.start();
window3.start();
}
}
class TicketSeller implements Runnable {
private int ticket = 1;
private boolean selling = true;
@Override
public void run() {
while (selling) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
if (ticket <= 10) {
System.out.println(Thread.currentThread().getName() + "窓口:第" + ticket + "枚のチケットを販売");
ticket++;
}
if (ticket > 10) {
selling = false;
}
}
}
}
}