Được chỉnh sửa ngày 12/06/2021.
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ơ đồ 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. À nhiều bạn hỏi mình chỗ này, nên mình bổ sung luôn, với Eclipse hay InteliJ, khi muốn vào xem source code của một lớp trong thư viện, bạn hãy nhấn giữ Ctrl đối với Windows (Cmd đối với Mac) rồi click vào lớp cần xem nhé, IDE sẽ dẫn bạn sang một tab mới với source code đầy đủ.
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.
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 đó.
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.
Cuối cùng, khi một Thread kết thúc, nó đến trạng thái TERMINATED.
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”.
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 MyThread ở Bà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.
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ẽ nói đến ở Bài 45. 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 chúng ta sẽ nói kỹ hơn về nó sau 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.
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()
Một số phương thức trên đây sẽ được nói ở bài học sau. Còn 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 đó hoàn thành xong thì nó mới được thực hiện tiếp tác vụ còn lại của nó. Cụ thể về join() mình có viết ra ở đây cho bạn tìm hiểu kỹ hơn.
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.
Đâ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.
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) hay Thread.sleep(long milis, int nanos)
- Object.wait(int timeout) hay Object.wait(int timeout, int nanos)
- Thread.join(long milis) hay Thread.join(long milis, int nanos)
- 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 và “nhường” cho các Thread khác chạy trong khoảng thời gian mili giây chỉ định trước.
Bạn có thể xem một vài phương thức đã được mình liệt kê cụ thể ở bài viết này.
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.
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 Luận
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 ở mỗi bài viết nếu thấy thích.
– Comment bên dưới mỗi bài viết 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.
– Ủng hộ blog theo hướng dẫn ở thanh bên phải để blog ngày càng phát triển hơn.
Bài Kế Tiếp
Bài kế tiếp chúng ta sẽ vận dụng các định nghĩa, các phương thức, và thế mạnh của Thread để bước vào một lĩnh vực mới, gọi là Đồng bộ hóa Thread.
ở bài thực hành 3 em tạo đến 10 MyThread mà sao cái nào cũng có trạng thái RUNNABLE vậy ạ?
Bạn phải đảm bảo các điều kiện sau. Thứ nhất, phương thức commonResource() của lớp DemoSynchronized phải được khai báo kèm từ khóa synchronized. Thứ hai, bên trong phương thức commonResource() này vòng lặp phải chạy thật nhiều, như bài thực hành số 3 lặp đến 100000 lần lận, để nó “giữ chân” một Thread đủ lâu. Thứ ba, phương thức này phải là static để đảm bảo tính dùng chung của các Thread. Nếu bạn đảm bảo cả 3 điều trên mà các Thread đều là RUNNABLE cả, thì chỉ có thể giải thích là vì computer của bạn xử lý quá nhanh mà thôi, vòng lặp 100000 lần mà nó chỉ chạy trong tích tắc là xong :))
Anh ơi, em thực hành ở phần vòng đời “Blocked” thì không phải như kết quả của anh.
“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.”
==> của em nó có khi 2 thằng runable, có khi 1 thằng runable, thằng còn lại terminated, cũng có khi có kết quả như anh ạ. Tại sao ? Mong anh giải thích giùm em nhé.!
Có thể các Thread bạn khởi tạo đều đã sử dụng tài nguyên dùng chung của ví dụ và đều kết thúc vòng đời của nó cả rồi, nên bạn hoặc là nhìn thấy RUNNABLE hoặc là TERMINATED là vậy. Để nhìn thấy 1 Thread RUNNABLE và các Thread còn lại là BLOCK, thì bạn phải đảm bảo có từ khóa synchronized ở phương thức static của lớp dùng chung, và trong lớp dùng chung này vòng lặp phải thật là lớn, như mình để con số 100.000 để đảm bảo một Thread vào sử dụng phương thức này nó phải ở đó đủ lâu để các Thread còn lại buộc phải chờ, thì mới thấy trạng thái BLOCK bạn nhé.
chỉnh sửa lại class DemoSynchronization như sau, dành cho máy của bạn quá mạnh
____________________________________________________________________
public class DemoSynchronization {
public static synchronized void commonResource() {
long millisecond1 = System.currentTimeMillis();
while (true) {
long millisecond2 = System.currentTimeMillis();
long result = millisecond2 – millisecond1;
if (result == 10000) {
break;
}
}
}
}
____________________________________________________________________
Bài viết của a hay thực sự ạ
Cảm ơn a nhiềuuuu
Mong a có thể tiếp tục ra bài viết nhiều và đều đặn hơn
Chúc a thành công trong cuộc sống
^^
Ở khúc TIME_WAITING, vì sao ad nói là chưa tới 500mili v ạ?
“MyThead sẽ đợi MyRunnable trong khoảng thời gian (chưa tới) 500 mili giây còn lại”
Chào bạn, mình đã đọc kỹ lại code, có thể có nhầm lẫn ở đây, để mình tính toán lại thời gian rồi có gì chỉnh sửa bài viết để cho kết quả thời gian chính xác hơn nhé. Nhưng trước mắt các bạn cứ xem đây là ví dụ để có thể nhìn thấy trạng thái TIME_WAITING của Thread.
cực kỳ chi tiết và dễ hiểu, giá như biết đến Yellow Code Books sớm hơn thì tốt biết mấy :((