การแนะนำ
Multithreading ถูกสร้างขึ้นใน Java มาตั้งแต่วันแรก ลองมาดูกันคร่าวๆ ว่า multithreading คืออะไร
ลองใช้บทเรียนอย่างเป็นทางการจาก Oracle เป็นจุดเริ่มต้น: "
บทเรียน: แอปพลิเคชัน "Hello World! " มาเปลี่ยนโค้ดของแอปพลิเคชั่น Hello World ของเรากันเล็กน้อยดังต่อไปนี้:
class HelloWorldApp {
public static void main(String[] args) {
System.out.println("Hello, " + args[0]);
}
}
args
คืออาร์เรย์ของพารามิเตอร์อินพุตที่ส่งผ่านเมื่อโปรแกรมเริ่มทำงาน
.java
มาบันทึกโค้ดนี้ ลงในไฟล์ด้วยชื่อที่ตรงกับชื่อของคลาสและนามสกุล มาคอมไพล์โดยใช้ ยูทิลิตี้
javac :
javac HelloWorldApp.java
หลังจากนั้นให้เรียกโค้ดของเราพร้อมกับพารามิเตอร์บางตัว เช่น Roger:
java HelloWorldApp Roger
ตอนนี้โค้ดของเรามีข้อบกพร่องร้ายแรง หากเราไม่ผ่านการโต้แย้งใดๆ (เช่น เพียงเรียกใช้งาน Java HelloWorldApp) เราจะได้รับข้อผิดพลาด:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
at HelloWorldApp.main(HelloWorldApp.java:3)
มีข้อยกเว้น (เช่น ข้อผิดพลาด) เกิดขึ้นในเธรดชื่อ
main
. ปรากฎว่ามีเธรดบางประเภทใน Java? นี่คือจุดเริ่มต้นของการเดินทางของเรา
Java และเธรด
เพื่อทำความเข้าใจว่าเธรดคืออะไร คุณต้องเข้าใจว่าแอปพลิเคชัน Java ถูกเรียกใช้งานอย่างไร มาเปลี่ยนรหัสของเราดังนี้:
class HelloWorldApp {
public static void main(String[] args) {
while (true) {
}
}
}
ตอนนี้เรามาคอมไพล์อีกครั้งโดยใช้ javac ต่อไป เพื่อความสะดวก เราจะเรียกใช้โค้ด Java ของเราในหน้าต่างแยกต่างหาก บน Windows คุณสามารถทำได้ดังนี้:
start java HelloWorldApp
. ตอนนี้ เมื่อใช้ ยูทิลิตี้
jpsเรามาดูกันว่าข้อมูลใดที่ Java จะบอกเรา:
ตัวเลขแรกคือ PID หรือ Process ID ซึ่งเป็นตัวระบุกระบวนการ กระบวนการคืออะไร?
Процесс — это совокупность codeа и данных, разделяющих общее виртуальное addressное пространство.
ด้วยความช่วยเหลือของกระบวนการ การทำงานของโปรแกรมต่างๆ จะถูกแยกออกจากกัน: แต่ละแอปพลิเคชันใช้พื้นที่หน่วยความจำของตัวเองโดยไม่รบกวนโปรแกรมอื่น ฉันแนะนำให้คุณอ่านบทความโดยละเอียด: "
https://habr.com/post/164487/ " กระบวนการไม่สามารถดำรงอยู่ได้หากไม่มีเธรด ดังนั้นหากมีกระบวนการอยู่ ก็จะมีอย่างน้อยหนึ่งเธรดอยู่ในนั้น สิ่งนี้เกิดขึ้นได้อย่างไรใน Java? เมื่อเรารันโปรแกรม Java การดำเนินการจะเริ่มต้นด้วยนามสกุล
main
. เราค่อนข้างจะเข้าสู่โปรแกรม ดังนั้นวิธีการพิเศษนี้
main
จึงเรียกว่าจุดเข้าหรือ "จุดเข้า" วิธีการ
main
จะต้องเป็นเช่นนั้นเสมอ
public static void
เพื่อให้ Java Virtual Machine (JVM) สามารถเริ่มรันโปรแกรมของเราได้ ดู "
เหตุใดวิธีการหลักของ Java จึงคงที่ " สำหรับรายละเอียดเพิ่มเติม ปรากฎว่าตัวเรียกใช้งาน Java (java.exe หรือ javaw.exe) เป็นแอปพลิเคชันธรรมดา (แอปพลิเคชัน C แบบง่าย): โหลด DLL ต่างๆ ซึ่งจริงๆ แล้วคือ JVM ตัวเรียกใช้งาน Java สร้างชุดการเรียก Java Native Interface (JNI) เฉพาะ JNI เป็นกลไกที่เชื่อมโยงโลกของ Java Virtual Machine และโลกของ C++ ปรากฎว่าตัวเรียกใช้งานไม่ใช่ JVM แต่เป็นตัวโหลด รู้คำสั่งที่ถูกต้องในการดำเนินการเพื่อเริ่มต้น JVM รู้วิธีจัดระเบียบสภาพแวดล้อมที่จำเป็นทั้งหมดโดยใช้การเรียก JNI การจัดระเบียบสภาพแวดล้อมนี้ยังรวมถึงการสร้างเธรดหลัก ซึ่งโดยปกติจะเรียก
main
ว่า เพื่อให้เห็นได้ชัดเจนยิ่งขึ้นว่าเธรดใดอยู่ในกระบวนการจาวา เราใช้ โปรแกรม
jvisualvmซึ่งรวมอยู่ใน JDK เมื่อรู้ pid ของกระบวนการ เราก็สามารถเปิดข้อมูลได้ทันที
jvisualvm --openpid айдипроцесса
ที่น่าสนใจคือ แต่ละเธรดมีพื้นที่แยกต่างหากในหน่วยความจำที่จัดสรรไว้สำหรับกระบวนการ โครงสร้างหน่วยความจำนี้เรียกว่าสแต็ก สแต็กประกอบด้วยเฟรม เฟรมคือจุดของการเรียกเมธอด จุดดำเนินการ เฟรมยังสามารถแสดงเป็น StackTraceElement ได้ (ดู Java API สำหรับ
StackTraceElement ) คุณสามารถอ่านเพิ่มเติมเกี่ยวกับหน่วยความจำที่จัดสรรให้กับแต่ละเธรดได้
ที่นี่ หากเราดูที่
Java APIและค้นหาคำว่า Thread เราจะเห็นว่ามีคลาส
java.lang.Thread คลาสนี้เองที่แสดงถึงสตรีมใน Java และด้วยเหตุนี้เองที่เราต้องทำงาน
java.lang.Thread
เธรดใน Java ถูกแสดงเป็นอินสแตนซ์ของ
java.lang.Thread
คลาส ควรทำความเข้าใจทันทีว่าอินสแตนซ์ของคลาส Thread ใน Java ไม่ใช่เธรดในตัวมันเอง นี่เป็นเพียง API ชนิดหนึ่งสำหรับเธรดระดับต่ำที่จัดการโดย JVM และระบบปฏิบัติการ เมื่อเราเปิดตัว JVM โดยใช้ตัวเรียกใช้งาน Java มันจะสร้างเธรดหลักพร้อมชื่อ
main
และเธรดบริการอีกมากมาย ตามที่ระบุไว้ใน JavaDoc ของคลาส Thread:
When a Java Virtual Machine starts up, there is usually a single non-daemon thread
มีเธรด 2 ประเภท: daemons และ non-daemons เธรด Daemon คือเธรดพื้นหลัง (เธรดบริการ) ที่ทำงานบางอย่างในเบื้องหลัง คำที่น่าสนใจนี้เป็นการอ้างอิงถึง "ปีศาจของ Maxwell" ซึ่งคุณสามารถอ่านเพิ่มเติมได้ในบทความ Wikipedia เกี่ยวกับ "
ปีศาจ " ตามที่ระบุไว้ในเอกสารประกอบ JVM ดำเนินการโปรแกรม (กระบวนการ) ต่อไปจนกระทั่ง:
- ไม่ได้เรียกเมธอดRuntime.exit
- เธรดที่ไม่ใช่ daemon ทั้งหมดทำงานเสร็จสิ้นแล้ว (ทั้งที่ไม่มีข้อผิดพลาดและมีข้อยกเว้นเกิดขึ้น)
ดังนั้นรายละเอียดที่สำคัญ: เธรด daemon สามารถยุติได้ในคำสั่งใด ๆ ที่กำลังดำเนินการ ดังนั้นจึงไม่รับประกันความสมบูรณ์ของข้อมูลในนั้น ดังนั้นเธรด daemon จึงเหมาะสำหรับงานบริการบางอย่าง ตัวอย่างเช่น ใน Java มีเธรดที่รับผิดชอบในการประมวลผลวิธีการสรุปหรือเธรดที่เกี่ยวข้องกับ Garbage Collector (GC) แต่ละเธรดเป็นของกลุ่มบางกลุ่ม (
ThreadGroup ) และกลุ่มสามารถเข้ามามีส่วนร่วมกันโดยสร้างลำดับชั้นหรือโครงสร้างบางอย่างได้
public static void main(String []args){
Thread currentThread = Thread.currentThread();
ThreadGroup threadGroup = currentThread.getThreadGroup();
System.out.println("Thread: " + currentThread.getName());
System.out.println("Thread Group: " + threadGroup.getName());
System.out.println("Parent Group: " + threadGroup.getParent().getName());
}
กลุ่มช่วยให้คุณปรับปรุงการจัดการโฟลว์และติดตามโฟลว์เหล่านั้นได้ นอกจากกลุ่มแล้ว เธรดยังมีตัวจัดการข้อยกเว้นของตนเอง ลองดูตัวอย่าง:
public static void main(String []args) {
Thread th = Thread.currentThread();
th.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("An error occurred: " + e.getMessage());
}
});
System.out.println(2/0);
}
การหารด้วยศูนย์จะทำให้เกิดข้อผิดพลาดที่ตัวจัดการจะตรวจจับได้ หากคุณไม่ได้ระบุตัวจัดการด้วยตนเอง การใช้งานตัวจัดการเริ่มต้นจะทำงานได้ ซึ่งจะแสดงสแต็กข้อผิดพลาดใน StdError คุณสามารถอ่านเพิ่มเติมได้ในบทวิจารณ์
http://pro-java.ru/java-dlya-opytnyx/obrabotchik-neperexvachennyx-isklyuchenij-java/ " นอกจากนี้ เธรดยังมีลำดับความสำคัญ คุณสามารถอ่านเพิ่มเติมเกี่ยวกับลำดับความสำคัญได้ใน บทความ "
Java Thread Priority ใน Multithreading "
การสร้างเธรด
ตามที่ระบุไว้ในเอกสารประกอบ เรามี 2 วิธีในการสร้างเธรด ประการแรกคือการสร้างทายาทของคุณเอง ตัวอย่างเช่น:
public class HelloWorld{
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello, World!");
}
}
public static void main(String []args){
Thread thread = new MyThread();
thread.start();
}
}
อย่างที่คุณเห็น งานจะถูกเปิดใช้งานใน method
run
และ thread จะถูกเปิดใช้งานใน
start
method ไม่ควรสับสนเพราะ... ถ้าเรารันเมธอด
run
โดยตรง จะไม่มีการเริ่มเธรดใหม่ เป็นวิธีการ
start
ที่ขอให้ JVM สร้างเธรดใหม่ ตัวเลือกที่มีการสืบทอดจาก Thread ไม่ดีเนื่องจากเรารวม Thread ไว้ในลำดับชั้นของชั้นเรียน ข้อเสียอย่างที่สองคือเรากำลังเริ่มละเมิดหลักการ “Sole Responsibility” SOLID เพราะ ชั้นเรียนของเราจะรับผิดชอบทั้งการจัดการเธรดและงานบางอย่างที่ต้องดำเนินการในเธรดนี้ไปพร้อม ๆ กัน ข้อไหนถูกต้อง? คำตอบอยู่ในวิธีการเดียว
run
กับที่เราแทนที่:
public void run() {
if (target != null) {
target.run();
}
}
นี่
target
คือบางส่วน
java.lang.Runnable
ซึ่งเราสามารถส่งผ่านไปยัง Thread เมื่อสร้างอินสแตนซ์ของคลาส ดังนั้นเราจึงสามารถทำได้:
public class HelloWorld{
public static void main(String []args){
Runnable task = new Runnable() {
public void run() {
System.out.println("Hello, World!");
}
};
Thread thread = new Thread(task);
thread.start();
}
}
นอกจากนี้ยัง เป็น
Runnable
อินเทอร์เฟซที่ใช้งานได้ตั้งแต่ Java 1.8 สิ่งนี้ทำให้คุณสามารถเขียนโค้ดงานสำหรับเธรดได้สวยงามยิ่งขึ้น:
public static void main(String []args){
Runnable task = () -> {
System.out.println("Hello, World!");
};
Thread thread = new Thread(task);
thread.start();
}
ทั้งหมด
ดังนั้น ฉันหวังว่าจากเรื่องราวนี้จะมีความชัดเจนว่ากระแสคืออะไร มีอยู่อย่างไร และการดำเนินการพื้นฐานใดบ้างที่สามารถทำได้กับกระแสเหล่านั้น ใน
ส่วนถัดไปควรทำความเข้าใจว่าเธรดมีปฏิสัมพันธ์กันอย่างไร และวงจรชีวิตของเธรดคืออะไร #เวียเชสลาฟ
GO TO FULL VERSION