You are on page 1of 24

intro. to Java (FEU.

faa)
ในบทที่เกานี้เราจะมาดูกันถึงเรื่องของการประมวลผลขอมูลหลาย ๆ ตัวในเวลาพรอม ๆ กัน
(ไลเลี่ยกัน) โดยไมเกิดปญหาที่ขัดแยงกันระหวางโปรแกรม (หรือ โปรแกรมยอย) ซึ่งเรา
สามารถทําไดดว ยการเรียกใช thread ของ Java

หลังจากจบบทเรียนนี้แลวผูอานจะไดทราบถึง

o ความหมายของ thread
o วงจรชีวิตของ thread
o การสรางและใช thread
o การ synchronize threads
o ตัวอยางการใช thread

9.1 Thread คืออะไร

การทํางานของสิ่งมีชีวิต เชน ตัวเราเอง (คน) สามารถที่จะคิดถึงสิ่งตาง ๆ หรือทํางานหลาย ๆ


อยางในเวลาเดียวกันไดไมยากเย็นนัก เชน เราสามารถที่จะคิดถึงเรื่องในอดีต เรื่องในอนาคต
พรอม ๆ กับการทํางานบางอยางในเวลาเดียวกันได เราสามารถที่จะปนจักรยาน พรอมกับการฟง
เพลง และดูดโอเลี้ยงในเวลาเดียวกันได (คงไมทุกคนที่ทําได?)

การ download เพลงจาก internet นั้นถาเรารอใหการ download เสร็จสิ้นกอนที่เราสามารถจะ


ฟงไดยอมไมใชสิ่งที่พึงประสงค คอมพิวเตอรสามารถที่จะ download เพลงที่เราตองการจาก
web site พรอม ๆ กับเลนเพลงใหเราฟงขณะที่การ download ยังดําเนินตอไปจนกวาการ
download จะสิ้นสุดลง

ระบบปฏิบัติการ (Operating System) เชน Windows XP สามารถทํางานไดหลาย ๆ อยางใน


เวลาเดียวกัน เชน เลนเพลงที่เราตองการ พรอม ๆ กับยอมใหเราพิมพงาน และทอง web site
หรือแมกระทั่ง run โปรแกรมหลาย ๆ ตัวในเวลาเดียวกัน

การทํางานที่ไดกลาวมา นั้นเราตองอาศัยการทํางานที่เรียกวา concurrent programming ซึ่ง


Java มีเครื่องมือที่เอื้ออํานวยถึงการทํางานดังกลาวใหเราไดใช โดยเราตองทําผานกระบวนการ
ของการประมวลผลของ thread (threads of execution) ซึ่งจะตองทําดวยการกําหนดของเรา
เอง (programmer) ใหมี thread หลาย ๆ ตัวในโปรแกรมของเรา โดยที่แตละตัวทํางานตาม
หนาที่ของตัวเอง ซึ่งอาจเปนการรวมกันกับ thread ตัวอื่น ๆ ที่อยูในโปรแกรมนั้น ๆ

Thread มีสวนประกอบที่คลายกับการทํางานของโปรแกรมโดยทั่วไป กลาวคือ thread จะมี


จุดเริ่มตนของการประมวลชุดคําสั่ง การประมวลชุดคําสั่งตามลําดับขั้น และสุดทายก็คือ การยุติ
การประมวลชุดคําสั่ง หนังสือหลายเลมใหความหมายของ thread วาเปน lightweight process1
ซึ่งหมายถึงกระบวนการที่ไมตองการใชทรัพยากรของระบบมากมายนัก ในภาพที่ 9.1 เรามี
thread อยูสองตัวที่มีชุดคําสั่งตามลําดับ (แสดงดวยเครื่องหมายลูกศร)

1
ดูขอมูลเพิ่มเติมไดจาก หนังสือที่เกี่ยวกับระบบปฏิบัติการ (Operating System)
เริ่มตนการเขียนโปรแกรมดวย Java

intro. to Java (FEU.faa)


โปรแกรมที่มี
thread อยู 2 ตัว

Thread 1 Thread 2

ภาพที่ 9.1 โปรแกรมที่แบงการทํางานให thread 2 ตัว

ในมุมมองของการเขียนโปรแกรมแบบ concurrent programming นั้น thread ก็อาจเรียกไดวา


เปน virtual CPU กลาวคือ มีสวนประกอบที่สําคัญสําหรับการประมวลผลอยูสองสวนที่เรียกวา
1). Program code และ 2). Data ดังนั้นถาเรามองการประมวลผลของ thread ใด ๆ แลวเราก็
สามารถแบงองคประกอบของ thread ออกไดเปนสามสวนคือ

1. A virtual CPU
2. code ที่ CPU ทําการประมวลผลอยู
3. data ที่ code ใชในการประมวลผล

CPU

CODE DATA

ภาพที่ 9.2 สวนประกอบของ Thread

การเขียนโปรแกรมดวยการใช thread ตองมีความระมัดระวังเปนอยางสูง ทั้งนี้ก็เพราะวา thread


เปนกระบวนการที่ตองการการดูแลเปนพิเศษ เพราะฉะนั้น Java จึงไดสราง class Timer และ
TimerTask ไวใหเราใชแทนการใช thread (โดยตรง) ในงานบางชิ้นที่มีการทํางานหลาย ๆ
อยางในเวลาเดียวกัน เชน โปรแกรมการหา compound interests ในบทที่สาม แตงานหลาย ๆ
อยางก็หลีกเลียงการใช thread ไมได Java กําหนดใหมีวิธก
ี ารอยูสองวิธีในการใช thread นั่นก็
คือ 1). การสราง class ที่มีการถายทอดจาก class Thread โดยตรง และ 2).การเรียกใช
interface Runnable

9.2 การสราง Thread เบื้องตนจาก class Thread

ณ เวลาหนึ่ง ๆ thread จะอยูในสถานะที่ตาง ๆ กันขึ้นอยูกับสภาพในขณะนั้นของ thread (life


cycle of a Thread) ซึ่งมีอยูทั้งหมด 4 สถานะดังนี้

1. new หรือ start


2. executing หรือ runnable
3. waiting หรือ blocked

276
บทที่ 9: Thread

4. terminating หรือ dead

intro. to Java (FEU.faa)


เพื่อใหเห็นภาพชัดเจนถึงสถานะของ thread ดูภาพ 9.2 ประกอบ

new

โปรแกรมเริ่มการทํางาน
ของ thread

Thread ตองรอ
waiting บางอยางใหเกิดขั้น executing Thread สิ้นสุด terminating
การทํางาน

การรอของ Thread สิ้นสุดลง

ภาพที่ 9.2 วงจรชีวต


ิ ของ Thread

Thread เริ่มวงจรชีวิตจากสถานะ new ซึ่งมาจากการสราง thread ครั้งแรก และจะเขาสูสถานะ


executing เมื่อโปรแกรมสั่งให thread ทํางาน ในบางครั้ง thread อาจจะตองเขาสูสถานะ
waiting เพื่อรอให thread ตัวอื่นทํางานกอน และเมื่อไดรับการสื่อสาร (signal) จาก thread ตัว
อื่นใหกลับมาทํางานไดเทานั้น thread ตัวนี้จึงจะสามารถกลับเขาสูสถานะ executing ได ซึ่ง
กระบวนการนี้อาจเกิดขึ้นหลาย ๆ ครั้งในชวงชีวิตหนึ่ง ๆ ของ thread และเมื่อสิ้นสุดการทํางาน
แลว thread ก็จะเขาสูสถานะ terminating ซึ่งหมายถึงการจบการทํางานอยางปกติ หรือไมก็ถูก
บังคับใหยุติการทํางานนั้น ๆ ของ thread ก็ได เชน ระบบปฏิบัติการ (Operating System) ดึง
เอา thread ออกจาก CPU เพื่อใหระบบ หรือ thread ตัวอื่นไดใชเวลาใน CPU แทน เปนตน

โปรแกรมตัวอยางตอไปนี้เปนตัวอยางการสราง thread อยางงาย ๆ สามตัว ซึ่งทั้งสามตัวก็ไมทํา


อะไรมาก แคเกิดมาแลวก็นอนหลับสักพักหนึ่งเสร็จแลวก็ออกจากระบบ

