Javaスレッド(三)


synchronizedはいつ使うべきですか?
       同期を実現するには大きなシステムオーバーヘッドが代償となり、デッドロックを引き起こす可能性もあるので、無駄な同期制御はできるだけ避けます。synchronizedキーワードの実現方式のため、しばしば無意味な同期制御を招き、合併度の低下をもたらす。
public class HelloRunnable implements Runnable {
	public void run() {
		System.out.println("hello");
	}

	public static void main(String[] args) {
		HelloRunnable run = new HelloRunnable();
		Thread thread = new Thread(run);
		thread.start();
	}
}
      実際には、mainメソッドのthread.start()後、ある時点でスレッドがタイムプレートに割り当てられ、オブジェクトrunのrunを起動して自動的に実行する方法があるが、run()メソッドは一回だけ実行され、即ち一回だけを印刷してプログラム運転が終了する。したがって、マルチスレッドのrun()方法については、一般的には、そのrun()法の中でまずwhile(true)のデッドサイクルがあります。
public class HelloRunnable implements Runnable {
	public void run() {
		while(true){                       //     while(true)
			System.out.println("hello");
		}
	}

	public static void main(String[] args) {
		HelloRunnable run = new HelloRunnable();
		Thread thread = new Thread(run);
		thread.start();
	}
}
       java.lang.Threadのstart()方法とrun()方法を見てみましょう。
public synchronized void start() {
	if (threadStatus != 0)
		throw new IllegalThreadStateException();
	group.add(this);
	start0();
	if (stopBeforeStart) {
		stop0(throwableFromStop);
	}
}

public void run() {
	if (target != null) {
		target.run();
	}
}
       例1.汽車の切符売り場をシミュレートするプログラムで、100枚の切符が売られています。
public class SaleTicketRunnable implements Runnable {
	private int ticket = 100;
	private boolean flag = true;
	
	@Override
	public void run() {
		while(flag) {
			sale();
		}
	}

	//private void sale () {	/**        ,         */
	private synchronized void sale () {	/**        */
		if (ticket > 0) {
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + " : sale ticket " + (ticket--));
		} 
		if (ticket <= 0) {
			flag = false;
		}
	}
	
	public static void main(String[] args) {
		SaleTicketRunnable run = new SaleTicketRunnable();
		Thread one = new Thread(run, "    ");
		one.start();
		Thread two = new Thread(run, "    ");
		two.start();
		Thread three = new Thread(run, "    ");
		three.start();
		Thread four = new Thread(run, "    ");
		four.start();
	}
}
       (1).synchronzedキーワードはrun()を修飾するために使用できません。同じ対象のsynchronized方法は一つのスレッドだけでアクセスできますので、他のスレッドにアクセスする場合は、前のスレッドが実行されるまで待たなければなりません。ここでは、4つのスレッドに対して、スレッドがあればまずタイムプレートを分けて、対象のロックを獲得します。他の3つのスレッドはチケットを売る機会がありません。
       (2)ここでは、もう一つの方法のSale()が切符を売るためのものとして定義されています。一つのスレッドが起動すると、ターゲットのrun()が自動的に呼び出されます。この時のrun()方法は、四つのスレッドが同時にアクセスできます。例えば、スレッドoneはタイムプレート(具体的にはどのスレッドが先に得られますか?先にCPUに割り当てられたタイムプレートを入手します。ここでは、スレッドoneを便利にするために)を取得し、sale()方法を呼び出します。sale()方法はsynchronizedのために、スレッドoneはこのオブジェクトのlockを取得します。スレッドoneはロックを取得した後、この時にsleep()方法を使用したため、スレッドoneはタイムプレートを譲って、他の3つのスレッドの中のいずれかが運転を開始し、同様にsale()メソッドに動作している場合、そのオブジェクトのロックはスレッドoneによって占有されていることが分かり、スレッドが待機している。
       (3)スレッドoneは、500 msのsleepを経て、ある時点で再びタイムスライスを取得し、前回のブレークポイントに続いて動作します。sale()メソッドは判断して印刷するだけなので、印刷後にticketの数を判断し、これでスレッドoneが持つオブジェクトロックはリリースされます。この時点でまだタイムカードが使われていますが、run()メソッドにはwhile(true)があります。スレッドoneはsale()メソッドを呼び出して切符を販売しています。他のスレッドはその時間が終わるまで待つしかありません。スレッドoneのタイムプレートがなくなったら、他のスレッドはsale()方法を実行し、対象のrun lockを取得して、チケットを販売します。
        例2.スレッドのデッドロック
public class DeadLockOneRunnable implements Runnable {
	private byte [] bytes;
	public DeadLockOneRunnable (byte [] bytes) {
		this.bytes= bytes;
	}
	@Override
	public void run() {
		synchronized (bytes) {
			while(true) {
				System.out.println(Thread.currentThread().getName() + "is running...");
			}
		}
	}

