Giải pháp chuyển đổi số trọn gói cho doanh nghiệp Việt
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.
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ì.
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.
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ơ đồ 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 đủ.
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õ.
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.
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 đầ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.
Để 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ó.
Và đây là “thành phẩm”.
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 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.
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.
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é.
Để 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é.
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.
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.
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.
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à.
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.
Trước hết chúng ta cùng chỉnh sửa một tí lớp MyRunnable như sau.
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.
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.
Kết quả in ra console như sau.
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à.
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.
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é.
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.
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.
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.
Kết quả in ra console như sau.
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.
Chưa có câu hỏi nào cho bài này.