1: /**
2: Simple Thread demonstration
3: */
4:
5: import java.util.Random;
6: import static java.lang.System.out;
7:
8: class Thread1Demo extends Thread {
9: private int count = 5;
10: private int sleepTime;
11: private static int threadCount = 0;
12: private static Random time;
13:
14: Thread1Demo(String name) {
15: super(name + ++threadCount);
16: time = new Random();
17: start();
18: }
19:
20: public void run() {
21: while(true) {
22: sleepTime = time.nextInt(1000);
23: out.printf("I'm %s, going to sleep for %d milliseconds%n",
getName(), sleepTime);
24: //maximum sleep time 1 second
25: try {
26: sleep(sleepTime);
27: }
28: catch(InterruptedException e) {
29: //do nothing
30: }
31: out.printf("%s done sleeping%n", getName());

277
เริ่มตนการเขียนโปรแกรมดวย Java

32: if(--count == 0) break;


33: }
34: out.printf("%s FINISHED.%n", getName().toUpperCase());

intro. to Java (FEU.faa)


35: }
36:
37: public static void main(String[] args) {
38: //creating 3 threads
39: for(int i = 0; i < 3; i++) {
40: Thread1Demo thread = new Thread1Demo("Thread#");
41: }
42: out.println("Done executing in main.");
43: }
44: }

โปรแกรม Thread1Demo.java สราง thread ดวยการ extends class Thread หลังจากนั้นก็ทํา


การ override method run() ที่มีอยูใน class Thread เพื่อทําการสงชื่อของ thread ออกไปยัง
หนาจอ

constructor ในโปรแกรมของเรา เปนจุดเริ่มตนของการสรางและเริ่มการทํางานของ thread เรา


สงชื่อของ thread ตัวนี้ไปให constructor ของ class Thread ดวยประโยค

super(name + ++threadCount);

ซึ่งจะเอาคาของ string name บวกกับคาของ threadCount เพราะฉะนั้นถาเราดูใน main() เรา


จะเห็นวาเราไดสราง thread สามตัวคือ Thread#1, Thread#2, และ Thread#3 (บรรทัดที่
40) หลังจากนั้นเราก็เริ่มการทํางานของ thread ทั้งสามตัวนี้ (ตามลําดับการสราง) ดวยคําสั่ง

start();

เราอาจมองไดวาการทํางานที่เกิดขึ้น คือ Thread#1.start(), Thread#2.start(), และ


Thread#3.start() ถูกเรียกตามลําดับ และเมื่อ start() ถูกเรียก run() ก็จะถูกเรียกโดยอัตโนมัติ
เชนกัน ดังที่แสดงในภาพที่ 9.3

Thread#1

Thread#2

Thread#3

start()

start()

start()

run()

run()

run()
Run จนกวา
count จะเทากับ
0 ซึ่งอาจเปนเวลา Run จนกวา
เทาใดก็ได ขึ้นอยู count จะเทากับ
กับ 0 ซึ่งอาจเปนเวลา Run จนกวา
nextInt(1000) เทาใดก็ได ขึ้นอยู count จะเทากับ
กับ 0 ซึ่งอาจเปนเวลา
nextInt(1000) เทาใดก็ได ขึ้นอยู
กับ
nextInt(1000)

ภาพที่ 9.3 การทํางานพรอม ๆ กันของ Thread 3 ตัว

278
บทที่ 9: Thread

เพื่อใหเห็นการทํางานของ thread ทั้งสามตัวชัดขึ้น (กอนที่เราจะอธิบายถึงสิ่งที่เกิดขึ้น) เราจึง


แสดงผลลัพธของการ run โปรแกรมใหดู

intro. to Java (FEU.faa)


I'm Thread#1, going to sleep for 884 milliseconds
Done executing in main.
I'm Thread#2, going to sleep for 91 milliseconds
I'm Thread#3, going to sleep for 66 milliseconds
Thread#3 done sleeping
I'm Thread#3, going to sleep for 532 milliseconds
Thread#2 done sleeping
I'm Thread#2, going to sleep for 80 milliseconds
Thread#2 done sleeping
I'm Thread#2, going to sleep for 968 milliseconds
Thread#3 done sleeping
I'm Thread#3, going to sleep for 669 milliseconds
Thread#1 done sleeping
I'm Thread#1, going to sleep for 947 milliseconds
Thread#2 done sleeping
I'm Thread#2, going to sleep for 785 milliseconds
Thread#3 done sleeping
I'm Thread#3, going to sleep for 742 milliseconds
Thread#1 done sleeping
I'm Thread#1, going to sleep for 614 milliseconds
Thread#2 done sleeping
I'm Thread#2, going to sleep for 256 milliseconds
Thread#3 done sleeping
I'm Thread#3, going to sleep for 329 milliseconds
Thread#2 done sleeping
THREAD#2 FINISHED.
Thread#3 done sleeping
THREAD#3 FINISHED.
Thread#1 done sleeping
I'm Thread#1, going to sleep for 71 milliseconds
Thread#1 done sleeping
I'm Thread#1, going to sleep for 230 milliseconds
Thread#1 done sleeping
THREAD#1 FINISHED.

ภายใน method run() เรากําหนดใหมีการแสดงชื่อของ thread (บรรทัดที่ 23) พรอมกับแสดง


จํานวนเวลาที่ thread ตองรอ หลังจากนั้นเราก็บังคับให thread พักการทํางานโดยเขาสูสถานะ
ของการรอ ดวยการเรียกใช sleep() ดวยคาที่มาจากการสุมในบรรทัดที่ 22 ระหวางการรอนี้
thread ตัวอื่นก็จะไดรับโอกาสใหทําการประมวลผล และหลังจากที่การรอสิ้นสุดลง thread ก็จะ
กลับเขาสูสถานะ executing อีกครั้งหนึ่ง ซึ่ง thread แตละตัวจะเวียนวายอยูในวงจรนี้จนกวาคา
ของ count จะเปนศูนย (บรรทัดที่ 32) เพราะฉะนั้นผลลัพธของการ run ในแตละครั้งของ
โปรแกรม Thread1Demo.java ก็จะออกมาไมเหมือนกัน ขึ้นอยูกับเวลาที่ใชไปในการรอ
(sleep) thread ที่ใชเวลานอยในการนอนหลับก็จะตื่นกอน thread ที่ใชเวลามาก

ผูอานจะสังเกตเห็นวาประโยคการแสดงผลในบรรทัดที่ 42 ไดรับการประมวลผลกอนที่ thread


ทั้งสามตัวจะสิ้นสุดการทํางานของตัวเอง ทั้งนี้ก็เนื่องจากวาการประมวลผลของ main() ก็เปน
การเรียกใช thread ของ Java Virtual Machine เหมือนกัน (เชนเดียวกันเราก็อาจมองไดวาเปน
main.start()) แต main() ก็ยังไมสามารถที่จะยุติการทํางานไดจนกวาการทํางานของ thread
ทั้งสามตัวจะเสร็จสิ้นลง

9.4 การสราง Thread เบื้องตนจาก Interface Runnable

โปรแกรมตัวอยางกอนหนานี้ใชการถายทอดคุณสมบัติจาก class Thread เปนตัวสรางและใช


thread แต Java ยังมีวิธีการที่เราสามารถใชไดอีกวิธีหนึ่ง นั่นก็คือ การใช Interface Runnable

ในการใช Interface Runnable นั้นเราไมจําเปนที่จะตองเรียกใช start() เหมือนที่เราทําตอนใช


class Thread เราเพียงแตเรียกใช run() เพียงตัวเดียวเทานั้นในการประมวลผลที่เราตองการ
แตสิ่งสําคัญที่เราตองทําก็คือ เรียกใช Interface Executor ทั้งนี้ก็เนื่องจากวา Runnable จะ
ไดรับการประมวลผลจาก object ที่เกิดมาจาก class ที่มีการถายทอดจาก Interface Executor
ผานทาง method execute()

279
เริ่มตนการเขียนโปรแกรมดวย Java

โปรแกรม Thread2Demo.java ที่เราเขียนขึ้นสราง thread 3 ตัวเหมือนเดิมแตเราใช Interface


Runnable และเราเลือกที่จะใช Interface ExecutorService ซึ่งเปน sub-interface ของ

intro. to Java (FEU.faa)


Executor ในการจัดการกับ thread ที่มีอยูทั้งสามตัว (เรียกวา thread pool) แตไมได
หมายความวาเราจะตองใช ExceuterService เทานั้นเรายังสามารถที่จะใชการเรียกใช start()
ไดเหมือนเดิม

และสิ่งสําคัญอีกอยางหนึ่งในการเรียกใช sleep() ก็คือ เราตองกําหนดใหเรียกโดยตรงผานทาง


class Thread เชนในบรรทัดที่ 28 เราเรียกใช Thread.sleep(sleepTime)

