Java Bài 43: Thread Tập 3 – Vòng Đời Của Thread

Posted by

Chào mừng các bạn đã đến với bài học Java số 43, bài học về Thread (phần tiếp theo). Đây là bài học trong chuỗi bài về lập trình ngôn ngữ Java của Yellow Code Books.

Với việc làm quen với Thread ở bài hôm trước, bạn đã biết rằng có hai cách để chúng ta tạo ra một Thread rồi.

Bước sang phần này của Thread, chúng ta cùng tìm hiểu sâu hơn về Thread, để xem khi bạn tạo ra một Thread nào đó, thì vòng đời của nó sẽ như thế nào? Thread đó sẽ trải qua những trạng thái nào trong vòng đời đó? Dựa vào các trạng thái đó, làm sao để các Thread có thể đồng bộ hoá, hay có thể hiểu là tự điều chỉnh độ ưu tiên trong việc thực thi tác vụ giữa các Thread trong cùng một Process với nhau? Mời bạn cùng đến với những kiến thức thú vị này hôm nay.

Trước hết, chúng ta cùng trả lời thắc mắc đầu tiên.

Vòng Đời Của Một Đối Tượng Là Gì?

Nếu đã làm quen với Android, thì bạn đã từng biết đến khái niệm “vòng đời” này rồi, như Vòng đời Activity, Vòng đời Fragment. Mình nhắc lại một chút thôi, đó là sở dĩ chúng ta xem xét “vòng đời” của một đối tượng nào đó, là khi mà đối tượng đó có một thời gian sống nhất định, và trong quá trình sống của đối tượng đó chúng ta muốn biết nó có thể sẽ trải qua nhiều trạng thái khác nhau như thế nào. Các trạng thái đó có phải là Sinh, Lão, Bệnh, Tử hay không? ^^

Tóm cái gì đó lại là, không phải cái gì chúng ta cũng đều nói về vòng đời của nó cả, chỉ những đối tượng có thời gian sống đủ lâu, và trải qua nhiều trạng thái trong quá trình sống, thì chúng ta mới xem xét đến vòng đời của nó thôi, như Activity và Fragment bên kiến thức Android, và Thread trong kiến thức Java này chẳng hạn.

Vậy tìm hiểu vòng đời, hay các trạng thái bên trong một vòng đời để làm gì.

Tại Sao Nên Tìm Hiểu Vòng Đời Của Một Đối Tượng?

Một lý do chính đáng nhất để chúng ta nên biết về vòng đời của một đối tượng nào đó, ngoài việc nó được sinh ra khi nào, và bị chết khi nào. Thì sự hiểu các trạng thái mà đối tượng đó trải qua trong quá trình sống đó cũng khá là quan trọng. Khi bạn nắm được các trạng thái của một vòng đời, bạn sẽ hiểu về đối tượng đó nhiều hơn, từ đó bạn có thể dễ dàng can thiệp vào nó, chèn vào các trạng thái đó các tác vụ tương ứng phù hợp nhất. Mục đích cuối cùng là làm cho ứng dụng của chúng ta trở nên mạnh mẽ hơn, và thậm chí, thông minh hơn nữa kìa. Chi tiết như thế nào mời các bạn cùng xem tiếp.

Tìm Hiểu Vòng Đời Của Thread

Nào chúng cùng quay lại phần chính của bài học, và cùng nhau tìm hiểu vòng đời của Thread. Trước hết mời bạn xem sơ qua vòng đời này thông qua sơ đồ sau.

Sơ Đồ Minh Họa Vòng Đời Của Thread

Sơ đồ minh hoạ vòng đời của Thread

Sơ đồ này dựng lên dựa trên các trạng thái đã được định nghĩa bên trong khai báo enum của lớp Thread. Enum là gì thì chúng ta sẽ nói đến ở bài học sau. Cơ bản thì bạn cứ hiểu enum giúp chúng ta định nghĩa ra một tập hợp các hằng số vậy, và trong tình huống này các hằng số này cũng chính là các trạng thái của vòng đời Thread.

Bạn có thể vào trong lớp Thread để xem việc khai báo các giá trị bên trong một enum là như thế nào. Bạn có thể vào bên trong Thread từ Eclipse để xem, hoặc có thể xem hình sau.

Các trạng thái chính bên trong vòng đời của thread

Mô Tả Vòng Đời Của Thread

Như mình có nói, sơ đồ hay các enum bên trong một Thread đã thể hiện rõ nhất các trạng thái bên trong một vòng đời của Thread này. Chúng được mô tả một cách tổng quát như sau.

