Mục lục
Thread trong java
Thread(luồng) là một phần thực thi độc lập bên trong một chương trình. JVM sẽ cung cấp cho mỗi thread một method-call stack để theo dõi quá trình thực thi của mỗi thread. Ngoài việc theo dõi đang thực thi mã code đến đâu thì method-call stack còn theo dõi các biến local, tham số truyền vào và kết quả trả về của từng method trong stack.
method-call stack: Là một Stack lưu trữ các method được gọi trong quá trình thực thi của một thread. Các method được gọi sau được thêm vào đỉnh Stack, một method sau khi thực hiện xong thì sẽ bị xoá khởi Stack nhường chỗ cho method tiếp theo cho đến khi Stack rỗng.
Mặc định trong một chương trình java có ít nhất một main thread. Nếu chúng ta có nhiều hơn 1 thread thực thi trong chương trình được gọi là đa luồng (multithreading).
Sử dụng multithreading giúp tận dụng tối đa tài nguyên máy tính từ đó tăng tốc độ xử lý của chương trình. Multithreading còn giúp trải nghiệm người dùng tốt hơn bằng cách chia công việc cho mỗi thread, trong khi một thread render giao diện thì thread khác lại lấy dữ liệu từ server người dùng sẽ không thấy hoặc không đáng kể về độ trễ của chương trình.
Java cho phép chúng ta lập trình multithreading bằng cách khởi tạo một class thừa kế từ java.lang.Thread. Mỗi Thread object đại diện cho một thread riêng. Khi thread bắt đầu khởi chạy run() method sẽ được gọi, chúng ta phải override run() method để thực thi đoạn mã mong muốn vì mặc định run() là một method rỗng.
class ThreadDemo { public static void main(String[] args) { MyThread mt = new MyThread(); mt.start(); for (int i = 0; i < 50; i++) System.out.println("Main thread: " + (i + 1)); } } class MyThread extends Thread { public void run() { for (int count = 1, row = 1; row < 20; row++, count++) { for (int i = 0; i < count; i++) System.out.print('*'); System.out.print('\n'); } } }
Ở ví dụ trên chúng ta có 2 class là ThreadDemo và MyThread extends Thread. Trong khai main() method cố gắng in giá trị từ 1 đến 50 ra màn hình console thì nó cũng khởi tạo MyThread object. Khi Mythread#start() thì run() method được gọi in ra một hình tam giác vào màn hình console,
Output:
* ** *** Main thread: 1 Main thread: 2 Main thread: 3 Main thread: 4 Main thread: 5 **Main thread: 6 Main thread: 7 Main thread: 8 Main thread: 9 Main thread: 10 Main thread: 11 Main thread: 12 Main thread: 13 Main thread: 14 Main thread: 15 Main thread: 16 ** ***Main thread: 17 Main thread: 18 Main thread: 19 ** ****** ******* ******** ********* ********** *********** ************ ************* ************** *************** **************** ***************** ***************Main thread: 20 Main thread: 21 Main thread: 22 Main thread: 23 Main thread: 24 Main thread: 25 Main thread: 26 Main thread: 27 Main thread: 28 Main thread: 29 Main thread: 30 Main thread: 31 Main thread: 32 Main thread: 33 Main thread: 34 Main thread: 35 Main thread: 36 Main thread: 37 Main thread: 38 Main thread: 39 Main thread: 40 Main thread: 41 Main thread: 42 Main thread: 43 Main thread: 44 Main thread: 45 Main thread: 46 Main thread: 47 Main thread: 48 Main thread: 49 Main thread: 50 *** *******************
Chúng ta có thể thấy được main() và run() method thực thi cùng lúc cho nên các kết quả của 2 hàm được in ra xen kẽ nhau.
Tạo thread với Thread class trong java
Hiễu rõ Thread class sẽ giúp chúng ta dễ dàng hơn khi viết mã về multithreading.
Khởi tạo Thread
Chúng ta có đến 8 constructor để khởi tạo Thread object, 2 constructor thường được sử dụng:
- Thread() – Khởi tạo Thread object với tên mặc định.
- Thread(String name) – Khởi tạo Thread object với tên được chỉ định trong tham số truyền vào.
Bản chất của Thead object
Thead object không phải là một thread(luồng) trong chương trình, chúng chỉ là một bản thiết kế mô tả các thành phần như tên và mã code(bên trong run() method) của một thread. Khi start() method được gọi một thread sẽ được tạo mới và thực thi code bên trong run() method.
class ThreadDemo { public static void main(String[] args) { MyThread mt = new MyThread(); mt.start(); for (int i = 0; i < 50; i++) System.out.println("Main thread: " + (i + 1)); } } class MyThread extends Thread { public void run() { for (int count = 1, row = 1; row < 20; row++, count++) { for (int i = 0; i < count; i++) System.out.print('*'); System.out.print('\n'); } } }
Xem lại ví dụ trên, khi chương trình khởi chạy, nó sẽ tạo ra một main thread để thực thi main() method. Method main() gọi start() method để khởi tạo thread thứ 2, và thead này sẽ thực thi mã code bên trong run() method của MyThread class.
Luồng chạy của chương trình
Biểu đồ trên mô tả quá trình sau:
- Starting thread(main thread) được tạo và khởi chạy.
- Thực thi code bên trong hàm main.
- start() method được gọi.
- Khởi tạo thread mới và thực thi run() method.
- Main thread và thread mới kết thúc.
Thread name
Trong quá trình debug, việc phân biệt giữa một thread với các thread khác là một điều cần thiết, nếu Thread object được khởi tạo mặc định thì java sẽ tự đặt tên cho Thead, lưu ý rằng các tên thread được java đặt sẽ rất kho nhận biết. Để dễ dàng chúng ta sẽ đặt tên cho thread thông qua constructor hoặc setName() method.
class NameThatThread { public static void main(String[] args) { MyThread mt = new MyThread("thread test"); // mt.setName("thread test"); mt.start(); } } class MyThread extends Thread { MyThread() { // The compiler creates the byte code equivalent of super (); } MyThread(String name) { super(name); // Pass name to Thread superclass } public void run() { System.out.println("My name is: " + getName()); } }
Output: My name is: thread test
Thread sleep
Gọi sleep(long millis) method của Thread class bắt buộc một thread phải tạm dừng một khoảng millis. Các thead khác đang chạy có thể làm gián đoạn các thead đang tạm dừng, chúng sẽ thức dậy và ném InterrupedException từ sleep() method. Chúng ta phải đặt sleep() trong khối try-catch để xử lý khi có exception xảy ra.
Ví dụ sau sẽ khởi tạo 1 thread mới áp dụng công thức toán học để tính giá trị hằng số PI. Khi thead được khởi tạo, sẽ bị tạm dừng 5s sau đó tiếp tục tính toán theo công thức.
class CalcPI1 { public static void main(String[] args) { MyThread mt = new MyThread(); mt.start(); try { Thread.sleep(5000); // Sleep for 50000 milliseconds } catch (InterruptedException e) { } System.out.println("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; // Initializes to 0.0, by default public void run() { for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println("Finished calculating PI"); } }
Output:
Finished calculating PI
pi = 3.1415726535897894
Note: Quá trình tạm dừng của thread phụ thuộc vào nền tảng, có thể cho phép nó tính toán quá trị PI hoàn thành trước khi thead được đánh thức.
Kiểm tra Thead còn sống hay đã chết
Khi chương trình gọi start() method của một thead, JVM coi như thead này bắt đầu chu kỳ sống cho đến khi run() được gọi thực thi và kết thúc. Sau khi run() method thực thi xong, JVM sẽ tiến hành dọn dẹp, quá trình này sẽ tốn một khoảng thời gian, trong khoảng thời gian này JVM xem như thead đã hết chu kỳ sống.
isAlive() method trả về true cho chúng ta biết chính xác một thread có đang còn trong chu kỳ sống, ngược lại trả về false. Dưới đây mình sẽ lấy ví dụ sử dụng isAlive() để đợi một thread khác thực thi xong và lấy giá từ nó để đảm bảo có được kết quả cuối cùng.
class CalcPI2 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); while (mt.isAlive ()) try { Thread.sleep (10); // Sleep for 10 milliseconds } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; // Initializes to 0.0, by default public void run () { for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } }
Output:
Finished calculating PI
pi = 3.1415726535897894
Nếu không sử dụng alive() để chờ cho đến khi 1 thread thực thi xong mà chúng ta đã truy cập các giá trị trong thead thì có thể có được các kết quả trung gian dẫn đến các kết quả không mong muốn.
class CalcPI2 { public static void main(String[] args) { MyThread mt = new MyThread(); mt.start(); System.out.println("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; // Initializes to 0.0, by default public void run() { for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println("Finished calculating PI"); } }
Output:
pi = 0.0
Finished calculating PI
Thead join
Ở phần trên, để chờ một thread thực thi xong chúng ta đã phải sử dụng đến vòng lặp while và isAlive(). Vì cấu trúc lặp đi lặp lại quá nhiều trong lập trình multithreading nên java đã xây dựng sẵn join() method để chờ một thread thực thi xong, ngoài ra còn có các biến thể join(long milis) và join(long millis, long nanos) để chúng ta có thể chỉnh sữa giữa các lần chờ.
class CalcPI3 { public static void main(String[] args) { MyThread mt = new MyThread(); mt.start(); try { mt.join(); } catch (InterruptedException e) { } System.out.println("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; // Initializes to 0.0, by default public void run() { for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println("Finished calculating PI"); } }
Output:
pi = 0.0
Finished calculating PI
Note: Nếu bạn sử dụng join() method bên trong chính thread của nó, bạn sẽ chờ mãi mãi đấy nhé.
class MyThread extends Thread { boolean negative = true; double pi; // Initializes to 0.0, by default public void run() { try { this.join(); } catch (InterruptedException e) { e.printStackTrace(); } // ... } }
Tạo thread với Runable trong java
Ở phần trước, chúng ta đã biết cách khởi tạo một thread với Thead class. Thế nhưng đó không phải là cách duy nhất trong java. Trước tiên hãy xem một số hạn chế khi tạo thead với Thead class.
Như các bạn đã biết rằng java chỉ cho phép một class thừa kế từ một class duy nhất, nếu class của bạn đã thừa kế Thread class thì sẽ không thể thừa kế thêm một class nào khác, điều nào sẽ gây khó khăn phần nào trong lúc lập trình multithreading.
Để giải quyết vấn đề này, chúng ta có thể kết hợp Thread class và Runnable interface để tạo một thread cho một class bằng cách implements từ Runnable interface, Khai báo biến Thread bên trong class, khởi tạo một thread cho class bằng Thread(Runnable runnable) constructor. Runnable là một functional interface chứa duy nhất run() method, chúng ta phải triển khai run() method khi implement từ Runnable interface.
class RunnableDemo extends java.applet.Applet implements Runnable { // khai báo biến Thread bên trong class private Thread t; public void run() { int width = rnd(30); if (width < 2) width += 2; int height = rnd(10); if (height < 2) height += 2; draw(width, height); } public void start() { if (t == null) { // Sử dụng constructor Thread(Runnable runnable) t = new Thread(this); t.start(); } } public void stop() { if (t != null) t = null; } private void draw(int width, int height) { for (int c = 0; c < width; c++) System.out.print('*'); System.out.print('\n'); for (int r = 0; r < height - 2; r++) { System.out.print('*'); for (int c = 0; c < width - 2; c++) System.out.print(' '); System.out.print('*'); System.out.print('\n'); } for (int c = 0; c < width; c++) System.out.print('*'); System.out.print('\n'); } private int rnd(int limit) { // Return a random number x in the range 0 <= x < limit. return (int) (Math.random() * limit); } } class Main { public static void main(String[] args) { new RunnableDemo().start(); } }
Constructor Thread(Runnable runnable) nhận một tham số là runnable. Vì RunnableDemo class implement Runnable interface nên hoàn toàn có thể làm tham số trong trường hợp này.
Khi chúng ta khởi tạo một Thread object bên trong RunnableDemo có thể truyền this(instance hiện tại của class). Như vậy Thread t khi start sẽ gọi đến run() method đã được triển khai trong RunnableDemo class.
Tóm lược
Multithreading giúp tận dụng tối đa nguồn tài nguyên của máy tính, bên cạnh đó cũng có nhiều thách thách trong xử lý đồng giữa các thread. Chúng ta có thể tạo ra thread với Thread class và Runnable Interface, chúng thường được sử dụng chung chứ không phải là 2 thứ riêng lẽ trong lập trình đa luồng.
Nguồn tham khảo