1: /**
2: Simple Thread demonstration
3: */
4:
5: import java.util.Random;
6: import java.util.concurrent.Executors;
7: import java.util.concurrent.ExecutorService;
8: import static java.lang.System.out;
9:
10: class Thread2Demo implements Runnable {
11: private int count = 5;
12: private int sleepTime;
13: private static int threadCount = 0;
14: private String name;
15: private static Random time;
16:
17: Thread2Demo(String name) {
18: this.name = name + ++threadCount;
19: time = new Random();
20: }
21:
22: public void run() {
23: while(true) {
24: sleepTime = time.nextInt(1000);
25: out.printf("I'm %s, going to sleep for %d milliseconds%n",
this.name, sleepTime);
26: //maximum sleep time 1 second
27: try {
28: Thread.sleep(sleepTime);
29: }
30: catch(InterruptedException e) {
31: //do nothing
32: }
33: out.printf("%s done sleeping%n", this.name);
34: if(--count == 0) break;
35: }
36: out.printf("%s FINISHED.%n", this.name.toUpperCase());
37: }
38:
39: public static void main(String[] args) {
40: //creating 3 threads
41: Thread2Demo []thread = new Thread2Demo[3];
42: for(int i = 0; i < 3; i++) {
43: thread[i] = new Thread2Demo("Thread#");
44: }
45: ExecutorService exec = Executors.newFixedThreadPool(3);
46: exec.execute(thread[0]);
47: exec.execute(thread[1]);
48: exec.execute(thread[2]);
49:
50: exec.shutdown();
51:
52: out.println("Done executing in main.");
53: }
54: }

ใน method main() เราใช array เปนตัวเก็บ thread ทั้งสามตัวเพื่อใหงายตอการสงไปให


method execute() ที่อยูใน class ExecutorService

280
บทที่ 9: Thread

Interface Executors มี static method ชื่อ newFixedThreadPool() ที่เปนตัวสราง thread


pool ที่ประกอบไปดวย thread ตามจํานวนที่กําหนดให (ในที่นี้สามตัว ดูบรรทัดที่ 45) ซึ่ง

intro. to Java (FEU.faa)


thread เหลานี้จะถูกใชโดยตัวแปร exec ในการ run (บรรทัดที่ 46 – 48) ถาหากวา method
executor() ถูกเรียกและ thread ที่มีอยูใน ExecutorService ทุกตัวกําลังทํางานอยู Runnable
ก็จะถูกเก็บไวใน queue พรอมที่จะให thread ตัวอื่นที่เสร็จงานแลว มาใชตอไป เพื่อใหเห็นภาพ
ชัดขึ้น เราจะอธิบายการทํางานของโปรแกรม Thread2Demo.java ตามขั้นตอนตอไปนี้

1. โปรแกรมสราง Runnable object สามตัวคือ Thread#1, Thread#2, และ


Thrdead#3 เก็บไวใน array ชื่อ thread (บรรทัดที่ 41 – 43)
2. โปรแกรมสราง thread pool จากคําสั่งในบรรทัดที่ 47 โดยกําหนดใหมี thread สูงสุด
ได 3 ตัว
3. โปรแกรมเรียก method execute() ใน Interface ExecutorService ดวย Runnable
object ที่มีอยูใน array thread

method execute() จะทํางานอยูสองอยางคือ

3.1 สราง thread ภายใน ExecutorService เพื่อรองรับการประมวลผลของ


Runnable object ที่ถูกสงไปให
3.2 เปลี่ยนสถานะของ Runnable object จาก new เปน runnable (หรือที่เรา
เรียกวา executing)

4. โปรแกรมเรียกใช method shutdown() เพื่อยุติการทํางานของ thread แตละตัวที่มีอยู


ใน ExecutorService หลังจากที่ thread สิ้นสุดการทํางานของ runnable object นั้น ๆ

[ทําไมถึงใช Thread pool]

การสราง thread เปนกระบวนการที่มีคาใชจายสูง ทั้งนี้ก็เพราะวาตองติดตอกับระบบปฏิบัติการ


อยูอยางสม่ําเสมอ ดังนั้นถาเรามีจํานวนของ thread อยูในระบบเยอะเราก็ควรที่จะใช thread
pool เปนตัวชวย

Thread pool ประกอบไปดวย idle thread ที่พรอมที่จะทํางานทันที (ready state) เมื่อเรา


กําหนดให thread pool ที่สรางขึ้นเปน Runnable หนึ่งในจํานวน thread ที่มีอยูเรียก method
run() และเมื่อเสร็จงาน thread ไมตายแตยังคงอยูเพื่อคอยใหบริการตอการรองขอครั้งใหม
โปรแกรมตัวอยางกอนหนานี้เรียกใช newFixedThreadPool ซึ่งกําหนดใหมี idle thread อยู
ตลอดเวลา แต Java ยังมี thread pool ตัวอืน่ ๆ ใหเลือกใชดังนี้

• newCachedThreadPool สราง thread ตามจํานวนที่กําหนดและ thread เหลานี้จะมี


ชีวิตอยูแค 60 วินาที
• newSingleThreadExecuter เปน pool ที่มี thread อยูตัวเดียวที่สงงานตามลําดับ
(sequential)
• newScheduledThreadPool เปน thread pool ที่ตายตัวสําหรับการกําหนดการทํางาน
(scheduled execution)
• newSingleThreadScheduledExecutor เปน pool ที่มี thread ตัวเดียวสําหรับการ
กําหนดการทํางาน

ถาไมใช thread pool โปรแกรมตัวอยางของเราก็ตองเขียนอีกแบบหนึ่งดังนี้ (ภายใน main())

Thread2Demo []thread = new Thread2Demo[3];


for(int i = 0; i < 3; i++) {
thread[i] = new Thread2Demo("Thread#");
}

new Thread(thread[0]).start();
new Thread(thread[1]).start();
new Thread(thread[2]).start();

ผลลัพธจากการ run โปรแกรม Thread2Demo.java ก็คลาย ๆ กับที่เราไดจากโปรแกรม


Thread1Demo.java

281
เริ่มตนการเขียนโปรแกรมดวย Java

9.5 Priority ของ Thread

intro. to Java (FEU.faa)


Thread ที่ถูกสรางขึ้น หรือเรียกใชงานจะมี priority ที่ไดกําหนดไว โดยคาของ priority ที่ Java
มีให thread แตละตัวจะมีคาอยูระหวาง 1 (MIN_PRIORITY) ถึง 10 (MAX_PRIORITY)
thread ที่มี priority สูงจะไดรับเวลาในการประมวลผลใน CPU กอน thread ที่มี priority นอย
กวา แตลําดับของการประมวลผลของ thread ไมมีสวนเกี่ยวของกับ priority แตอยางใด โดย
ปกติแลว thread เมื่อถูกสรางทุกตัวจะไดรับคา priority เทากับ 5 (NORM_PRIORITY) และจะ
ไดรับการถายทอดคุณสมบัติจาก thread ที่เปนผูสราง thread นี้

9.5.1 กําหนด และตรวจสอบ priority ของ Thread

โปรแกรม ThreadPriority.java ที่เห็นนี้เราดัดแปลงมาจากโปรแกรม Thread1Demo.java โดย


มีการเรียกใช setPriority() และ getPriority() เพื่อกําหนดและแสดง priority ของ thread ที่
สรางขึ้น

1: /**
2: Thread priority
3: */
4:
5: import java.util.Random;
6: import java.util.concurrent.Executors;
7: import java.util.concurrent.ExecutorService;
8: import static java.lang.System.out;
9:
10: class ThreadPriority extends Thread {
11: private int count = 5;
12: private int sleepTime;
13: private static int threadCount = 0;
14: private String name;
15: private static Random time;
16:
17: ThreadPriority(String name, int priority) {
18: this.name = name + ++threadCount;
19: setPriority(priority);
20: time = new Random();
21: }
22:
23: public void run() {
24: while(true) {
25: sleepTime = time.nextInt(1000);
26: out.printf("I'm %s, with a priority of %d%n",
this.name, getPriority());
27: //maximum sleep time 1 second
28: try {
29: Thread.sleep(sleepTime);
30: }
31: catch(InterruptedException e) {
32: //do nothing
33: }
34: out.printf("%s done sleeping%n", this.name);
35: if(--count == 0) break;
36: }
37: out.printf("%s FINISHED.%n", this.name.toUpperCase());
38: }
39:
40: public static void main(String[] args) {
41: //creating 3 threads with different priority
42: ThreadPriority []thread = new ThreadPriority[3];
43: for(int i = 0; i < 3; i++) {
44: thread[i] = new ThreadPriority("Thread#", i+3);
45: }
46:
47: //using Executor to manage threads
48: ExecutorService exec = Executors.newFixedThreadPool(3);
49:
50: //start threads
51: exec.execute(thread[0]);
52: exec.execute(thread[1]);
53: exec.execute(thread[2]);

282
บทที่ 9: Thread

54:
55: exec.shutdown(); //terminate threads
56: }

intro. to Java (FEU.faa)


57: }