Ngay khi bạn tạo mới một Thread, nhưng vẫn chưa gọi đến phương thức start(), trạng thái của nó sẽ là NEW.

Trạng thái NEW trong vòng đời của thread

Còn khi bạn đã gọi đến start(), Thread đó sẽ vào trạng thái RUNNABLE, trạng thái này đưa Thread vào hàng đợi để đợi hệ thống cấp tài nguyên và khởi chạy sau đó.

Trạng thái RUNNABLE trong vòng đời của thread

Trong quá trình Thread đang chạy, nếu có bất kỳ tác động nào, ngoại trừ làm kết thúc vòng đời của Thread, nó sẽ vào trạng thái BLOCKED, hoặc WAITING, hoặc TIMED_WAITING.

Các trạng thái Non-Runnable trong vòng đời của thread

Cuối cùng, khi một Thread kết thúc, nó đến trạng thái TERMINATED.

Trạng thái TERMINATED trong vòng đời của thread

Tổng quan là vậy, còn chi tiết từng trạng thái thì mình mời các bạn đến với mục tiếp theo sẽ rõ.

Các Trạng Thái Bên Trong Một Vòng Đời

Mục này chúng ta sẽ xem kỹ từng trạng thái một. Cái chính của mục này đó là giúp chúng ta hiểu được khi nào mà một Thread rơi vào một trạng thái nào đó. Qua đó bạn có thể tận dụng cho các mục đích cụ thể ở các project cụ thể của bạn sau này.

NEW

Trạng thái này rất dễ hiểu, khi bạn khởi tạo một Thread, nhưng vẫn chưa gọi đến phương thức start() của nó, thì Thread này sẽ rơi vào trạng thái NEW. Không tin à, mời bạn đến bài thực hành sau.

Bài Thực Hành Số 1

Ở bài thực hành đầu tiên này, chúng ta cùng xem trạng thái khi mà một Thread được khởi tạo nhưng phương thức start() vẫn chưa được gọi có phải là NEW hay không.

Để có thể xem được trạng thái của một Thread, chúng ta sẽ gọi đến phương thức getState(). Phương thức này được xây dựng sẵn ở lớp cha Thread.

Bạn hãy tạo mới một Thread nhé. Tạo bằng cách kế thừa từ lớp Thread hay implement từ interface Runnable cũng được. Hãy đặt tên Thread này là MyThread. Đây là cách mình xây dựng MyThread.

public class MyThread extends Thread {

	@Override
	public void run() {
		System.out.println("Thread Start");
	}
}

Để xem được trạng thái NEW này, ở phương thức main() bạn hãy khai báo MyThread rồi in ra ngay getState() mà không cần phải start() nó.

public static void main(String[] args) {
	MyThread myThread = new MyThread();
	System.out.println(myThread.getState());
}

Và đây là “thành phẩm”.

Kết quả bài thực hành số 1 - Vòng đời của thread

RUNNABLE

Trạng thái này xảy ra khi Thread đã được gọi phương thức start(). Ồ, bạn cũng nên biết một chút rằng không phải start() xong là Thread được chạy ngay đâu, nó còn phải chờ đợi hệ thống cấp phát tài nguyên xong xuôi thì mới bắt đầu chạy. Chính vì vậy mà bên trong trạng thái này dường như chia ra làm 2 trạng thái con, đó là, Ready to Run – Chờ đợi cấp phát tài nguyên, và Running – Đã chính thức chạy.

Bài Thực Hành Số 2

Bài này chúng ta sẽ thử gọi phương thức start() của MyThreadBài thực hành số 1 trên kia. Rồi cũng gọi đến phương thức getState() ngay sau đó để xem trạng thái của MyThread lúc này là gì nhé.

Code ở phương thức main() như sau.

public static void main(String[] args) {
	MyThread myThread = new MyThread();
	myThread.start();
	System.out.println(myThread.getState());
}

Kết quả in ra console.

Kết quả bài thực hành số 2 - Vòng đời của thread

Lưu ý rằng không phải lúc nào code trên đây cũng luôn in trạng thái RUNNABLE ra console đâu nhé. Vì sao vậy? Bạn có thể thấy rằng bên trong MyThread chỉ có in ra console chuỗi “Thread Start” thôi, sau khi in xong chuỗi này Thread sẽ kết thúc vòng đời của nó ngay. Do đó có trường hợp geState() ở phương thức main() sẽ gọi khi MyThread đã kết thúc rồi, nên RUNNABLE có thể sẽ không được in ra (mà là một trạng thái nào đó khác ở các mục sau bạn sẽ rõ) là vậy.

