【Java同時性とマルチスレッド】Javaのロック


本文は転載学習
テキストリンク:http://ifeve.com/locks/
ロックはsynchronized同期ブロックのようにスレッド同期メカニズムですが、Javaのsynchronized同期ブロックよりも複雑です.ロック(および他のより高度なスレッド同期メカニズム)はsynchronized同期ブロックによって実現されるため、synchronizedキーワード(Java 5以前の場合)から完全に抜け出すことはできません.
Java 5からjava.util.concurrent.locksパッケージにはロックの実装が含まれているので、自分のロックを実装する必要はありません.しかし、これらのロックの使用方法を理解し、これらの実装の背後にある理論を理解する必要があります.Java.util.concurrent.locks.Lockの説明を参照して、ロックに関する詳細を参照してください.
以下に、本明細書で説明するトピックを示します.
  • 簡単なロック
  • ロックの再入性
  • 錠の公平性
  • finally文でunlock()
  • を呼び出す
    簡単なロック
    Javaの同期ブロックから始めましょう.
    public class Counter{
    	private int count = 0;
    
    	public int inc(){
    		synchronized(this){
    			return ++count;
    		}
    	}
    }

    inc()メソッドにsynchronized(this)コードブロックがあることがわかります.このコードブロックは、return++countを同じ時間に1つのスレッドだけで実行できることを保証することができる.synchronizedの同期ブロックにおけるコードはより複雑であるが、++countという簡単な操作はスレッド同期の意味を表すのに十分である.
    以下のCounterクラスはsynchronizedの代わりにLockを用いて同様の目的を達成した.
    public class Counter{
    	private Lock lock = new Lock();
    	private int count = 0;
    
    	public int inc(){
    		lock.lock();
    		int newCount = ++count;
    		lock.unlock();
    		return newCount;
    	}
    }

    lock()メソッドは、ロックインスタンスオブジェクトをロックするので、ロックオブジェクトのunlock()メソッドが呼び出されるまで、そのオブジェクトに対してlock()メソッドを呼び出すすべてのスレッドがブロックされます.
    ここにはロッククラスの簡単な実装があります.
    public class Counter{
    public class Lock{
    	private boolean isLocked = false;
    
    	public synchronized void lock()
    		throws InterruptedException{
    		while(isLocked){
    			wait();
    		}
    		isLocked = true;
    	}
    
    	public synchronized void unlock(){
    		isLocked = false;
    		notify();
    	}
    }

    その中のwhile(isLocked)サイクルに注意して、それはまた“スピンロック”と呼ばれます.スピンロックおよびwait()およびnotify()法は,スレッド通信のこの論文でより詳細に紹介されている.isLockedがtrueの場合、lock()を呼び出すスレッドはwait()呼び出しで待機をブロックする.スレッドがnotify()呼び出しを受信せずにwait()から返されることを防止するために、このスレッドはisLocked条件を再チェックして、スレッドが起動して安全に実行を継続できるかどうか、または再待機する必要があるかを決定します.スレッドが起動すれば安全に実行を継続できるとは思いません.isLockedがfalseの場合、現在のスレッドはwhile(isLocked)ループを終了し、isLockedをtrueに戻し、lock()メソッドを呼び出している他のスレッドがLockインスタンスにロックできるようにします.
    スレッドが臨界領域(lock()とunlock()の間にある)のコードを完了すると、unlock()が呼び出されます.unlock()を実行すると、isLockedがfalseに再設定され、そのうちの1つ(もしあれば)がlock()メソッドでwait()関数を呼び出して待機状態にあるスレッドを通知(起動)します.
    ロックされたリエントラントJavaのsynchronized同期ブロックはリエント可能です.これは、javaスレッドがコード内のsynchronized同期ブロックに入り、そのため、同期ブロックが使用する同期オブジェクトに対応するパイプ上のロックが得られると、このスレッドは、同じパイプオブジェクトによって同期された別のjavaコードブロックに入ることができることを意味する.次に例を示します.
    public class Reentrant{
    	public synchronized outer(){
    		inner();
    	}
    
    	public synchronized inner(){
    		//do something
    	}
    }

    注意outer()とinner()はsynchronizedとして宣言され、Javaではsynchronized(this)ブロックと等価です.スレッドがouter()を呼び出すと、outer()でinner()を呼び出すのは問題ありません.この2つのメソッド(コードブロック)は同じスレッドオブジェクトであるためです.(「this」)が同期されています.スレッドがすでにパイプオブジェクトのロックを持っている場合、そのパイプオブジェクトに同期されているすべてのコードブロックにアクセスできます.これが再入力可能です.スレッドは、すでに所有しているロックが同期されている任意の世代コードブロックにアクセスできます.
    前述のロック実装は再入力可能ではありません.Reentrantクラスを以下のように書き換えると、スレッドがouter()を呼び出すとinner()メソッドのlock.lock()でブロックされます.
    public class Reentrant2{
    	Lock lock = new Lock();
    
    	public outer(){
    		lock.lock();
    		inner();
    		lock.unlock();
    	}
    
    	public synchronized inner(){
    		lock.lock();
    		//do something
    		lock.unlock();
    	}
    }

    outer()を呼び出すスレッドは、まずロックインスタンスをロックし、inner()を呼び出し続けます.inner()メソッドでは、このロックインスタンスがouter()メソッドでロックされているため、このロックインスタンスは失敗します(つまり、スレッドがブロックされます).
    2回のlock()の間にunlock()が呼び出されず、2回目のlockがブロックされ、lock()の実装を見た後、原因が明らかになります.
    public class Lock{
    	boolean isLocked = false;
    
    	public synchronized void lock()
    		throws InterruptedException{
    		while(isLocked){
    			wait();
    		}
    		isLocked = true;
    	}
    
    	...
    }

    スレッドがlock()を終了することを許可されるかどうかは、whileループ(スピンロック)の条件によって決定されます.現在の判断条件は、isLockedがfalseである場合にのみlock操作が許可され、どのスレッドがロックされているかは考慮されません.
    このロッククラスに再入力性を持たせるためには、小さな変更が必要です.
    public class Lock{
    	boolean isLocked = false;
    	Thread  lockedBy = null;
    	int lockedCount = 0;
    
    	public synchronized void lock()
    		throws InterruptedException{
    		Thread callingThread =
    			Thread.currentThread();
    		while(isLocked && lockedBy != callingThread){
    			wait();
    		}
    		isLocked = true;
    		lockedCount++;
    		lockedBy = callingThread;
      }
    
    	public synchronized void unlock(){
    		if(Thread.curentThread() ==
    			this.lockedBy){
    			lockedCount--;
    
    			if(lockedCount == 0){
    				isLocked = false;
    				notify();
    			}
    		}
    	}
    
    	...
    }

    現在のwhileループ(スピンロック)も、ロックインスタンスがロックされているスレッドを考慮していることに注意してください.現在のロックオブジェクトがロックされていない場合(isLocked=false)、または現在の呼び出しスレッドがロックインスタンスにロックされている場合、whileループは実行されません.lock()を呼び出すスレッドは、このメソッドを終了することができます(注:「このメソッドを終了することを許可されます」現在の意味ではwait()が呼び出されずにブロックされることを意味します.
    それ以外に、同じスレッドが1つのロックオブジェクトに対して繰り返しロックをかけた回数を記録する必要があります.そうしないと、現在のロックが複数回ロックされていても、1回のunblock()呼び出しでロック全体が解除されます.unlock()呼び出しが対応するlock()呼び出しの回数に達しないまで、ロックが解除されることを望んでいません.
    今このロック類は再入可能です.
    ロックの公平性
    Javaのsynchronizedブロックは、それらにアクセスしようとするスレッドの順序を保証しない.したがって、同じsynchronized同期ブロックへのアクセスを複数のスレッドが競合し続けると、1つまたは複数のスレッドがいつまでもアクセス権を得ることができないというリスクがある.つまり、アクセス権は常に他のスレッドに割り当てられている.この場合、スレッド飢餓と呼ばれる.このような質問を避けるために問題:ロックは公平性を実現する必要があります.本明細書で示すロックは、内部でsynchronized同期ブロックで実現されるため、公平性も保証されません.飢餓と公平には、この内容に関する議論がもっとあります.
    finally文でunlock()を呼び出す
    ロックを使用して臨界領域を保護し、臨界領域が例外を放出する可能性がある場合は、finally文でunlock()を呼び出すことが重要です.これにより、他のスレッドがロックを継続できるようにロックオブジェクトがロック解除されることを保証できます.次の例を示します.
    lock.lock();
    try{
    	//do critical section code,
    	//which may throw exception
    } finally {
    	lock.unlock();
    }

    この単純な構造は、臨界領域が異常を放出した場合、ロックオブジェクトがロック解除されることを保証する.finally文で呼び出されたunlock()でなければ、臨界領域が異常を放出した場合、ロックオブジェクトは常にロックされた状態にとどまり、ロックオブジェクト上でロック()を呼び出す他のすべてのスレッドがブロックされる.