Code ในบรรทัดที่ 19 เปนการกําหนด priority ของ thread สวน code ในบรรทัดที่ 26 เปนการ
เรียกดู priority ของ thread ขอควรระวังในการใช setPriority() ตองไมกําหนดให priority มีคา
สูงเกิน MAX_PRIORITY หรือต่ํากวา MIN_PRIORITY ผลลัพธที่เราไดจากการ run คือ

I'm Thread#1, with a priority of 3


I'm Thread#2, with a priority of 4
I'm Thread#3, with a priority of 5
Thread#2 done sleeping
I'm Thread#2, with a priority of 4
Thread#1 done sleeping
I'm Thread#1, with a priority of 3
Thread#3 done sleeping
I'm Thread#3, with a priority of 5
Thread#3 done sleeping
I'm Thread#3, with a priority of 5
Thread#3 done sleeping
I'm Thread#3, with a priority of 5
Thread#2 done sleeping
I'm Thread#2, with a priority of 4
Thread#1 done sleeping
I'm Thread#1, with a priority of 3
Thread#2 done sleeping
I'm Thread#2, with a priority of 4
Thread#3 done sleeping
I'm Thread#3, with a priority of 5
Thread#1 done sleeping
I'm Thread#1, with a priority of 3
Thread#2 done sleeping
I'm Thread#2, with a priority of 4
Thread#3 done sleeping
THREAD#3 FINISHED.
Thread#1 done sleeping
I'm Thread#1, with a priority of 3
Thread#2 done sleeping
THREAD#2 FINISHED.
Thread#1 done sleeping
THREAD#1 FINISHED.

9.6 Thread Synchronization (การประสานเวลาการทํางาน)

ในการทํางานของ thread หลาย ๆ ตัวที่มีการใชทรัพยากรรวมกัน เชน คอมพิวเตอรในระบบ


เครือขายใช printer รวมกัน โปรแกรมที่เกี่ยวกับงานระบบบัญชี สินคาคงคลัง หรือการปรับปรุง
(update) ขอมูลที่อยูในบัญชีธนาคาร หาก thread ตัวใดตัวหนึ่งกําลังปรับปรุงขอมูลอยู และ
thread อีกตัวก็พยายามที่จะปรับปรุงขอมูล ผลลัพธที่ออกมาอาจไมสะทอนถึงขอมูลที่ถูกตองได
ดังนั้นการแกไขก็ตองกําหนดให thread ตัวใดตัวหนึ่งมีสิทธิในการปรับปรุงขอมูลอยางสูงสุด นั่น
ก็คือ thread ตัวอื่นไมสามารถที่จะปรับปรุงขอมูลชุดเดียวกันไดจนกวา thread ตัวแรกจะ
ปรับปรุงเสร็จ วิธีการนี้จะทําใหขอมูลที่ไดรับการปรับปรุงเปนขอมูลที่ถูกตองและเชื่อถือได2

กอนที่เราจะไปดูถึงวิธีการของการทํางานรวมกันของ thread เราจะทดลองเขียนโปรแกรม


สําหรับการนําเงินเขาและออกจากบัญชี โดยไมมีการใชเครื่องมือของการประสานเวลาชวย เรา
ไดออกแบบให UnSyncAccount.java เปนโปรแกรมสําหรับการนําเงินเขา (deposit) และการ
ถอนเงินออก (withdraw) จากบัญชีที่ใชรวมกันระหวาง thread 4 ตัว ไฟล Deposit.java เปน
กระบวนงานที่เรียกใช method deposit() ที่มีอยูใน UnSyncAccount สวนไฟล Withdraw.java
เปนกระบวนงานที่เรียกใช method withdraw() ของ UnSyncAccount

ไฟล UnSyncAccountTest.java เปนไฟลหลักที่เราเขียนเพื่อทดสอบการทํางานรวมกันของ


thread ซึ่งมีหนาตาดังนี้

2
หนังสือหลาย ๆ เลมเรียกกระบวนการนี้วา Mutual Exclusion

283
เริ่มตนการเขียนโปรแกรมดวย Java

1: /**

intro. to Java (FEU.faa)


2: unsynchronized bank account
3: */
4:
5: import static java.lang.System.out;
6:
7: class UnSyncAccountTest {
8: public static void main(String[] args) {
9: //print header
10: out.printf("%15s%15s%15s%n", "Deposit", "Withdrawal", "Balance");
11:
12: //set up shared account with initial balance
13: UnSyncAccount sharedAccount = new UnSyncAccount(0);
14:
15: Deposit d1 = new Deposit(sharedAccount, 100.0);
16: Withdraw w1 = new Withdraw(sharedAccount, 100.0);
17: Deposit d2 = new Deposit(sharedAccount, 100.0);
18: Withdraw w2 = new Withdraw(sharedAccount, 100.0);
19:
20: //perform deposit and withdrawal simultaneously
21: new Thread(d1).start();
22: new Thread(w1).start();
23: new Thread(d2).start();
24: new Thread(w2).start();
25: }
26: }

ในโปรแกรม UnSyncAccountTest.java เรากําหนดให sharedAccount เปนบัญชีที่มีผูใช


รวมกันอยูสี่คน คือ d1, d2, w1, และ w2 โดยสองคนแรกเปนการนําเงินเขาบัญชี สวนสองคนที่
เหลือเปนการถอนเงินออกจากบัญชี ซึ่งถาดูตามการกําหนดแลว (บรรทัดที่ 15 – 18) เมื่อ
โปรแกรมยุติการทํางานจํานวนเงินที่มีอยูในบัญชีควรจะมีคาเปนศูนย (จากการกําหนดในบรรทัด
ที่ 13)

ในการนําเงินเขา และออกจากบัญชีจะถูกทําใน method deposit() และ withdraw() ที่อยูใน


ไฟล UnSyncAccount.java ซึ่งมีหนาตาดังนี้

1: /**
2: unsynchronized bank account
3: */
4:
5: import static java.lang.System.out;
6:
7: class UnSyncAccount {
8: //a shared balance
9: private double balance;
10:
11: //setup initial balance
12: public UnSyncAccount(double balance) {
13: this.balance = balance;
14: out.printf("%15c%15c%,15.2f%n", ' ', ' ', balance);
15: }
16:
17: //return a balance
18: public double getBalance() {
19: return balance;
20: }
21:
22: //deposit amount into account
23: public void deposit(double amount) {
24: out.printf("%s deposits %.2f%n",
25: Thread.currentThread(), amount);
26: balance += amount;
27: out.printf("%,15.2f%15c%,15.2f%n", amount,
28: ' ', getBalance());
29: }
30:
31: //withdraw some amount
32: public void withdraw(double amount) {
33: //do nothing when amount is not enough
34: if(amount < balance)

284
บทที่ 9: Thread

35: return;
36: out.printf("%s withdraws %.2f%n",
37: Thread.currentThread(), amount);

intro. to Java (FEU.faa)


38: balance -= amount;
39: out.printf("%15c%,15.2f%,15.2f%n",
40: ' ', amount, getBalance());
41: }
42: }

เรากําหนดใหการทํางานกับบัญชีตัวนี้ในไฟล Deposit.java และ Withdraw.java ซึ่งมี code


สําหรับการทํางานดังนี้

1: /**
2: Program to deposit into a shared account
3: */
4:
5: import static java.lang.System.out;
6: import java.util.Random;
7:
8: class Deposit implements Runnable {
9: //shared saving account
10: private UnSyncAccount saving;
11: //amount to deposit
12: private double amount;
13: //waiting time
14: private Random sleepTime = new Random();
15:
16: //set the shared account
17: public Deposit(UnSyncAccount saving, double amount) {
18: this.saving = saving;
19: this.amount = amount;
20: }
21:
22: //deposit amount 5 times
23: public void run() {
24: try {
25: for(int i = 0; i < 5; i++) {
26: saving.deposit(amount);
27: Thread.sleep(sleepTime.nextInt(2000));
28: }
29: }
30: catch(InterruptedException ie) {
31: ie.printStackTrace();
32: }
33: }
34: }

1: /**
2: Program to withdraw from a shared account
3: */
4:
5: import static java.lang.System.out;
6: import java.util.Random;
7:
8: class Withdraw implements Runnable {
9: //shared account
10: private UnSyncAccount saving;
11: //amount to withdraw
12: private double amount;
13: //busy time
14: private Random sleepTime = new Random();
15:
16: //set a shared account
17: public Withdraw(UnSyncAccount saving, double amount) {
18: this.saving = saving;
19: this.amount = amount;
20: }
21:
22: //withdraw amount five times
23: public void run() {
24: try {
25: for(int i = 0; i < 5; i++) {

285
เริ่มตนการเขียนโปรแกรมดวย Java

26: saving.withdraw(amount);
27: Thread.sleep(sleepTime.nextInt(2000));
28: }

intro. to Java (FEU.faa)


29: }
30: catch(InterruptedException ie) {
31: ie.printStackTrace();
32: }
33: }
34: }