BLOCKED

Một Thread khi rơi vào trạng thái BLOCKED là khi nó không có đủ điều kiện để chạy. Không đủ điều kiện để chạy là như thế nào? Bạn có thể hiểu là, bản chất các Thread trong một ứng dụng đều có khả năng chạy song song khi chúng được start(). Như vậy thì sẽ xảy ra trường hợp cùng một thời điểm nào đó, sẽ có nhiều hơn một Thread đều có “mưu đồ” muốn chỉnh sửa một File hay một đối tượng nào đó, chúng ta gọi tắt các File hay các đối tượng bị “tranh chấp” này là các tài nguyên dùng chung. Nếu có sự tranh chấp này xảy ra, sẽ khiến cho ứng dụng bị lỗi, có thể dẫn đến mất mát dữ liệu hoặc các tính toán sai lầm. Do đó, trong Java có một cơ chế giúp điều khiển các Thread, cơ chế này đảm bảo một thời điểm nào đó chỉ có một Thread có thể can thiệp vào tài nguyên dùng chung mà thôi. Cơ chế này liên quan đến khái niệm Synchronization (đồng bộ hoá) mà chúng ta sẽ có một bài riêng về nó. Như vậy nếu có sự đồng bộ hoá này xảy ra, thì chỉ một Thread là được ưu tiên sử dụng đến tài nguyên dùng chung này, các Thread còn lại  bị khoá và phải đợi cho Thread ưu tiên kia sử dụng xong tài nguyên rồi mới được chạy, các Thread bị khoá này sẽ bị rơi vào trạng thái BLOCKED.

Như vậy để có thể nhìn thấy được trạng thái này, chúng ta sẽ làm quen trước một tí với kiến thức về Đồng bộ hoá ở bài thực hành sau, để rồi bài sau chúng ta cùng nhau nói kỹ hơn về nó nhé.

Bài Thực Hành Số 3

Để thực hành mục này, chúng ta hãy tạo ra một tài nguyên dùng chung, chính là một lớp nào đó, mình đặt tên lớp dùng chung này là DemoSynchronized. Trong lớp này có chứa một phương thức static có đánh dấu synchronized. Phương thức này có tên commonResource(). Bạn cũng đừng tập trung vào từ khoá synchronized quá, bài sau mình sẽ giải thích rõ. Bạn chỉ cần hiểu rằng phương thức commonResource() này được đánh dấu synchronized sẽ được hệ thống “bảo trợ” sao cho chỉ có một Thread được truy cập đến nó mà thôi.

Nào chúng ta cứ xây dựng trước lớp này nhé.

public class DemoSynchronized {

	public static synchronized void commonResource() {
		for (int i = 0; i < 100000; i++) {
			// Không làm gì cả, chỉ chạy vòng lặp để đảm
			// bảo phương thức này sống lâu một tí,
			// để cho có Thread dùng đến và các Thread
			// khác phải chờ đợi
		}
	}
}

Sau đó chúng ta để cho MyThread (đã code ở các bài thực hành trên đây) có cơ hội gọi đến commonResource(). Như sau.

public class MyThread extends Thread {

	@Override
	public void run() {
		DemoSynchronized.commonResource();
	}
}

Và rồi ở phương thức main(), chúng ta sẽ tạo nhiều hơn một đối tượng của MyThread, cụ thể là 2 đối tượng, bạn cũng có thể tạo ra 3, hay 4 MyThread để kiểm chứng. Sau khi tạo ra các MyThread, chúng ta đều cùng start() chúng, để chúng cùng lúc gọi đến commonResource() khi chạy. Sau đó bạn chỉ cần “ung dung” gọi getState() của chúng.

public class MainClass {
	
	public static void main(String[] args) {
		// Khai báo nhiều đối tượng của MyThread
		MyThread myThread1 = new MyThread();
		MyThread myThread2 = new MyThread();
		
		// Đều start() hết các đối tượng MyThread
		// để xem Thread nào sẽ được vào commonResource()
		myThread1.start();
		myThread2.start();
		
		// In ra các trạng thái của chúng
		System.out.println(myThread1.getName() + ": " + myThread1.getState());
		System.out.println(myThread2.getName() + ": " + myThread2.getState());
	}
}

Kết quả là, chỉ có một Thread lúc này là RUNNABLE thôi, còn lại sẽ đều là BLOCKED.

Kết quả bài thực hành số 3 - Vòng đời của thread

WAITING

