เมื่อเราเรียกโปรแกรมมาทำงานจะเกิดสิ่งที่เรียกว่าโปรเซส (process) ซึ่งโปรเซสคือหน่วยของการดำเนินการ (execution) โดยโปรเซสจะมีหน่วยความจำเป็นของตนเองเรียกว่าฮี๊บ (heap) ดังนั้นถ้าเราเรียกใช้โปรแกรม 2 โปรแกรมจะเกิดโปรเซสและฮี๊บของใครของมัน โดยแต่ละโปรแกรมจะไม่สามารถใช้งานฮี๊บของอีกโปรแกรม และนอกจากโปรเซสที่เกิดจากการใช้งานโปรแกรมที่เราเขียนขึ้นมาแล้วยังมีโปรเซสที่เกิดจากการทำงานของ java virtual machine
ภายในโปรเซสจะมีหน่วยการดำเนินการที่แยกย่อยลงไปเรียกว่าเธรด (thread) ซึ่งแต่ละเธรดจะมีหน่วยความจำของตนเองเรียกว่าเธรดแสตก (thread stack) เธรดที่เกิดจากโปรเซสเดียวกันจะใช้ไฟล์และหน่วยความจำร่วมกัน ดังนั้นในบางกรณีอาจจะทำให้เกิดปัญหาได้
โดยปรกติโปรแกรมหรือโปรเซสของเราจะมีเธรดเดียว คือ main thread (หรือ fx application thread ในกรณีที่เราสร้างโปรแกรมแบบ GUI) แต่เราสามารถสร้างเธรดเพิ่มขึ้นมาได้ สาเหตุที่เราต้องสร้างเธรดเพิ่มเพราะในเธรดเดียวกันโปรแกรมจะทำงานเรียงลำดับกันไป เมื่อทำงานใดงานหนึ่งเสร็จแล้วจึงจะสามารถทำงานถัดไปได้ หากงานที่กำลังทำอยู่ต้องใช้เวลานาน เช่น อ่านข้อมูลจำนวนมากจากฐานข้อมูล ตราบเท่าที่ยังอ่านข้อมูลไม่เสร็จ โปรแกรมส่วนถัดไปก็จะไม่ถูกทำงาน ซึ่งในมุมมองของผู้ใช้งานจะเห็นว่าโปรแกรมค้างและไม่สามารถใช้งานอื่นได้ ดังนั้นเราจึงต้องสร้างเธรดขึ้นมาสำหรับงานที่ต้องใช้เวลานาน และกำหนดให้ทำงานเป็นเบื้องหลัง ทำให้เธรดหลักสามารถทำงานอื่นต่อไปได้ นั่นคือเราสามารถเริ่มงานใหม่ได้โดยไม่จำเป็นต้องรอให้งานก่อนหน้าเสร็จเสียก่อนซึ่งก็คือความหมายของความสามารถในการทำ concrrency ของภาษาจาวา ในการใช้งานอาจจะดูเหมือนว่าเรามีเธรด 2 เธรดทำงานพร้อมกันอยู่ แต่อันที่จริงเธรดทั้ง 2 เธรดไม่ได้ทำงานพร้อมกันแต่สลับกันทำงานไปมาจนกว่าจะเสร็จงานของตนเอง
การสร้างเธรด
เราสามารถสร้างเธรดได้ 2 วิธีคือสร้างเป็นคลาสที่สืบทอด ( subclass) จากคลาส Thread แล้วโอเวอร์ไรด์เมธอด run() เพื่อให้ทำงานตามที่เราต้องการ หรือหากเราต้องการใช้แค่เมธอด run() เท่านั้นเราสามารถสร้างเธรดได้โดยการ implement คลาสอินเตอร์เฟส Runnable ให้กับคลาสใดๆก็ได้แล้วโอเวอร์ไรด์เมธอด run() เพื่อให้ทำงานตามที่เราต้องการ แต่ถ้าเราต้องการใช้เมธอดอื่นในคลาส Thread ด้วยก็ต้องสร้างคลาสที่สืบทอดจากคลาส Thread
การสร้างเธรดในแบบที่เป็นคลาสที่สืบทอด ( subclass) จากคลาส Thread ทำได้ 2 แบบคือใช้คลาสที่ระบุชื่อ (named class – คลาสพื้นฐานตามที่เราใช้งานทั่วไป) หรือใช้งานในแบบคลาสที่ไม่ระบุชื่อ (anonymous class)
การสร้างเธรดจากคลาสที่ระบุชื่อ วิธีการคือสร้างคลาสที่สืบทอดจากคลาส Thread ด้วยการใช้คีย์เวิร์ด extends และโอเวอร์ไรด์เมธอด run() ด้วยโปรแกรมที่เราต้องการให้ทำงานในเธรดที่สร้างขึ้นมา ในการใช้งานเราจะสร้างออบเจกต์ของคลาสดังกล่าวขึ้นมาและสั่งทำงานด้วยเมธอด start() ซึ่งจะไปบอก java virtual machine ให้เรียกเมธอด run ของออบเจกต์ที่เราสร้างขึันมาอีกทีหนึ่ง ซึ่งจะเป็นการสร้างเธรดขึ้นมาแล้วทำงานตามที่เรากำหนดในเธรดนั้นๆ หากเราต้องการเรียกใช้งานเธรดนี้อีกครั้งเราจะต้องสร้างออบเจกต์ขึ้นมาใหม่ ไม่สามารถเรียกแค่เมธอด start() เพราะออบเจกต์ที่เป็นเธรดเมื่อจบงานแล้วก็จะหายไป ตัวอย่างด้านล่างเป็นการสร้างเธรดจากคลาสที่ระบุชื่อ
จากตัวอย่างเราสร้างคลาส SubClass ขึ้นมาซึ่งสืบทอดมาจากคลาส Thread จากนั้นเราจึงโอเวอร์ไรด์เมธอด run() ด้วยโปรแกรมที่เราต้องการให้ทำงานในเธรดที่สร้างขึ้นมา ดังบรรทัดที่ 4 – 7
เมื่อเราสร้างคลาสขึ้นมาแล้วในบรรทัดที่ 9 เป็นการสร้างออบเจกต์จากคลาส SubClass และเรียกใช้งานโดยใช้เมธอด start() ในบรรทัดที่ 10
สำหรับการสร้างเธรดโดยใช้คลาสที่ไม่ระบุชื่อ (anonymous class) เรามักจะใช้เมื่อต้องการทำงานเพียงครั้งเดียว ตัวอย่างด้านล่างในบรรทัดที่ 12 – 16 เป็นการสร้างเธรดโดยใช้คลาสที่ไม่ระบุชื่อและโอเวอร์ไรด์เมธอด run() เพื่อทำงานตามที่เราต้องการ
ผลลัพธ์ที่ได้คือพิมพ์ข้อความตามที่กำหนด ซึ่งลำดับการพิมพ์ข้อความจะขึ้นอยู่กับการจัดลำดับการทำงานของเธรดของเครื่องคอมพิวเตอร์ เราไม่สามารถคาดกำหนดได้ว่าลำดับการทำงานของเธรดจะเป็นอย่างไร
สำหรับการสร้างเธรดโดยใช้คลาสอินเตอร์เฟซ Runnable สามารถทำได้ทั้งแบบการใช้คลาสที่ระบุชื่อหรือคลาสที่ไม่ระบุชื่อ เช่นเดียวกัน ดังตัวอย่างด้านล่าง สำหรับการใช้คลาสที่ระบุชื่อ เราสร้างคลาสที่ใช้คลาสอินเตอร์เฟซ Runnable และโอเวอร์ไรด์เมธอด run()
ในการสร้างเธรดเราสร้างออบเจกต์ของคลาส Thread โดยมีออบเจกต์ที่เป็น Runnable (ในที่นี้ทำโดยสร้างออบเจกต์ SubClass ขึ้นมา) เป็นพารามิเตอร์ ดังบรรทัดที่ 3 และสั่งสร้างเธรดโดยใช้มธอด start() ดังบรรทัดที่ 4
สำหรับการใช้คลาสที่ไม่ระบุชื่อ เราจะโอเวอร์ไรด์เมธอด run() ในคำสั่งเลยดังบรรทัดที่ 8 – 9
การกำหนดให้เธรดหยุดทำงานชั่วคราว
เราอาจจะต้องการให้เธรดหยุดทำงานชั่วคราวเพื่อให้เธรดอื่นได้ทำงาน เช่น กำหนดให้เธรดที่แสดงผลข้อมูลหยุด 2 นาทีเพื่อรอให้เธรดอื่นอ่านข้อมูลจากฐานข้อมูลเข้ามาก่อน เป็นต้น เราสามารถกำหนดให้เธรดหยุดทำงานช่วงระยะเวลาหนึ่งแล้วกลับมาทำงานต่อได้โดยใช้เมธอด sleep() ซึ่งมีพารามิเตอร์คือช่วงเวลาที่เราต้องการให้เธรดหยุดทำงานในหน่วยมิลลิวินาที จากตัวอย่างด้านล่างเป็นการกำหนดให้เธรดหยุดทำงาน 3 วินาที (1 วินาที = 1000 มิลลิวินาที) จากตัวอย่าง เราเรียกใช้เมธอด sleep() ในบรรทัดที่ 17 ซึ่งต้องกำหนดใน try-catch เพื่อจัดการเรื่องข้อผิดพลาด และหลังจากเธรดกลับมาทำงานใหม่ให้พิมพ์ข้อความตามบรรทัดที่ 21 ซึ่งในการเรียกใช้งานจะเห็นว่าต้องรอ 3 วินาทีข้อความจึงจะแสดง
การยุติการทำงานของเธรดโดยเธรดอื่น
เราสามารถกำหนดให้เธรดหยุดทำงานโดยการร้องขอจากเธรดอื่นได้ เช่น เธรดที่ทำหน้าที่ส่งออกข้อมูลเมื่อไม่มีข้อมูลที่จะส่งออกแล้ว จึงบอกให้เธรดที่คอยรับข้อมูลหยุดทำงาน เป็นต้น การกำหนดให้เธรดหยุดทำงานโดยการร้องขอจากเธรดอื่นนั้นเป็นการใช้ประโยชน์จาก InterruptedException โดยเรียกใช้เมธอด interrupt() ของเธรดที่ต้องการให้หยุดทำงาน จากตัวอย่างเราเพิ่มเมธอด return() เพื่อให้โปรแกรมหยุดทำงานในบรรทัดที่ 20 และเรียกใช้เมธอด interrupt() ในบรรทัดที่ 26 ซึ่งเป็นการเรียกจาก main thread ผลลัพธ์คือ try-catch ของเธรดที่ต้องการให้หยุดทำงานจะดักจับข้อผิดพลาดได้และพิมพ์ข้อความตามที่กำหนดและหยุดทำงานด้วยเมธอด return()
การกำหนดให้เธรดรอให้เธรดอื่นทำงานให้เสร็จ
เราสามารถกำหนดให้เธรดหยุดพักการทำงานเพื่อรอให้เธรดอื่นทำงานให้เสร็จก่อนแล้วจึงกลับมาทำงานต่อโดยใช้เมธอด join() โดยเธรดที่จะหยุดพักการทำงานจะเรียกใช้เมธอด join() ของเธรดที่จะปล่อยให้ทำงานไปก่อน
จากตัวอย่างด้านล่างเราจะสร้างเธรดขึ้นมา 2 เธรดโดยเธรดที่อ่านข้อมูลจะต้องรอให้เธรดที่ปรับปรุงข้อมูลทำงานเสร็จก่อน คลาสแบบ runnable ที่เป็นต้นแบบของทั้ง 2 เธรดคือคลาส ReadDataClass และคลาส FetchDataClass
ในคลาส FetchDataClass เรากำหนดให้พิมพ์สถานะว่ากำลังดำเนินการและดำเนินการเสร็จสิ้น โดยมีการหน่วงเวลาไว้ 3 วินาที
ในคลาส Main บรรทัดที่ 9-10 เราสร้างออบเจกต์เธรดจากคลาส FetchDataClass และในบรรทัดที่ 12-25 เราใช้คลาสที่ไม่ระบุชื่อ (anonymous class) เพื่อสร้างออบเจกต์เธรดจากคลาส ReadDataClass และโอเวอร์ไรด์เมธอด run() เพื่อพิมพ์สถานะว่ากำลังรอการปรับปรุงข้อมูลและเริ่มอ่านข้อมูล
เนื่องจากเธรดที่อ่านข้อมูลต้องรอให้เธรดที่ปรับปรุงข้อมูลทำงานเสร็จก่อน ดังนั้นการทำงานของเธรดที่อ่านข้อมูลจึงเรียกใช้เมธอด join() ของเธรดที่ปรับปรุงข้อมูลดังตัวอย่างในบรรทัดที่ 17
เมื่อเราใช้งานโปรแกรม เธรด main จะถูกทำงานก่อนซึ่งพิมพ์ “This is main thread.” จากนั้นเธรดปรับปรุงข้อมูลจะถูกทำงานซึ่งพิมพ์ “I’m fetching data…” และหยุดรอ 3 วินาทีตามที่กำหนด และเธรดที่อ่านข้อมูลจะถูกทำงานซึ่งจะพิมพ์ “I’m waiting other thread to fetch data.” แต่เนื่องจากในโปรแกรมของเธรดที่อ่านข้อมูลข้อมูลเรียกใช้ fecthdata.join() จึงหยุดรอให้เธรดปรับปรุงข้อมูลทำงานเสร็จก่อน ซึ่งหลังจากครบการหยุดรอ 3 วินาทีเธรดปรับปรุงข้อมูลจะพิมพ์ “Fetch data completed.” และจบการทำงานหลังจากนั้นเธรดที่อ่านข้อมูลจึงจะทำงานต่อโดยพิมพ์ “I’m starting reading data.”
ในบางกรณีเราอาจจะไม่ต้องการให้รอเธรดอื่นทำงานให้จบก่อน เราสามารถกำหนดเวลาที่จะให้รอได้เองโดยการกำหนดเวลาในเมธอด join เช่น fecthdata.join(3000) เป็นต้น
การรบกวนกันของเธรด
เมื่อเราเรียกใช้งานโปรแกรมจะเกิดโปรเซสและหน่วยความจำที่ถูกจองไว้สำหรับแต่ละโปรเซสเรียกว่าฮี๊บ ในโปรเซสจะมีเธรดและหน่วยความจำที่ถูกจองไว้สำหรับแต่ละเธรดเรียกว่าเธรดแสตก และเมื่อมีการสร้างออบเจกต์ขึ้นมาออบเจกต์จะอยู่ในฮี๊บ เธรดแต่ละเธรดสามารถเข้าถึงและใช้งานอะไรที่อยู่ในฮี๊บได้ แต่จะไม่สามารถเข้าถึงและใช้งานอะไรที่อยู่ในเธรดแสตกของเธรดอื่นได้
เมื่อเรามีการใช้งานตัวแปรในเธรด ตัวแปรจะถูกพิจารณาได้เป็น 2 แบบคือตัวแปรที่อยู่ในขอบเขตบล็อกย่อย เช่น ตัวแปรในลูป for หรือ ตัวแปรในบล็อก if เรียกว่า local variable และตัวแปรที่เป็นตัวแปรในระดับออบเจกต์เรียกว่า instance variable หรือ global variable
ตัวแปรแบบ local variable จะเป็นตัวแปรที่เก็บอยู่ในเธรดแสตก ดังนั้นตัวแปรจะไม่ถูกเธรดอื่นเข้ามาใช้งาน แต่ถ้าเป็นตัวแปรในระดับออบเจกต์ตัวแปรจะอยู่ในฮี๊บ ซึ่งเธรดทุกเธรดจะสามารถเข้าถึงและใช้งานตัวแปรดังกล่าวได้ ดังนั้นหากเราต้องการให้เธรดใช้ตัวแปรในระดับออบเจกต์ เราต้องระมัดระวังและนึกอยู่เสมอว่าเธรดจะสลับกันทำงานในแบบที่เราไม่สามารถคาดเดาลำดับการทำงานของเธรดได้ หากการใช้งานตัวแปรเป็นแค่การอ่านค่าจะไม่เกิดปัญหาอะไร แต่ถ้าการใช้งานตัวแปรมีการเปลี่ยนแปลงค่า ตัวแปรจะถูกเปลี่ยนค่าในแบบที่เราไม่สามารถคาดเดาได้เช่นกัน หรือเรียกว่าเป็นการรบกวนกันในการทำงานของเธรด (thread interference หรือ race condition) ทำให้ผลลัพธ์ไม่เป็นไปอย่างที่ต้องการ
สาเหตุที่เราต้องคำนึงถึงเรื่องนี้เพราะโปรแกรมที่เราเขียนขึ้นจะมีขั้นตอนการทำงานที่มากกว่าที่เราเห็น เช่น ในการใช้ลูป for จะมีขั้นตอนการ เพิ่ม/ลดค่าของลูป ขั้นตอนการตรวจสอบค่า ขั้นตอนการทำงานตามคำสั่งในลูป หรือ เมธอดง่ายๆเช่น println() ก็มีขั้นตอนในการแปลงข้อมูลมาเป็น string ขั้นตอนการติดต่อกับ i/o ขั้นตอนการเตรียมข้อมูลที่จะพิมพ์ ขั้นตอนการพิมพ์ข้อความในหน้าต่างคอนโซล ซึ่งขั้นตอนย่อยๆเหล่านี้จะถูกแบ่งออกเป็นเธรดย่อยๆ และหากเรามีหลายเธรดทำงานพร้อมกันเราไม่สามารถกำหนดหรือยืนยันได้ว่าเธรดจะหยุดที่ขั้นตอนใดเพื่อสลับไปทำงานเธรดอื่น
จากตัวอย่างด้านล่างจะแสดงให้เห็นผลลัพธ์การทำงานของเธรด 2 เธรดในการพิมพ์ตัวเลข 1- 10 เราสร้างคลาส RunnableClass เพื่อพิมพ์ตัวเลข 1 – 10 โดยใช้ลูป for และพิมพ์ชื่อเธรดกำกับไว้ด้วยเพื่อให้ทราบว่าเป็นการพิมพ์ตัวเลขของเธรดใด และสร้างคลาส SubClass ซึ่งเป็นคลาสที่สืบทอดมาจากคลาส Thread เพื่อใช้สร้างออบเจกต์ของเธรดโดยจะเรียกใช้การพิมพ์ตัวเลขจากออบเกจต์ RunnableClass และที่คลาส Main เราสร้างออบเจกต์ RunnableClass ขึ้นมาชื่อ run1 และสร้างออบเจกต์เธรดขึ้นมา 2 ออบเจกต์โดยทั้งคู่ใช้ออบเจกต์ run1 ในการพิมพ์ตัวเลข
เมื่อเราทดลองเรียกใช้โปรแกรมหลายๆครั้งจะพบว่าผลลัพธ์ที่ได้จะไม่เหมือนกันขึ้นอยู่กับลำดับและการสลับกันทำงานของเธรด
จากตัวอย่างด้านบนจะเห็นว่าลำดับการพิมพ์ไม่สามารถคาดเดาได้ แต่การทำงานของเธรดยังไม่รบกวนกัน สังเกตุได้จากแต่ละเธรดยังคงพิมพ์ตัวเลข 1- 10 ได้อย่างครบถ้วน เพราะถึงแม้ว่าทั้ง 2 เธรดจะใช้ออบเจกต์เดียวกัน (run1) แต่ตัวแปร i ในลูป for เป็นตัวแปรแบบ local variable ของแต่ละเธรดจะถูกเก็บไว้ที่เธรดแสตคของแต่ละเธรด
ถ้าเราต้องการทดสอบเรื่องการรบกวนกันของเธรด (thread interference) เราจะเปลี่ยนให้ตัวแปร i เป็น instance variable (global variable) ซึ่งจะถูกเก็บไว้ที่ฮี๊บ ดังตัวอย่างด้านล่าง
จากผลลัพธ์ด้านล่างจะแสดงให้เห็นการรบกวนกันของเธรด โดยค่า i ที่พิมพ์ออกมาจะดูสับสนอันเนื่องมาจากการสลับการทำงานของเธรด เช่น รูปทางด้านซ้าย sub2 อ่านค่าที่จะพิมพ์ก่อนคือ 1 แต่ยังไม่ทันได้พิมพ์ก็ถูกสลับการทำงานไป sub1 ซึ่งพิมพ์ค่าและเปลี่ยนค่าเป็น 1 2 3 4 พอ sub2 ได้สิทธิในการทำงานจึงพิมพ์ข้อมูลที่เตรียมไว้คือ 1 ทั้งๆที่ตัวแปรถูกเปลี่ยนไปเป็น 4 แล้ว
การกำหนด synchronization ให้เมธอด
ในกรณีที่เราต้องการให้เธรดใช้ตัวแปรหรือออบเจกต์ร่วมกันและทำงานได้โดยที่เธรดไม่รบกวนกัน เราจะใช้ความสามารถในการ synchronization โดยกำหนดคีย์เวิร์ด synchronized ให้กับเมธอดหรือบล็อกของคำสั่ง การ synchronization จะทำให้เธรดที่จะใช้เมธอดหรือบล็อกของคำสั่งดังกล่าวจะต้องได้สิทธิ (acquire lock) เสียก่อน และเฉพาะเธรดที่ได้สิทธิจึงจะใช้เมธอดหรือบล็อกคำสั่งที่เป็น synchronization ได้ ในระหว่างที่สิทธิถูกถือครองโดยเธรดใดเธรดหนึ่ง เธรดอื่นจะไม่สามารถใช้เมธอดหรือบล็อกของคำสั่งดังกล่าว เมื่อเธรดใช้เมธอดหรือบล็อกคำสั่งที่เป็น synchronization จนจบบล็อกของคำสั่งก็จะปล่อยสิทธิให้เธรดอื่นต่อไป การ synchronization สามารถทำได้ 2 วิธีคือกำหนดให้เมธอดที่จะเกิดปัญหาเป็นเมธอดแบบ synchronization หรือกำหนดให้เฉพาะบล็อกของคำสั่งที่จะทำให้เกิดปัญหาทำงานภายใต้บล็อก synchronization
จากตัวอย่างด้านบนเรากำหนดให้เมธอด count1to10 เป็น synchronization โดยระบุคีย์เวิร์ด synchronized จะพบว่าเธรดไม่รบกวนกัน โดยแต่ละเธรดจะทำงานจนเสร็จแล้วเมธอดถึงจะถูกปล่อยให้เธรดอื่นทำงาน
นอกจากการกำหนดให้ทั้งเมธอดเป็น synchronization แล้ว เราสามารถกำหนดการ synchronization เฉพาะบล็อกของคำสั่งที่จะมีผลกระทบกับการรบกวนกันของเธรด ซึ่งการกำหนดแบบนี้จะช่วยให้ประสิทธิภาพการทำงานของโปรแกรมดีกว่าการกำหนดทั้งเมธอด โดยสร้างบล็อกของ synchronization และระบุตัวแปรชนิดออบเจกต์ที่อยู่ในฮี๊บของคลาสดังกล่าวเป็นพารามิเตอร์ จากตัวอย่างด้านล่าง เฉพาะส่วนของลูป for เท่านั้นที่จะก่อให้เกิดปัญหาการรบกวนกัน เราจึงกำหนดให้เฉพาะส่วนดังกล่าวไว้ภายใน บล็อกของ synchronization และใช้ตัวออบเจกต์เองเป็นพารามิเตอร์ (เราไม่สามารถใช้ตัวแปร i เป็นพารามิเตอร์เพราะ i เป็น primitive data type ไม่ใช่ออบเจกต์ หากเรามีตัวแปรที่เป็น String ซึ่งเป็นออบเจกต์จะสามารถใช้เป็นพารามิเตอร์ได้ แต่ต้องแน่ใจว่าตัวแปรไม่ได้เป็น local variable ซึ่งจะถูกเก็บในเธรดแสตกซึ่งจะไม่ส่งผลต่อการล๊อคเพราะข้อมูลในเธรดแสตกเป็นของเธรดใครเธรดมัน) จากผลลัพธ์จะเห็นว่าแต่ละเธรดทำงานของตัวเองจนเสร็จจึงปลดล๊อคให้เธรดอื่นทำงานต่อ
ใน Java API หากคลาสที่จะใช้ระบุว่า thread safe หมายถึงทางผู้พัฒนาได้กำหนดการ synchronization ในส่วนที่จะเกิดปัญหาการรบกวนกันของเธรดไว้หมดแล้ว แต่ถ้าไม่มีการระบุไว้เราจะต้องระวังเรื่องการรบกวนกันของเธรดด้วยตนเอง
การใช้เมธอด wait notify และ notifyAll
ในกรณีที่เราใช้งานเธรดมากกว่า 1 เธรดซึ่งทำงานพร้อมๆกันตลอดเวลาเพื่อสลับกันทำงานกับออบเจกต์หรือตัวแปรเดียวกัน เช่น การใช้เธรด 2 เธรดเพื่อคอยอ่านและเขียนข้อความลงในตัวแปรเดียวกันวนๆไป เธรดที่จะเขียนข้อความก็ต้องแน่ใจว่าข้อความก่อนหน้าถูกอ่านไปหมดแล้ว ในขณะที่เธรดที่จะอ่านข้อความก็ต้องรอจนกว่าจะมีข้อความถึงจะเริ่มอ่าน และการทำงานของทั้งสองเธรดจะต้องไม่รบกวนกันดังนั้นเราจึงกำหนด synchronization ให้กับเมธอดการอ่านและเขียนข้อมูล ซึ่งกลายเป็นก่อปัญหาให้กับโปรแกรมเพราะเธรดทำงานตลอดเวลา กลายเป็นว่าเธรดที่ได้สิทธิในการทำงานจะไม่ยอมปล่อยให้เธรดอื่นใช้งานออบเจกต์หรือตัวแปรที่ถูกล๊อคไว้ (ไม่ปล่อยเพราะตัวมันเองก็วนลูปทำงานอยู่) จากตัวอย่างด้านล่าง เราเขียนโปรแกรมเพื่อจำลองการส่งลูกบอลไปมาระหว่างผู้เล่น 2 คน โดยเราสร้างคลาส Ball เพื่อเป็นตัวแทนลูกบอลขึ้นมาโดยมีตัวแปร status ที่ถูกกำหนดค่า true เป็นค่าเริ่มต้น และสร้างคลาส FirstPlayer และ SecondPlayer เป็นตัวแทนผู้เล่นซึ่งเป็นเธรด โดยผู้เล่นจะเรียกเมธอด fisrtHoldBall() หรือเมธอด secondHoldBall เพื่อตรวจสอบว่าตนเองมีลูกบอล (ตรวจสอบจากตัวแปร status) และส่งลูกบอล (พิมพ์ข้อความ) ออกไปหาผู้เล่นอีกคนหนึ่ง เนื่องจากคลาสผู้เล่นเป็นเธรดดังนั้นทั้งสองเมธอดจะวนลูปตรวจสอบสถานะของลูกบอลอยู่ตลอดและจะส่งลูกบอลเมื่อตัวแปร status ไม่เป็นไปตามเงื่อนไข ซึ่งตัวแปร status จะต้องถูกเปลี่ยนโดยอีกเมธอดหนึ่งนั่นเอง เมื่อเราเรียกใช้งานโปรแกรมจะพบว่าไม่มีการทำงานใดๆเหมือนโปรแกรมแฮงค์ ทั้งนี้เป็นเพราะเมธอด fisrtHoldBall() และเมธอด secondHoldBall เรากำหนดให้เป็น synchronization พอเริ่มทำงานด้วย status เป็น true จะเกิดการวนลูปไม่รู้จบและไม่ปล่อยให้เธรดอื่นทำงาน
ดังนั้นเพื่อให้ทั้ง 2 เธรดสามารถทำงานสอดประสานกันได้ เราจะใช้เมธอด wait() และเมธอด notifyAll() เพื่อให้เกิดการสลับการทำงาน โดยกำหนดเมธอด wait() ในลูปและเมธอด notifyAll() เมื่อจบการทำงานของแต่ละเมธอด สิ่งที่เกิดขึ้นคือเมื่อเกิดการทำงานในลูป เธรดจะทำงานตามเมธอด wait() คือหยุดคอยและยอมให้เธรดอื่นทำงาน ซึ่งอีกเธรดหนึ่งจะเปลี่ยนสถานะของตัวแปร status และเรียกเมธอด notifyAll() เพื่อแจ้งเธรดอื่นให้ทำงานได้ เป็นการสลับกันทำงานด้วยเมธอด wait() และเมธอด notifyAll() นั่นเอง ซึ่งจะได้ผลลัพธ์ตามตัวอย่างด้านล่าง
สำหรับในกรณีที่เรามีเธรดอยู่หลายเธรด การใช้เมธอด notifyAll() เพื่อปลุกทุกเธรดมาทำงานอาจจะกระทบกับประสิทธิภาพของระบบ ดังนั้นเราจะใช้เมธอด notify() แทนแต่เราไม่สามารถรู้ได้ว่าเธรดใดจะถูกปลุกมาทำงาน เมธอด wait() notify() และ notifyAll() สามารถใช้ได้เฉพาะใน synchronization code เท่านั้น
การใช้คลาส ReentrantLock
ถึงแม้เราจะสามารถใช้การ Synchronization เพื่อแก้ไขการรบกวนกันของเธรดได้ แต่การใช้ Synchronization ยังมีข้อด้อย คือ 1) เธรดที่รอสิทธิเพื่อทำงานจะต้องรออยู่อย่างนั้นจนกว่าจะได้สิทธิในการทำงาน 2) บล็อกของ Synchronization จะต้องอยู่ในเมธอดเดียวกันเท่านั้น 3) เราไม่สามารถตรวจสอบได้ว่ามีการล๊อคไว้อยู่หรือไม่ และไม่สามารถรู้ข้อมูลอื่นๆเกี่ยวกับการล๊อค 4) ไม่สามารถยืนยันได้ว่าเธรดที่มารออยู่ก่อนจะได้ทำงานก่อน
ภาษาจาวาได้เตรียมแพคเกจ java.util.concurrent.locks คลาส ReentrantLock ให้เราใช้ในการแก้ปัญหาการรบกวนกันของเธรด โดยวิธีการใช้งานคือสร้างออบเจกต์ ReentrantLock และให้ทุกเธรดออบเจกต์ใช้งานออบเจกต์ ReentrantLock เดียวกันนี้ โดยเราจะใช้เมธอด lock() ของออบเจกต์ ReentrantLock ในการล๊อคส่วนของโปรแกรมที่ต้องการ และเมื่อใช้งานเสร็จแล้วเราจะต้องปลดล๊อคด้วยเมธอด unlock() (ต่างจากการทำ synchronization ที่การปลดล๊อคจะเป็นไปอย่างอัตโนมัติเมื่อทำงานจบบล็อกของเมธอดหรือคำสั่งที่เป็น synchronization )
การใช้งานออบเจกต์ ReentrantLock ต้องใช้งานควบคู่กับ try-finally บล็อกเสมอเพื่อป้องกันความผิดพลาดจากการที่เมธอด unlock() ไม่ถูกเรียกใช้งาน โดยเราจะกำหนดเมธอด unlock() ไว้ในบล็อก finally เพื่อให้แน่ใจว่าจะเกิดการปลดล๊อคเสมอไม่ว่าเกิดอะไรขึ้นก็ตาม
จากตัวอย่างด้านล่างเป็นการใช้เธรด 3 เธรดทำงาน โดยเธรด putword จะใส่ตัวเลข 1-9 ลงในอาเรย์ และเธรด getword1 getword2 จะอ่านค่ามาพิมพ์และลบค่าออกจากอาเรย์ บรรทัดที่ 10 เป็นการสร้างออบเจกต์ ReentrantLock บรรทัดที่ 11 เป็นการสร้างอาเรย์เพื่อเก็บตัวเลข บรรทัดที่ 12-17 เป็นการสร้างเธรดออบเจกต์ โดยเราส่งผ่านออบเจกต์ ReentrantLock และอาเรย์ตัวเดียวกันนี้ให้ทั้ง 3 เธรดใช้งานร่วมกัน
บรรทัดที่ 21 เป็นการสร้างคลาส PutWord เพื่อใส่ตัวเลขลงในอาเรย์ โดยในบรรทัดที่ 34-39 และบรรทัดที่ 44-49 เป็นการใช้ออบเจกต์ ReentrantLock ร่วมกับ try-finally บล็อกในการป้องกันการรบกวนกันของเธรด คลาส PutWord จะใช้ลูป for เพื่อเพิ่มค่าลงในอาเรย์ และใส่ข้อความ EOF เพื่อบอกว่าหมดข้อมูลแล้ว
บรรทัดที่ 53 เป็นการสร้างคลาส GetWord เพื่ออ่านตัวเลขและลบออกจากอาเรย์ โดยในบรรทัด 66-79 เป็นการใช้ออบเจกต์ ReentrantLock ร่วมกับ try-finally บล็อกในการป้องกันการรบกวนกันของเธรด โดยคลาส GetWord จะใช้ลูป while ซึ่งกำหนดค่าให้เป็นจริงเสมอเพื่ออ่านค่าจากอาเรย์ตลอดเวลา โดยจะตรวจสอบว่าอาเรย์ว่างหรือไม่ หากว่างให้วนลูปใหม่ หากพบค่าที่ไม่ใช่ EOF ให้พิมพ์และลบค่าออก และออกจากลูปหากเป็นค่า EOF
จากผลลัพธ์ที่ได้จะเป็นว่าเธรดทำงานได้สำเร็จโดยที่ไม่เกิดการรบกวนกัน แต่ลำดับการทำงานของเธรดก็ยังคงไม่สามารถคาดเดาได้
เราสามารถใช้เมธอด tryLock() เพื่อตรวจสอบว่าสามารถล๊อคได้หรือไม่ซึ่งหากว่าไม่สามารถล๊อคได้เรายังสามารถให้เธรดทำงานอื่นได้แทนที่จะต้องหยุดคอยเฉยๆ ซึ่งจะต่างกับการใช้ synchronization ที่เธรดต้องรอจนกว่าจะได้สิทธิเพื่อทำงาน ตัวอย่างเช่น
If (reentrantLockObject.tryLock() {
\\ ทำงานตามที่ต้องการหลังจากได้สิทธิ
} else {
\\ ทำงานอื่นหากว่ายังไม่ได้สิทธิ
}
การใช้ ExecutorService จัดการเธรด
เมื่อเราต้องมีเธรดหลายเธรดทำงานพร้อมๆกัน เราสามารถใช้คลาสอินเตอร์เฟส ExecutorService ช่วยในการจัดการเธรดให้เราแทนได้ ExecutorService จะช่วยเราจัดการทั้งเรื่องการสร้างเธรด เรียกเธรด (start) การจัดลำดับการทำงานของเธรด การยุติการทำงานของเธรด
เราใช้งาน ExecutorService โดยการสร้างออบเจกต์ ExecutorService ซึ่งกลุ่มของเธรด (Thread pool) จะถูกสร้างขึ้นมา จากนั้นจึงใช้เมธอด execute() เพื่อจัดการเธรดที่ต้องการและใช้เมธอด shutdown() เมื่อเสร็จงานเพื่อปิดการทำงานของ ออบเจกต์ ExecutorService ถ้าหากเราไม่สั่ง shutdown โปรแกรมจะยังไม่จบการทำงาน แม้ว่าจะไม่มีเธรดใดทำงานแล้ว
เราสามารถกำหนดจำนวนเธรดในกลุ่มเธรดซึ่งหากมีเธรดทำงานเต็มตามจำนวนที่กำหนดแล้ว เธรดที่มาใหม่จะต้องเข้าคิวคอยจนกว่าจะมีเธรดในกลุ่มยุติการทำงาน (terminate)
จากตัวอย่างด้านล่างเราสร้างออบเจกต์ ExecutorService ขึ้นมาในบรรทัดที่ 14 โดยกำหนดชนิดของกลุ่มเธรดเป็น FixedThreadPool และกำหนดจำนวนสูงสุดของเธรดในกลุ่มไว้ที่ 3 เธรด และแทนที่การเรียกใช้เธรดของเดิมด้วยเมธอด execute() ในบรรทัดที่ 21-23 และสั่งปิดด้วยเมธอด shutdown() ในบรรทัดที่ 24
การสร้างออบเจกต์ ExecutorService จากตัวอย่างด้านบนเป็นการสร้างโดยใช้ Factory Method และกำหนดชนิดของกลุ่มเธรด ซึ่งมีให้เลือกตามความต้องการใช้งาน
ปัญหาที่มักจะพบจากการใช้หลายเธรดพร้อมกัน
ในการใช้งานจริงจะมีการใช้งานเธรดพร้อมๆกันหลายเธรด ซึ่งหากเราออกแบบโปรแกรมไม่ดีอาจจะพบกับปัญหาเหล่านี้ได้ไม่ยาก
Deadlocks คือปัญหาที่เกิดจากการที่เธรดต้องการสิทธิการใช้งานจากหลายๆตัวแปรหรือออบเจกต์ แต่พบว่าสิทธิที่ต้องการถูกถือโดยเธรดอื่น ในขณะเดียวกันเธรดอื่นก็รอสิทธิที่ถูกถืออยู่โดยเธรดดังกล่าวเช่นกัน เช่น Thread-A ต้องการสิทธิใน method-1 และ method-2 ตามลำดับ ส่วน Thread-B ต้องการสิทธิใน method-2 และ method-1 ตามลำดับ เมื่อโปรแกรมทำงาน Thread-A จะได้สิทธิใน method-1 และ Thread-B จะได้สิทธิใน method-2 หลังจากนั้น Thread-A จะขอสิทธิใน method-2 โดยที่ยังถือครองสิทธิใน method-1 อยู่ ในขณะที่ Thread-B จะขอสิทธิใน method-1 โดยที่ยังถือครองสิทธิใน method-2 อยู่ ดังนั้นทั้ง 2 เธรดจึงหยุดรอแบบไม่รู้จบ วิธีการแก้ไขคือออกแบบลำดับการเข้าใช้งานของเธรดให้สอดคล้องกัน เช่น Thread-A ต้องการสิทธิใน method-1 และ method-2 ตามลำดับ ส่วน Thread-B ต้องการสิทธิใน method-1 และ method-2 ตาลำดับเดียวกัน
Live Lock จะมีลักษณะปัญหาเหมือนกับ Deadlocks ต่างกันที่เธรดไม่ได้
ถูกบล็อกให้รอเฉยๆแต่ยังคงทำงานอยู่ มักจะเกิดในกรณีที่เธรดมีการทำงานแบบวนลูป
Thread Starvation เป็นปัญหาที่เกิดจากการกำหนดลำดับความสำคัญ (priority) ของเธรด ทำให้บางเธรดที่มีลำดับความสำคัญน้อยกว่าเธรดอื่นไม่มีโอกาศได้สิทธิ วิธีการแก้ไขคือการทำ synchronization ด้วยคลาส ReentrantLock โดยระบุพารามิเตอร์เป็น true เพื่อใช้ความสามารถ fair lock ซึ่งจะจัดการให้เธรดที่มาก่อนได้สิทธิก่อน โดยมีรูปแบบคือ
ReentrantLock reentrantLock = new ReentrantLock(true);
ส่วนย่อยที่สุดที่เธรดไม่สามารถถูกหยุดได้
จากหัวข้อที่ผ่านมาจะเห็นว่าเธรดสามารถถูกหยุดการทำงานกลางคันเพื่อสลับการทำงานไปยังเธรดอื่นได้ เพราะว่ากิจกรรมต่างๆนั้นยังสามารถแยกย่อยออกเป็นขั้นตอนต่างๆได้อีก อย่างไรก็ตามมีส่วนที่ย่อยที่สุดที่เธรดไม่สามารถถูกหยุดกลางคันได้เรียกว่ากิจกรรมแบบอะตอมมิค (atomic action) โดยกิจกรรมแบบอะตอมมิคมี 3 ชนิดคือ
1) การเขียนและอ่านตัวแปรชนิด reference data type เช่น myObject1 = myObject2
2) การเขียนและอ่านตัวแปรชนิด primitive data type ยกเว้นชนิด long และ double เช่น myInt = 10
3) การเขียนและอ่านตัวแปรชนิด volatile
ตัวแปรชนิด volatile คือตัวแปรที่เรากำกับด้วยคีย์เวิร์ด volatile เพื่อบอกให้ JVM จัดการการเก็บค่าของตัวแปรในแบบพิเศษคือค่าที่เก็บในหน่วยความจำและค่าที่เก็บในแคช (cach) ของ CPU จะต้องเหมือนกัน ทั้งนี้ในการจัดการกับค่าของตัวแปรนอกจากเก็บค่าของตัวแปรในหน่วยความจำแล้ว ค่าของตัวแปรยังถูกเก็บในแคชของ CPU เพื่อความรวดเร็วในการทำงาน แต่เนื่องจากเครื่องคอมพิวเตอร์ในปัจจุบันมีมากกว่า 1 CPU ดังนั้นจึงมีโอกาศที่ค่าที่เก็บในแคชของแต่ละ CPU และค่าที่เก็บในหน่วยความจำจะไม่ตรงกัน การกำกับตัวแปรด้วยคีย์เวิร์ด volatile ก็เพื่อแก้ปัญหาดังกล่าว ตัวอย่างเช่น
public volatile int integerVariable;
โดยมากคีย์เวิร์ด volatile จะใช้กับตัวแปรชนิด long และ double เพื่อให้การเขียนและอ่านตัวแปรชนิดดังกล่าวเป็นกิจกรรมแบบอะตอมมิค
Java API ได้เตรียแพคเกจ java.util.concurrent.atomic ให้เราใช้งานเพื่อสร้างและจัดการตัวแปรแบบอะตอมมิคโดยในแพคเกจจะประกอบด้วยคลาส เช่น AtomicInteger เป็นการสร้างและจัดการตัวแปรชนิด Integer ในแบบอะตอมมิค วิธีใช้คือประกาศตัวแปรด้วยชนิด AtomicInteger ตัวอย่างเช่น
AtomicInteger atomicInteger = new AtomicInteger(0);