Method หลักในการทํางานก็คือ run() โดยเราจะใหมีการนําเงินเขาและออกเปนจํานวนเทากัน


คือ 5 ครั้งและจํานวนเงินจะถูกกําหนดมาจากโปรแกรม UnSyncAccountTest.java ซึ่งในการ
ทดสอบของเราเราใหจํานวนเงินเขา และออกเทากันคือ 100 เมื่อเรา run ดูผลลัพธที่เราไดคือ

Deposit Withdrawal Balance


0.00
Thread[Thread-0,5,main] deposits 100.00
100.00 100.00
Thread[Thread-1,5,main] withdraws 100.00
100.00 0.00
Thread[Thread-2,5,main] deposits 100.00
100.00 100.00
Thread[Thread-3,5,main] withdraws 100.00
100.00 0.00
Thread[Thread-1,5,main] withdraws 100.00
100.00 -100.00
Thread[Thread-2,5,main] deposits 100.00
100.00 0.00
Thread[Thread-3,5,main] withdraws 100.00
100.00 -100.00
Thread[Thread-0,5,main] deposits 100.00
100.00 0.00
Thread[Thread-0,5,main] deposits 100.00
100.00 100.00
Thread[Thread-0,5,main] deposits 100.00
100.00 200.00
Thread[Thread-2,5,main] deposits 100.00
100.00 300.00
Thread[Thread-0,5,main] deposits 100.00
100.00 400.00
Thread[Thread-2,5,main] deposits 100.00
100.00 500.00
Thread[Thread-2,5,main] deposits 100.00
100.00 600.00

ถาผูอาน run โปรแกรมตัวนี้ดูอาจตองทําหลาย ๆ ครั้งถึงจะสังเกตเห็น error ที่เกิดขึ้น บางครั้งก็


เกิดขึ้นทันที แตบางครั้งก็ตองใชเวลานานในการ run ถึงจะเห็น error ดังกลาว ผูอานอยาลืมวา
thread ทั้งสี่ตัวทํางานในเวลาที่ไลเลี่ยกัน (concurrent programming) กับทรัพยากรตัว
เดียวกัน ดังนั้นถาทั้งสี่ตัวตางทํางานโดยไมมีการประสานกัน ผลลัพธที่ไดก็อาจไมตรงกับความ
เปนจริง

ในโปรแกรมตัวอยางที่แสดงไวกอนหนานี้ เราไดกําหนดใหมีการนําเงินเขาสองครั้ง และออกสอง


ครั้งในจํานวนที่เทากัน สิ่งที่เราคาดหวังที่จะเห็นก็คือ เมื่อโปรแกรมยุติการทํางานเงินในบัญชี
จะตองมีคาเทากับตอนเริ่มตน (ศูนย) แตประโยคที่วา

balance += amount;

ซึ่งอาจตีความเปนลําดับขั้นการทํางาน (ระดับลาง) ดังนี้

1. load balance เขาสู register


2. บวก amount
3. ยายผลลัพธที่ไดจากการบวกเขาสู balance

ทีนี้ลองสมมติวา thread ตัวหนึ่งประมวลผลขั้นตอนที่ 1 และ 2 แลวถูก interrupt และสมมติ


ตอไปวา thread ตัวที่สองถูกปลุกใหตื่น พรอมกับทําการประมวลผลที่เดียวกันกับ thread ตัวที่

286
บทที่ 9: Thread

หนึ่ง และ ณ เวลานี้เอง thread ตัวที่หนึ่งถูกปลุกใหตื่นขึ้นมาประมวลผลชุดคําสั่งที่สาม ซึ่งการ


กระทําดังกลาวทําใหคาของ balance ไมถูกตอง (ดูภาพที่ 9.4 ประกอบ)

intro. to Java (FEU.faa)


Thread 1 register Thread 2 register balance

Thread 2 sleeps
load 1000 1000

add 1500 1000

Thread 1 sleeps
load 1000 1000

add 2000 1000

store 2000 2000

Thread 2 sleeps
store 1500 1500

ภาพที่ 9.4 การเขาหาทรัพยากรรวมของ thread 2 ตัว

สิ่งที่เกิดขึ้นกับโปรแกรมของเรา มีชื่อเรียกวา Race condition ซึ่งเปนปรากฏการณที่เกิดขึ้น


ถาเราไมกําหนดใหมีการประสานเวลาของ thread เมื่อมีการใชทรัพยากรรวมกัน ในขณะใด
ขณะหนึ่งเมื่อมีกระบวนการใด ๆ ทําอะไรสักอยางหนึ่งกับทรัพยากรที่ใชรวมกันอยู กระบวนการ
อื่นจะตองรอใหกระบวนการดังกลาวเสร็จสิ้นการทํางานกับทรัพยากรนั้นกอน จึงจะมีสิทธที่จะ
ทํางานกับทรัพยากรนั้นได

Java มีเครื่องมือที่ชวยใหการทํางานที่ตองมีการประสานเวลากันเปนไปได อยู 2 ทางคือ

1. ใช locks
2. ใช synchronized

9.6.1 การใช locks ในการประสานเวลา

เมื่อ thread ตัวใดตัวหนึ่งตองการที่จะกระทํากระบวนการอยางใดอยางหนึ่งกับทรัพยากรที่ใช


รวมกัน thread ตัวนี้ก็จะทําการ lock ทรัพยากรนี้เพื่อตัวเอง thread ตัวอื่นก็ไมสามารถทําอะไร
กับทรัพยากรนี้ได จนกวา thread ตัวแรกจะปลอยทรัพยากร (unlock) ใหกับระบบ ถาหากมี
thread หลาย ๆ ตัวพยายามที่จะทําการ lock ทรัพยากรนี้เพื่อตัวเอง จะมีเพียง thread ตัวเดียว
เทานั้นที่ lock ทรัพยากรได ตัวอื่น ๆ ที่เหลืออยูจะเขาสูสถานะของการรอ (wait)

เราจะแสดงใหดูถึงการใช lock กับบัญชีในธนาคารที่มีผูรวมกันอยู (ซึ่งเราไดดัดแปลงมาจาก


โปรแกรมที่แสดงใหดูกอนหนานี้) การนําเงินเขา (deposit) และการนําเงินออก (withdraw)
จะตองทําเพียงทีละครั้ง หากคนใดนําเงินเขา อีกคนหนึ่งจะตองรอจนกวากระบวนการจะสิ้นสุดลง
การนําเงินออกก็เชนเดียวกัน ไมเชนนั้น เงินที่อยูในบัญชีจะไมตรงกันกับความเปนจริง

public void deposit(double amount) {


//lock this account
account.lock();

try {
balance += amount;
out.printf("%,15.2f%15c%,15.2f%n", amount, ' ', getBalance());

287
เริ่มตนการเขียนโปรแกรมดวย Java

}
finally {
//unlock this account

intro. to Java (FEU.faa)


account.unlock();
}
}

เราปรับปรุงการนําเงินเขาสูบัญชีดวยการปองกันไมให thread ตัวอื่นเขามาวุน


 วายดวยการใช
class ReentrantLock เปนตัวกําหนด ซึ่งเราตองประกาศ ดังนี้

private final Lock account = new ReentrantLock();

เมื่อประกาศแลวการ lock ก็ทําไดดวยการเรียกใช method lock() ดังที่เห็นใน method


deposit()

Method deposit() เริ่มการทํางานดวยการใช Lock เพื่อไมให thread ตัวอื่นสามารถเขามา


ทํางานในเวลาเดียวกันนี้ได ประโยค account.lock() เปนการบอกถึงความพยายามของ thread
ที่จะใหไดการควบคุมการใชบัญชีที่ใชรวมกันอยู หลังจากที่ได Lock มาแลวเราก็นําเงินเขาสู
บัญชีของเราตอไป ผูอานจะเห็นวา thread ตัวอื่นไมสามารถที่จะเขามาทําอะไรกับบัญชีนี้ได
จนกวา lock ของเราจะถูกเปดออกดวยคําสั่ง unlock() สําหรับการถอนเงินออกจากบัญชีก็
เชนกัน เราก็เขียนไดงาย ๆ เหมือนกับที่เราทํากับ deposit() ดังนี้

public void withdraw(double amount) {


//lock this account
account.lock();

try {
//if balance is too low, do nothing
if(balance < amount)
return;
//reset the balance
balance -= amount;
out.printf("%15c%,15.2f%,15.2f%n", ' ', amount, getBalance());
}
finally {
//unlock this account
account.unlock();
}
}