Trạng thái này xảy ra khi một Thread phải đợi Thread nào đó hoàn thành tác vụ của nó, với một khoảng thời gian không xác định trước. Trạng thái này khác với trạng thái BLOCKED trên kia nhé, ở trên kia là các Thread bị hệ thống khoá lại khi cùng truy xuất chung đến một tài nguyên hệ thống. Còn trạng thái này là giữa các Thread tự điều đình với nhau. BLOCKED giống như các phương tiện bị chú cảnh sát giao thông chặn lại để nhường cho phương tiện được ưu tiên khác. Còn WAITING là tự các phương tiện tự nhường nhịn nhau, không cần phải có công an điều tiết ấy mà.

Do là các Thread sẽ nhường nhịn nhau, nên nếu một Thread nào đó có động thái gọi đến một trong các phương thức sau, nó sẽ “nhường” và tự rơi vào trạng thái WAITING này, các phương thức đó là.

Object.wait()
Thread.join()
LockSupport.park()

Cụ thể các phương thức trên đây là như thế nào thì mời các bạn đến bài học sau sẽ rõ. Bài hôm nay chúng ta thử thực hành với phương thức join(). Khi một Thread gọi đến phương thức join() của Thread khác, nó sẽ phải đợi Thread khác đó chết đi thì nó mới được thực hiện tiếp tác vụ còn lại của nó.

Lưu ý rằng các phương thức mình liệt kê trên đây không có tham số truyền vào nhé.

Chúng ta cùng đến với bài thực hành để hiểu rõ hơn về trạng thái này của Thread.

Bài Thực Hành Số 4

Trước hết chúng ta cùng chỉnh sửa một tí lớp MyRunnable như sau.

public class MyRunnable implements Runnable {

	@Override
	public void run() {
		System.out.println("MyRunnable Start");
		for (int i = 0; i < 100; i++) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		System.out.println("MyRunnable End");
	}

}

Code trên đây chưa liên quan gì đến việc đưa Thread vào trạng thái WAITING đâu nhé. Code này chỉ có in ra console cho thấy MyRunnable vừa được “Start”, rồi cho làm một việc nặng nặng nào đó, như lặp 100 lần, mỗi lần lặp sẽ ngủ 100 mili giây thôi. Cuối cùng sẽ in ra console cho thấy MyRunnable đã “End”.

Tiếp theo chúng ta đến với lớp MyThread. Ở MyThread này, khi được khởi chạy, chúng ta cố tình khai báo rồi khởi chạy MyRunnable luôn. Nhưng khi vừa mới khởi chạy MyRunnable, chúng ta gọi đến phương thức join() của nó. Điều này báo với hệ thống rằng, MyThread này sẽ đợi MyRunnable chạy hết (kết thúc vòng đời của MyRunnable) thì MyThread mới chạy tiếp. Bạn cứ code rồi khởi chạy nhé, đền bài học sau mình sẽ nói rõ hơn về phương thức join() này.

Đây là code của MyThread.

public class MyThread extends Thread {

