Java 同期変数

Shubham Vora 2023年10月12日
  1. 同期 キーワード
  2. ReentrantLock メソッドを使用する
  3. バイナリまたはカウンティング セマフォを使用する
  4. アトミック変数を使用する
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() を使用して最終的にロックを解放します。

ブール値の finishedavailable は、ロックの可用性とタスクの完了を追跡します。

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 availablepermit 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 には基本レベルの同期と固定ロック メカニズムしかありません。 バイナリ セマフォは、高レベルの同期、カスタム ロック、およびデッドロック防止を提供します。

アトミック変数は、単純なデータの読み取り/書き込みアクティビティの同期を実現する方法でもあります。

著者: Shubham Vora
Shubham Vora avatar Shubham Vora avatar

Shubham is a software developer interested in learning and writing about various technologies. He loves to help people by sharing vast knowledge about modern technologies via different platforms such as the DelftStack.com website.

LinkedIn GitHub

関連記事 - Java Variable