แต code ที่เห็นนี้ก็ไมสามารถที่จะรับรองถึงความถูกตองของการทํางานรวมกันของ thread


ทั้งหลายได ทัง้ นี้ก็อาจเปนเพราะวา เมื่อ thread เขาสู critical section หรือจุดวิกฤต (ภาพที่
9.5 แสดงจุดวิกฤตของ thread – การปรับปรุงจํานวนเงิน) แลวยังทําอะไรไมไดจนกวา
condition บางอยางจะเอื้ออํานวย เชนในกรณีของการถอนเงิน เราทําไมไดถามีเงินไมพอ
ประโยค

if(balance < amount)


return;

ก็ไมสามารถที่จะการันตีใหเราไดวา จะไมเกิดการถอนเงินออกถามีเงินไมพอในบัญชี (ดูผลลัพธ


กอนหนานี้) ทั้งนี้ก็เนื่องจากวา โอกาสที่ thread จะถูกระงับการทํางานระหวางการเปรียบเทียบ
ดังกลาว กับการถายโอนการทํางานกลับไปยังผูเรียก (return) นั้นมีความเปนไปไดสูง

if(balance < amount)


//Thread อาจถูกระงับการทํางาน ณ เวลานี้ได – กอนการ return
return;

เพราะฉะนั้นเราตองกําหนดใหมีการใช condition object และตัวแปรที่เปน boolean เปนตัว


ชวยในการที่จะไมใหมีการถอนเงินในบัญชีถาจํานวนเงินนั้นมีไมพอ หรือมี thread ตัวอื่นกําลัง
ทํางานอยูกับบัญชีนี้ ดังนี้

public void withdraw(double amount) {


//lock this account
account.lock();

288
บทที่ 9: Thread

try {
//if balance is too low or other
//thread is busy with balance, we wait...

intro. to Java (FEU.faa)


while(balance < amount || !occupied) {
out.printf("waiting for deposit...%n");
funds.await();
}
//reset the balance
balance -= amount;
out.printf("%15c%,15.2f%,15.2f%n",
' ', amount, getBalance());
occupied = false;
//signal threads waiting for the account
funds.signalAll();
}
catch(InterruptedException ie) {
ie.printStackTrace();
}
finally {
//unlock this account
account.unlock();
}
}

ผูอานจะเห็นวาเราจะกําหนดใหมีการรอถาเงินในบัญชีของเรามีไมพอ ดวยการใช while/loop


และ condition object: funds.await() ซึ่งตองประกาศใชดังนี้

private Condition funds = account.newCondition();

ตัว condition object จะตองถูกกําหนดใหเปน condition ของ lock ที่เราใชอยู (account)


ประโยค funds.await() จะทําให thread ที่กําลังทํางานอยู (current thread) ถูก block ซึ่งเปน
การเปดโอกาสให thread ตัวอื่นสามารถทํางานกับบัญชีนี้ได ซึ่งอาจเปน thread ที่นําเงินเขาสู
บัญชีของเรา (นี่เปนความหวัง แตก็มีสิทธเปนไปไดที่จะเปน thread สําหรับการถอนเงินก็ได)

หลังจากที่การรอคอยของเราสิ้นสุดลง เราก็ถอนเงินออกจากบัญชี (balance -= amount)


พรอมทั้งสงสัญญาณไปยัง thread ตัวอื่นที่อาจรออยูดวยการเรียกใช

funds.signalAll();

[และเราก็ตองเรียกใชคําสั่งเดียวกันหลังจากที่เรานําเงินเขาสูบัญชีดวยเหมือนกัน เพราะฉะนั้น
deposit() ก็จะกลายเปน]

public void deposit(double amount) {


//lock this account
account.lock();

try {
//if other thread is busy with balance, we wait...
while(occupied) {
out.printf("waiting for withdrawal...%n");
funds.await();
}
balance += amount;
out.printf("%,15.2f%15c%,15.2f%n", amount, ' ', getBalance());
occupied = true;
//signal threads waiting for the account
funds.signalAll();
}
catch(InterruptedException ie) {
ie.printStackTrace();
}
finally {
//unlock this account
account.unlock();
}
}

289
เริ่มตนการเขียนโปรแกรมดวย Java

แผนภาพที่เห็นดานลางนีแ ้ สดงถึงการที่ทั้ง deposit thread และ withdraw thread ทํางานกับ


บัญชีที่ใชรวมกันอยูเมื่อตัวใดตัวหนึ่งทํางานเสร็จก็จะสงสัญญาณไปใหอีกตัวหนึ่งที่รออยูใน

intro. to Java (FEU.faa)


waiting queue

Deposit Thread Withdraw Thread

account.lock(); account.lock();
while(occupied) { while(balance < amount ||
… !occupied) {
funds.await(); …
} funds.await();
}
CRITICAL
UPDATE BALANCE UPDATE BALANCE
SECTION
occupied = true; occupied = false;

funds.signalAll(); funds.signalAll();

account.unlock(); account.unlock();

ภาพที่ 9.5 Thread สําหรับการฝากและถอนเงินออกจากบัญชี

Code ของ Deposit.java และ Withdraw.java ก็เหมือนเดิม เราเพียงแตเปลี่ยนการเรียกใช


ภายใน ดวยการเรียกใช SyncAccount แทน UnSyncAccount

สวนสําคัญอีกสวนหนึ่งที่จําเปนตองมีก็คือการเรียกใช thread ทั้งหลายซึ่งเราไดเขียนไวใน


โปรแกรม SyncAccountTest.java ดังนี้

1: /**
2: Synchronization on a bank account
3: */
4:
5: import java.util.concurrent.Executors;
6: import java.util.concurrent.ExecutorService;
7: import static java.lang.System.out;
8:
9: class SyncAccountTest {
10: public static void main(String[] args) {
11: //create pool of threads
12: ExecutorService app = Executors.newFixedThreadPool(4);
13:
14: //print header
15: out.printf("%15s%15s%15s%n", "Deposit", "Withdrawal", "Balance");
16:
17: //set up shared account with initial balance
18: SyncAccount sharedAccount = new SyncAccount(0);
19:
20: Deposit p1 = new Deposit(sharedAccount, 100.0);
21: Withdraw p2 = new Withdraw(sharedAccount, 1000.0);
22: Withdraw p3 = new Withdraw(sharedAccount, 100.0);
23: Deposit p4 = new Deposit(sharedAccount, 1000.0);
24:
25: //perform deposit and withdrawal simultaneously
26: try {
27: app.execute(p1);
28: app.execute(p2);
29: app.execute(p3);
30: app.execute(p4);
31: }
32: catch(Exception ex) {
33: ex.printStackTrace();

290
บทที่ 9: Thread

34: }
35: //stop synchronization
36: app.shutdown();

intro. to Java (FEU.faa)


37: }
38: }

เราไดเปลี่ยน code ในโปรแกรมตัวนี้มากพอสมควร เรายกเลิกการใช Thread โดยตรงเหมือนที่


เราไดทํามากอนหนานี้ในโปรแกรม UnSycAccountTest.java และหันมาใช class
ExecuterService แทนในการจัดการกับ thread กระบวนการก็ไมยาก โดยกอนอื่นเราก็สราง
thread pool จํานวนสี่ตัวดวยการเรียก

ExecutorService app = Executors.newFixedThreadPool(4);

หลังจากที่เราสรางบัญชีที่ thread เหลานี้ใชรวมกัน (SyncAccount sharedAccount = new


SyncAccount(0)) เราก็สราง thread 4 ตัวคือ p1, p2, p3, และ p4 ขึ้นมาทําการฝากและถอน
เงินออกจากบัญชีโดยกําหนดให p1 และ p4 เปนผูฝากสวน p2 และ p3 เปนผูถอน

เราฝากและถอนดวยการเรียก app.execute() กับ thread ทั้งสี่ตัวดังที่แสดงใหดูในบรรทัดที่ 27


– 30 และเราจะเรียกใช app.shutdown() หลังจากที่ thread ทั้งสี่ทํางานเสร็จแลว จากการ
ทดลอง run โปรแกรมดู ผลลัพธที่เราไดคือ

Deposit Withdrawal Balance


0.00
100.00 100.00
waiting for deposit...
100.00 0.00
waiting for deposit...
1,000.00 1,000.00
1,000.00 0.00
waiting for deposit...
waiting for deposit...
100.00 100.00
100.00 0.00
waiting for deposit...
100.00 100.00
waiting for deposit...
waiting for withdrawal...
100.00 0.00
waiting for deposit...
1,000.00 1,000.00
1,000.00 0.00
waiting for deposit...
100.00 100.00
waiting for deposit...
waiting for withdrawal...
waiting for withdrawal...
100.00 0.00
waiting for deposit...
1,000.00 1,000.00
waiting for withdrawal...
1,000.00 0.00
100.00 100.00
waiting for deposit...
waiting for withdrawal...
100.00 0.00
waiting for deposit...
1,000.00 1,000.00
1,000.00 0.00
1,000.00 1,000.00
1,000.00 0.00