	@Override
	public void run() {
		System.out.println("MyThread Start");
		Thread myRunnableThread = new Thread(new MyRunnable());
		myRunnableThread.start();
		
		try {
			myRunnableThread.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		System.out.println("MyThread End");
	}
}

Sau đó ở phương thức main() bạn chỉ cần khởi chạy MyThread, đợi khoảng 100 mili giây thì in trạng thái của MyThread ra console để xem chơi. Sở dĩ phải đợi một tí mới in trạng thái của MyThread là vì để đảm bảo MyThread có đủ thời gian để khởi chạy MyRunnable nữa, rồi thời gian mà MyThread vào WAITING nhường cho MyRunnable nữa, bạn in vội quá thì khó mà trông thấy trạng thái của MyThread.

public static void main(String[] args) {
	MyThread myThread = new MyThread();
		
	myThread.start();
		
	try {
		Thread.sleep(100);
		System.out.println("MyThread State: " + myThread.getState());
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
		
}

Kết quả in ra console như sau.

Kết quả bài thực hành số 4 - Vòng đời của thread

TIMED_WAITING

Cũng tương tự với WAITING trên kia thôi, nhưng khi này các phương thức khiến một Thread “nhường” cho một Thread khác thực thi có truyền vào đối số là khoảng thời gian mà Thread đó nhường. Các phương thức đó là.

Thread.sleep(long milis)
Object.wait(int timeout) hay Object.wait(int timeout, int nanos)
Thread.join(long milis)
LockSupport.parkNanos()
LockSupport.parkUtil()

Chà, phương thức sleep() quen thuộc lắm đúng không. Giờ thì bạn mới hiểu, rằng ở đâu đó trong Thread khi gọi đến Thread.sleep() này, thì Thread đó sẽ rơi vào trạng thái TIMED_WAITING“nhường” cho các Thread khác chạy trong khoảng thời gian mili giây chỉ định trước.

Tuy nhiên chúng ta cũng sẽ nói rõ các phương thức này ở bài học sau. Giờ thì xem code của các bài thực hành nào.

Bài Thực Hành Số 5

Với bài thực hành này bạn chỉ cần chỉnh sửa một chút so với Bài thực hành 4 trên kia. Cái nơi mà MyThread khởi chạy MyRunnable rồi gọi join() để nhường cho MyRunnable í, giờ bạn hãy truyền giá trị mili giây vào phương thức join() này. Nó có nghĩa rằng là tuy MyThread có nhường MyRunnable chạy trước đấy, nhưng chỉ nhường với một khoản thời gian đã chỉ định thôi, hết thời gian đó là tao chạy, đụng ai thì đụng nhé.

public class MyThread extends Thread {

	@Override
	public void run() {
		System.out.println("MyThread Start");
		Thread myRunnableThread = new Thread(new MyRunnable());
		myRunnableThread.start();
		
		try {
			myRunnableThread.join(500);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		System.out.println("MyThread End");
	}
}

Nếu bạn thực thi chương trình, hãy để ý kỹ, sau khi in ra “MyThread State: TIMED_WAITING” rồi, MyThead sẽ đợi MyRunnable trong khoảng thời gian (chưa tới) 500 mili giây còn lại, và sẽ in ra “MyThread End”. Vấn đề là MyRunnable vẫn chưa kết thúc vòng lặp, nên mãi sau nó mới kết thúc và in ra “MyRunnable End”. Bạn có thấy sự nhịp nhàng giữa các Thread không nào.

Kết quả bài thực hành số 5 - Vòng đời của thread

TERMINATED

Trạng thái này đánh dấu sự kết thúc vòng đời của Thread. Xảy ra khi Thread kết thúc hết các tác vụ bên trong phương thức run() của nó, hoặc có những kết thúc một cách không bình thường khác, như rơi vào Exception chẳng hạn.

Bài Thực Hành Số 6

Code của bài này không có gì nhiều. Bạn cứ lấy code của Bài thực hành số 5 trên kia. Rồi ở phương thức main(), bạn sleep() lâu lâu một tí, nhằm mục đích đợi cho MyThread kết thúc tác vụ của nó rồi thì in trạng thái của nó ra. Mình cho thời gian ngủ là 20 giây, như sau.

public static void main(String[] args) {
	MyThread myThread = new MyThread();
		
	myThread.start();
		
	try {
		Thread.sleep(20000);
		System.out.println("MyThread State: " + myThread.getState());
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
		
}

Kết quả in ra console như sau.

Kết quả bài thực hành số 6 - Vòng đời của thread

Phù! Chúng ta vừa đi qua kiến thức về vòng đời của một Thread, qua đó chúng ta biết được một Thread sẽ trải qua các trạng thái của nó như thế nào trong suốt đời sống của nó. Chắc bạn cũng biết rằng không phải lúc nào một Thread cũng trải qua cả đủ các trạng thái kể trên đâu nhé, có Thread chỉ NEW, RUNNABLE rồi TERMINATED thôi. Tuy nhiên qua kiến thức về vòng đời này, bạn cũng đã biết được sơ sơ cách các Thread đồng bộ hoá, nhường nhịn nhau để thực thi các tác vụ như thế nào rồi đúng không nào. Và kiến thức về Thread cũng còn khá nhiều và không kém phần thú vị. Hẹn các bạn ở các bài học sau.

Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy ủng hộ blog bằng cách:

Đánh giá 5 sao bên dưới mỗi bài nếu thấy thích.
Comment bên dưới mỗi bài nếu có thắc mắc.
Để lại địa chỉ email của bạn ở thanh bên phải để nhận được thông báo sớm nhất khi có bài viết mới.
Chia sẻ các bài viết của Yellow Code Books đến nhiều người khác.

 

Bài Kế Tiếp

Bài kế tiếp chúng ta sẽ nói cụ thể hơn về các phương thức bên trong một Thread mà bài hôm nay bạn đã có dịp làm quen một ít rồi đấy. Để xem các phương thức này giúp ích gì cho việc quản lý các Thread có trong hệ thống nhé.

Advertisements
Rating: 5.0/5. From 3 votes.
Please wait...

Gửi phản hồi