	public static void main(String[] args) {
		byte [] bytes = new byte[0];
		DeadLockOneRunnable run = new DeadLockOneRunnable(bytes);
		Thread thread = new Thread(run, "thread");
		thread.start();
		synchronized (bytes) {
			try {
				thread.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println("main thread run over .");
	}
}
        説明:mainスレッドでオブジェクトbytesのロックを取得し、スレッドthreadがjoinを通過する場合、メインスレッドmainはthreadスレッドが終了するまで継続して実行する必要があります。しかし、スレッドthreadが実行された後、そのrun()メソッド内部でsynchronized(bytes)が発見された場合、スレッドthreadはオブジェクトbytesのlockを持って実行する必要があります。この場合、メインスレッドがオブジェクトbytesのロックを持っている場合は、スレッドthreadの運転が終了したら実行しますが、スレッドthreadはオブジェクトbytesのロックがリリースされます。二つのスレッドは互いにリソースを待ち、デッドロックを発生する。
        例3.スレッドのデッドロック
public class DeadLockTwoRunnable implements Runnable {
	private byte[] source;
	private byte[] dest;

	public DeadLockTwoRunnable(byte[] source, byte[] dest) {
		this.source = source;
		this.dest = dest;
	}

	@Override
	public void run() {
		synchronized (source) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + " is running...");
			synchronized (dest) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + " is running...");
			}
		}
	}

	public static void main(String[] args) {
		byte[] bytesOne = new byte[0];
		byte[] bytesTwo = new byte[0];
		DeadLockTwoRunnable runOne = new DeadLockTwoRunnable(bytesOne, bytesTwo);
		Thread threadOne = new Thread(runOne, "threadOne");
		DeadLockTwoRunnable runTwo = new DeadLockTwoRunnable(bytesTwo, bytesOne);
		Thread threadTwo = new Thread(runTwo, "threadTwo");
		threadOne.start();
		threadTwo.start();
	}
}
       例えば2と3を挙げてスレッドのデッドロックと言いますが、デッドロックなどが現れるというわけではありません。synchronizedとsynchronized(dest)が関係ではなく並列関係であれば出現する可能性が低くなります。
       例1では、同期制御を使用しないと発生する場合について言及し、例2および例3では、同期制御を使用すると発生する場合についても言及した。同期機構は一体どうやって使うべきですか?
public class TestSyncLock {
	private int position = 0;
	/**              */
	private int[] nums;
	private String[] strs;

	public synchronized void methodOne() {

	}

	public synchronized void methodTwo() {

	}

	public synchronized void methodThree() {

	}

	public synchronized void methodFour() {

	}
}
       このクラスはスレッドセキュリティであり、マルチスレッドの同時アクセスによって配列が破損しないようにするために、各方法はsynchronizedとして宣言しなければなりません。例えば、methodOne()とmethodTwo()は共に配列numにアクセスするので、同期制御が必要です。methodThree()とmethodFour()は配列のstsにアクセスします。       methodOne()とmethodTwo()は互いに同期制御しなければならないが、これらはmethodThre()またはmethodFour()と同期制御する必要がない。これは、methodOne()とmethodTwo()が、methodThre()またはmethodFour()によって操作されるデータを操作しないからです。逆もまた然り       実際、いくつかのクラスにおいて、このようなinstance方法の同期制御方式は珍しくない。Java同期制御機構は実際には粒度化されておらず、同期機構は各オブジェクトに対して一つのロックのみを提供する。TestSyncLockのオブジェクトが作成された場合、メインスレッドにおいてオブジェクト(lockObj)のmethodOneにアクセスし、次スレッドにおいてオブジェクト(lockObj)のmethodThree()にアクセスすると、これらの方法は互いに同期制御されます。       一つの方法がsynchronizedであると宣言した場合、獲得したlockはこの方法を呼び出す対象に属する。したがって、両方の方法は同じロックを獲得しようとしています。この問題を修正するためには、各オブジェクトに複数のロックを装備する必要があります。Javaはこの機能を提供していないので、自分の仕組みを作る必要があります。いくつかのオブジェクトはinstanceデータとして作成され、これらのオブジェクトはlocksを提供するためだけに使用されます。
public class TestSyncLock {
	private int position = 0;
	/**              */
	private int[] nums;
	private String[] strs;
	private byte[] bytesOne = new byte[0];
	private byte[] bytesTwo = new byte[0];

	public void methodOne() {
		synchronized (bytesOne) {
			/** */
		}
	}

	public void methodTwo() {
		synchronized (bytesTwo) {
			/** */
		}
	}

	public void methodThree() {
		synchronized (bytesOne) {
			/** */
		}
	}

	public void methodFour() {
		synchronized (bytesTwo) {
			/** */
		}
	}
}
      このコードはもうsynchronized方法を持っていません。代わりに、方法内のsynchronizedコードブロックです。これにより、同期制御は、異なるオブジェクトに発生し、それを安全に同時進行させることができる。例えば、methodOne()は、異なるオブジェクトのlocksであるため、methodThre()またはmethodFour()と同時に実行することができる。
      しかし、この技術を使う時には、データの不一致が起こらないように、同期機構で操作する方法が必要であると確信しなければなりません。実際にはmethodOne()とmethodTwo()も同時にnumsをロックすることができますが、それは間違えやすいです。複数のオブジェクトをロックするときは、コードの中で最初から最後まで同じ順序でロックしておく必要があります。そうしないとデッドロックになります。
 
synchronized         ,    ,     synchronized f(){}           synchronized f(){},     f(){}。                  synchronized  。
  <>