ผูอานควรสังเกตถึงผลลัพธที่ไดวาถูกตองตามขั้นตอนหรือไม ซึ่งสามารถตรวจสอบไดดวยการ
run หลาย ๆ ครั้ง แนนอนวาผลลัพธที่ไดจะไมเหมือนกัน แตสิ่งที่เหมือนกันคือ balance จะตอง
เปนศูนยเสมอเมื่อการทํางานสิ้นสุดลง

โครงสรางหลัก ๆ ในการเรียกใช lock และ condition object มีดังนี้คือ

291
เริ่มตนการเขียนโปรแกรมดวย Java

someLock.lock();
try {
while(!(ok to proceed))

intro. to Java (FEU.faa)


condition.await();

CRITICAL SECTION

condition.signalAll();
}
catch() {
…//some exception might be thrown here
}
finally {
//must unlock the lock even though exception is thrown
someLock.unlock();
}

เราอาจเรียกใช signal() แทน signalAll() ก็ได แตถาเราตองการให thread ตัวใด ๆ มีสิทธใน


การใชทรัพยากร (หลังจากที่ถูกสงคืน) เทา ๆ กันเราก็ควรเรียกใช signalAll() มากกวา signal()
และกอนที่เราจะไปดูเรื่องของการประสานเวลาดวยการใช synchronized เราจะมาสรุปการใช
lock ดังนี้

1. lock จะปกปอง code จากการ execute ของ thread ตัวอื่น


2. lock จะเปนตัวบริหารจัดการ การเขาหา code ของ thread ที่มีอยูในระบบ
3. เราสามารถใช lock รวมกับ condition object อื่น ๆ ได

9.6.2 การใช synchronize ในการประสานเวลา

การใช synchronized (keyword) เปนวิธีการประสานเวลาแบบเดิมที่ Java ใชกอนที่จะมีการใช


lock เหมือนกับที่เราไดแสดงใหดูในตอนกอนหนานี้ ถา object ที่ java สรางขึ้นจะมี lock ของ
ตัวเองอยูถา method ใดมีการเรียกใช synchronized code ของ method นั้นจะไดรับการ
ปองกันจาก lock นั้น ภาพที่ 9.6 แสดงการเปรียบเทียบการใช synchronized และการใช lock
ในการประสานเวลา

การใช synchronized การใช lock

public synchronized void method() { public void method() {


… implicit.lock();
Code ภายใน method try {
… …
} Code ภายใน method

หรือ }
finally {
public void method() { implicit.unlock();
synchronized(lock) { }
… }
Code …
}
}

ภาพที่ 9.6 code แสดงการประสานเวลาดวยการใช synchronized และการใช lock

โปรแกรมตัวอยางตอไปจะเปนตัวอยางการประสานเวลาดวยการใช synchronized เปนตัวชวย


โดยเราจะปรับปรุงการทํางานของโปรแกรม SyncAccount.java ดังนี้

1: /**
2: Account using keyword synchronized
3: BankAccount is shared by Account1 and Account2
4: */
5:

292
บทที่ 9: Thread

6: import static java.lang.System.out;


7:
8: class BankAccount {

intro. to Java (FEU.faa)


9: private double balance;
10: private boolean occupied = false;
11: private Object THIS_ACCOUNT; //critical section
12:
13: //set up initial balance and critical section
14: BankAccount(double balance) {
15: this.balance = balance;
16: THIS_ACCOUNT = new Object();
17: out.printf("%15c%15c%,15.2f%n", ' ', ' ', balance);
18: }
19:
20: //deposit amount into a shared account
21: public void deposit(double amount) {
22: //synchronize shared account
23: synchronized(THIS_ACCOUNT) {
24: try {
25: //wait for other thread
26: while(occupied) {
27: out.println("Wait for withdrawal ...");
28: THIS_ACCOUNT.wait();
29: }
30: balance += amount; //update balance
31: occupied = true;
32: out.printf("%,15.2f%15c%,15.2f%n", amount,
33: ' ', balance);
34: THIS_ACCOUNT.notifyAll(); //notify threads
35: }
36: catch(InterruptedException e) {
37: e.printStackTrace();
38: }
39: }
40: }
41:
42: //withdraw amount from a shared account
43: public void withdraw(double amount) {
44: //synchronize shared account
45: synchronized(THIS_ACCOUNT) {
46: try {
47: //wait for other thread
48: while(balance < amount || !occupied) {
49: out.println("Wait for deposit ...");
50: THIS_ACCOUNT.wait();
51: }
52: occupied = false;
53: balance -= amount; //update balance
54: out.printf("%15c%,15.2f%,15.2f%n",
55: ' ', amount, balance);
56: THIS_ACCOUNT.notifyAll(); //notify other thread
57: }
58: catch(InterruptedException e) {
59: e.printStackTrace();
60: }
61: }
62: }
63: }

ภายในโปรแกรม BankAccount เราไดกําหนดใหมีการใชตัวแปร THIS_ACCOUNT เปนตัว


ประสานเวลา โดยสรางมาจาก Object (บรรทัดที่ 11) และเราจะทําการประสานเวลาใน method
deposit() และ withdraw() ซึง่ ในการทํางานของ deposit() และ withdraw() นั้นเราจะ
กําหนดใหมีการรอเหมือนกับที่เราทําในตัวอยางกอนหนานี้ สวนสําคัญของการประสานเวลาก็คือ

synchronized(. . .) {
//code … ที่มีการเรียกใช wait() และ notify() หรือ notifyAll()
}

wait() ก็คือการรอให thread ตัวอื่นเสร็จจากงานที่ทําอยู


notify() และ notifyAll() เปนการบอกให thread ตัวอื่นรูวา critical section นั้นเปดโอกาสให
เขาไปทํางานไดแลว

293
เริ่มตนการเขียนโปรแกรมดวย Java

เชน code ที่อยูใน synchronized block เชนการประสานเวลาของการนําเงินเขาบัญชีนี้

intro. to Java (FEU.faa)


