ปัญหา Performance ของ synchronized กับการใช้งานที่ถูกวิธี

คำเตือน: บทความชุด best practice ไม่เหมาะสำหรับผู้ที่มีความคิดว่า “เขียนๆ ไปเถอะ มันก็ใช้งานได้เหมือนกัน”

ที่มา


ในระบบที่เป็น Multithreading คงหนีไม่พ้นการใช้งาน keyword ที่ชื่อ synchronized เพื่อคอยกำกับ Thread เข้าใช้งาน resource ได้ทีละหนึ่ง Thread ตัวอย่างโค้ดนี้เป็นการแสดงว่าหากมีการใช้ Thread 2 ตัวทำการเรียกเมธอด inc1() และ inc2() จะทำให้ค่าไม่ถูกต้อง

package demo;

public class App {

	private long c1 = 0;
	private long c2 = 0;
	
	private void inc1(){
		try {
			Thread.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		c1++;
	}
	
	private void inc2(){
		try {
			Thread.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		c2++;
	}
	
	private void process(){
		for (int i = 0; i < 1000; i++) {
			inc1();
			inc2();
		}
	}
	
	public void doWork(){
		System.out.println("Starting ...");

		long start = System.currentTimeMillis();

		Thread t1 = new Thread(new Runnable() {

			@Override
			public void run() {
				process();
			}

		});

		t1.start();

		Thread t2 = new Thread(new Runnable() {

			@Override
			public void run() {
				process();
			}

		});

		t2.start();

		try {
			t1.join();
			t2.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		long end = System.currentTimeMillis();

		System.out.println("Time take: " + (end - start));
		System.out.println("c1: " + c1 + "; c2: "
				+ c2);
	}
	
	public static void main(String[] args) {
		App app = new App();
		app.doWork();
	}

}

เมื่อรันโปรแกรมก็จะได้ผลลัพธ์ประมาณนี้

Starting …
Time take: 2477
c1: 1997; c2: 1999

หากรันอีกที ค่าอาจจะจะเปลี่ยนไปเรื่อยๆ ไม่แน่นอน โปรแกรมทำงานจบโดยใช้เวลาประมาณ 2.5 วินาทีแต่ว่าได้ค่าที่ไม่ถูกต้อง ซึ่งตามทฤษฏีแล้วมันควรจะได้ c1: 2000; c2: 2000
(ผมใช้คำสั่ง Thread.sleep(1); เพื่อจำลองว่ามีการประมวลผลบางอย่างที่ใช้เวลานาน)

synchronized


หากต้องการให้ 2 thread นี้ทำงานได้ถูกต้อง เราต้องใช้ keyword ที่ชื่อ synchronized ไว้ที่ method ทั้ง inc1() และ inc2() โปรแกรมก็จะทำงานได้ถูกต้อง แต่เราอาจจะได้ยินมาว่า synchronized มันทำงานช้า ช้าขนาดใหนนั้นลองดูตัวอย่างที่ผมทำดู

package demo;

public class App {

	private long c1 = 0;
	private long c2 = 0;
	
	private synchronized void inc1(){
		try {
			Thread.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		c1++;
	}
	
	private synchronized void inc2(){
		try {
			Thread.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		c2++;
	}
	
	private void process(){
		for (int i = 0; i < 1000; i++) {
			inc1();
			inc2();
		}
	}
	
	public void doWork(){
		System.out.println("Starting ...");

		long start = System.currentTimeMillis();

		Thread t1 = new Thread(new Runnable() {

			@Override
			public void run() {
				process();
			}

		});

		t1.start();

		Thread t2 = new Thread(new Runnable() {

			@Override
			public void run() {
				process();
			}

		});

		t2.start();

		try {
			t1.join();
			t2.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		long end = System.currentTimeMillis();

		System.out.println("Time take: " + (end - start));
		System.out.println("c1: " + c1 + "; c2: "
				+ c2);
	}
	
	public static void main(String[] args) {
		App app = new App();
		app.doWork();
	}

}

ผมเพิ่ม synchronized ทั้งสองและพอรันโปรแกรมดูผลลัพธ์ พบว่าโปรแกรมทำงานได้ค่าที่ถูกต้องแต่ทว่าโปรแกรมช้าลงเกือบสองเท่า!

Starting …
Time take: 5399
c1: 2000; c2: 2000

Best Practice


แก้โปรแกรมใหม่โดยทำการประกาศตัวแปรประเภท Object ขึ้นมาสองตัวดังนี้

private Object lock1 = new Object();
private Object lock2 = new Object();

และทำการลบ synchronized ออกแล้วเพิ่ม synchronized block ข้างใน method แทน ดังนี้

private void inc1() {
	synchronized (lock1) {
		try {
			Thread.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		c1++;
	}
}

private void inc2() {
	synchronized (lock2) {
		try {
			Thread.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		c2++;
	}
}

และเมื่อรันโปรแกรมก็จะได้ผลลัพธ์ที่ดีขึ้นประมาณนี้

Starting …
Time take: 2673
c1: 2000; c2: 2000

สรุป


แทนที่เราจะใช้การ lock แบบ synchronized methods เราก็เปลี่ยนมาเป็น synchronized statements แทน และทำการ lock โดยใช้ Object แทนที่จะเป็น synchronized(this) ศึกษาเพิ่มเติมได้ที่ http://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html

ส่งท้าย


โค้ดฉบับสมบูรณ์

package demo;

public class App {

	private long c1 = 0;
	private long c2 = 0;

	private Object lock1 = new Object();
	private Object lock2 = new Object();

	private void inc1() {
		synchronized (lock1) {
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			c1++;
		}
	}

	private void inc2() {
		synchronized (lock2) {
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			c2++;
		}
	}

	private void process() {
		for (int i = 0; i < 1000; i++) {
			inc1();
			inc2();
		}
	}

	public void doWork() {
		System.out.println("Starting ...");

		long start = System.currentTimeMillis();

		Thread t1 = new Thread(new Runnable() {

			@Override
			public void run() {
				process();
			}

		});

		t1.start();

		Thread t2 = new Thread(new Runnable() {

			@Override
			public void run() {
				process();
			}

		});

		t2.start();

		try {
			t1.join();
			t2.join();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		long end = System.currentTimeMillis();

		System.out.println("Time take: " + (end - start));
		System.out.println("c1: " + c1 + "; c2: " + c2);
	}

	public static void main(String[] args) {
		App app = new App();
		app.doWork();
	}

}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s