synchronized(THIS_ACCOUNT) {
try {
//wait for other thread
while(occupied) {
out.println("Wait for withdrawal ...");
THIS_ACCOUNT.wait();
}
balance += amount; //update balance
occupied = true;
out.printf("%,15.2f%15c%,15.2f%n", amount, ' ', balance);
THIS_ACCOUNT.notifyAll(); //notify other thread
}
catch(InterruptedException e) {
e.printStackTrace();
}

ในสวนของ Account1.java และ Account2.java ก็เปนการนําเงินเขา และการถอนเงินออกจาก


บัญชีตามลําดับ โดยมีกระบวนการทํางานดังนี้

1: /**
2: Program to access a shared account
3: */
4:
5: import static java.lang.System.out;
6: import java.util.Random;
7:
8: class Account1 extends Thread {
9: //shared saving account
10: private BankAccount saving;
11: //amount to deposit
12: private double amount;
13: //waiting time
14: private Random sleepTime = new Random();
15:
16: //set the shared account
17: public Account1(BankAccount saving, double amount) {
18: this.saving = saving;
19: this.amount = amount;
20: }
21:
22: //deposit random amounts 5 times
23: public void run() {
24: try {
25: for(int i = 0; i < 5; i++) {
26: saving.deposit(amount);
27: Thread.sleep(sleepTime.nextInt(2000));
28: }
29: }
30: catch(InterruptedException ie) {
31: ie.printStackTrace();
32: }
33: }
34: }

1: /**
2: Program to access a shared account
3: */
4:
5: import static java.lang.System.out;
6: import java.util.Random;
7:
8: class Account2 extends Thread {
9: //shared account
10: private BankAccount saving;
11: //amount to withdraw
12: private double amount;
13: //busy time
14: private Random sleepTime = new Random();

294
บทที่ 9: Thread

15:
16: //set a shared account
17: public Account2(BankAccount saving, double amount) {

intro. to Java (FEU.faa)


18: this.saving = saving;
19: this.amount = amount;
20: }
21:
22: //withdraw random amounts five times
23: public void run() {
24: try {
25: for(int i = 0; i < 5; i++) {
26: saving.withdraw(amount);
27: Thread.sleep(sleepTime.nextInt(2000));
28: }
29: }
30: catch(InterruptedException ie) {
31: ie.printStackTrace();
32: }
33: }
34: }

การทํางานของทั้งสองโปรแกรมก็คลาย ๆ กับ Deposit และ Withdraw เราเพียงแตทําการ


extends Thread ใหกับทั้งสอง class แทนการ implement Runnable สวนการทดสอบ
กระบวนการของทั้งสองโปรแกรมเราทําในโปรแกรม BankAccountTest.java ซึ่งมีขั้นตอนดังนี้

1: /**
2: Synchronization on a bank account
3: */
4:
5: import static java.lang.System.out;
6:
7: class BankAccountTest {
8: public static void main(String[] args) {
9: out.printf("%15s%15s%15s%n", "Deposit", "Withdrawal", "Balance");
10: //set up a shared account
11: BankAccount sharedAccount = new BankAccount(0);
12:
13: //create 2 accounts
14: Account1 acc1 = new Account1(sharedAccount, 100.0);
15: Account2 acc2 = new Account2(sharedAccount, 100.0);
16: Account1 acc3 = new Account1(sharedAccount, 100.0);
17: Account2 acc4 = new Account2(sharedAccount, 100.0);
18:
19: //start running the two processes
20: acc1.start();
21: acc2.start();
22: acc3.start();
23: acc4.start();
24: }
25: }

ผลลัพธของการ run ของโปรแกรมนี้ก็คลาย ๆ กับที่เราได run กอนหนานี้ คือ

Deposit Withdrawal Balance


0.00
100.00 100.00
100.00 0.00
100.00 100.00
100.00 0.00
100.00 100.00
100.00 0.00
Wait for deposit process ...
100.00 100.00
100.00 0.00
Wait for deposit process ...
100.00 100.00
100.00 0.00
100.00 100.00
100.00 0.00
100.00 100.00
100.00 0.00

295
เริ่มตนการเขียนโปรแกรมดวย Java

Wait for deposit process ...


100.00 100.00
100.00 0.00

intro. to Java (FEU.faa)


Wait for deposit process ...
100.00 100.00
100.00 0.00
Wait for deposit process ...
100.00 100.00
100.00 0.00

เราใชรูปแบบของ synchronized แบบที่ไมตอ


 งใช object (เชนที่เราใช object:
THIS_ACCOUNT ในโปรแกรมตัวอยาง) โดยตรงก็ได เราเพียงแคเปลีย ่ น code ของเราใหเปน
(แสดงเฉพาะ deposit() ผูอานควรพิจารณาวา withdraw() นาจะเปลี่ยนอยางไร)

public synchronized void deposit(double amount) {


try {
//wait for other thread
while(occupied) {
out.println("Wait for withdrawal process ...");
wait();
}
balance += amount;
occupied = true;
out.printf("%,15.2f%15c%,15.2f%n", amount,
' ', balance);
notifyAll();
}
catch(InterruptedException e) {
e.printStackTrace();
}
}

9.6.3 Deadlock

ลองดูตัวอยางที่เราไดจากการ run โปรแกรมการฝากและถอนเงินนี้

Deposit Withdrawal Balance


0.00
100.00 100.00
waiting for deposit...
100.00 0.00
waiting for deposit...
waiting for withdrawal...
waiting for withdrawal...
waiting for deposit...

เหตุการณนี้เกิดขึ้นเมื่อเราเปลี่ยน code ของการถอนเงินใหเปน

account.lock();
try {
while(balance < amount) {
out.printf("waiting for deposit...%n");
funds.await();
}
//reset the balance
balance -= amount;
out.printf("%15c%,15.2f%,15.2f%n", ' ', amount, getBalance());
//signal threads waiting for the account
funds.signalAll();
}
catch(InterruptedException ie) {
ie.printStackTrace();
}
finally {
//unlock this account
account.unlock();
}

296
บทที่ 9: Thread

ผูอานจะสังเกตเห็นวาเราไดตัดเอา code ของการเปรียบเทียบ boolean ที่วา !occupied ออก


จาก while/loop และจากการกําหนดหลังจากนั้น ซึ่งทําให thread ทั้งสองกลุม (deposit และ

intro. to Java (FEU.faa)


withdraw) ไมสามารถที่จะทํางานในสวนของตนตอไปได เกิดการรอคอยซึ่งกันและกัน เราเรียก
สิ่งที่เกิดขึ้นนี้วา deadlock กระบวนการในการแกปญหาเรื่อง deadlock เปนหัวขอที่สําคัญ
อันหนึ่งในเรื่องของระบบปฏิบัติการ การ synchronize เปนกระบวนการที่สามารถนํามาใช
แกปญหาของ deadlock ได

สรุป

เราไดพูดถึงการสราง thread ในทั้งสองรูปแบบตลอดจนถึงการใช thread ในงานทางดาน


การเงิน ซึ่งโดยรวมแลวเราไดพูดถึง

9 การสราง thread จาก Runnable และจาก Thread


9 การประสานเวลาของ thread
9 ตัวอยางการประสานเวลาของ thread

แบบฝกหัด

1. จงเขียนโปรแกรมทดสอบการสราง thread จํานวนสิบตัว

2. จงหาขอมูลของการประสานเวลาระหวาง producer และ consumer ในการผลิตและบริโภค


ขอมูลชนิดหนึ่ง (อะไรก็ได) ซึ่งสามารถหาไดจากหนังสือที่เกี่ยวกับเรื่องระบบปฏิบัติการ
(Operating System) เมื่อไดแลวใหเขียนโปรแกรมแกปญหาเรื่อง deadlock ระหวาง
producer 1 คนและ consumer 1 คน

3. จากโจทยในขอสอง จงเขียนโปรแกรมแกปญหาถาจํานวนของ producer และ consumer


มีมากกวาหนึ่งคน

4. การแกปญหาในเรื่องของการฝากและถอนเงินของเราที่แสดงไวในบทนี้ ยังไมสมบรูณ
ทีเดียวนัก กลาวคือ การถอนเงินจะทําไมไดถามีเงินไมครบในบัญชี โดยเรากําหนดให
โปรแกรมยุติการทํางานทันที ถาเหตุการณดงั กลาวเกิดขึ้น จงปรับปรุงโปรแกรมดังกลาวให
สลับการถอนเงินมาเปนการฝากเงิน ถามีเงินในบัญชีนอยกวาที่ตองถอน

5. สมมติวาเราเปลี่ยนกระบวนการภายในโปรแกรม BankAccountTest.java ใหเปน

class BankAccountTest1 {
public static void main(String[] args) {
out.printf("%15s%15s%15s%n", "Deposit",
"Withdrawal", "Balance");
//set up a shared account
BankAccount sharedAccount = new BankAccount(10000);

//create accounts
Account1 []acc1 = new Account1[3];
Account2 []acc2 = new Account2[3];
for(int i = 0; i < 3; i++) {
acc1[i] = new Account1(sharedAccount);
acc1[i].setPriority((int)(Math.random()*10 + 1));

acc2[i] = new Account2(sharedAccount);


acc2[i].setPriority((int)(Math.random()*10 + 1));

acc1[i].start();
acc2[i].start();
}
}
}

จงหาผลลัพธที่ไดจากการ run พรอมทั้งอธิบายถึงการทํางานที่เกิดขึ้น (ทําไมจึงได


ผลลัพธดังกลาว) จงอธิบายผลลัพธของการ run ถาเราเอาการกําหนด priority ออกทั้ง
สองที่

297
เริ่มตนการเขียนโปรแกรมดวย Java

6. จงปรับปรุง หรือออกแบบโปรแกรมการฝากและถอนเงินใหม โดยกําหนดใหมีการถายโอน


เงิน (transfer) ระหวางบัญชีตาง ๆ ที่มีอยูในธนาคาร โดยกําหนดใหมีจํานวนบัญชีที่เปดไว

intro. to Java (FEU.faa)


ไมนอยกวา 5 บัญชี ใหทําการทดสอบการโอนเงินระหวางบัญชีดวยการสุมจํานวนการโอน
และสุมจํานวนเงินที่ตองการโอนระหวางบัญชี โดยโปรแกรมจะตองแสดงจํานวนเงินที่มีการ
โอน พรอมทั้งยอดเงินทั้งหมดในเวลาตาง ๆ ของการโอนดังกลาว

7. Java มี method ชื่อ join() ที่ใชทํางานเกี่ยวกับ thread จงอธิบายถึงการทํางานของ join()


พรอมทั้งยกตัวอยางประกอบ

8. Java มี method ชื่อ yield() ที่ใชทํางานเกี่ยวกับ thread จงอธิบายถึงการทํางานของ


yield() พรอมทั้งยกตัวอยางประกอบ

9. Java มี method ชื่อ interrupt() ที่ใชทํางานเกี่ยวกับ thread จงอธิบายถึงการทํางานของ


interrupt() พรอมทั้งยกตัวอยางประกอบ

298

You might also like