Được chỉnh sửa ngày 05/06/2021.
Chào mừng các bạn đã đến với bài học Java số 42, 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.
Sau khi tập 1 về Thread ra lò, mình nhận được nhiều chia sẻ và phản hồi từ các bạn. Mình cảm nhận được mối quan tâm rất lớn của các bạn với kiến thức này. Điều này thật sự thú vị. Thực ra mình cũng từng rất thích thú khi tiếp cận với Thread. Tuy chỉ là một kiến thức nhỏ nhoi trong biển kiến thức Java, nhưng Thread như mang đến một làn gió mới, một khả năng mới để chúng ta xây dựng các ứng dụng đa nhiệm, mạnh mẽ, thiết thực hơn, tận dụng tối đa hiệu năng của hệ thống hơn. Và đặc biệt hơn nữa, sau khi biết đến Thread là gì, thì chúng ta đã có thể bắt tay vào tìm hiểu các kiến thức về xây dựng một game viết bằng Java được rồi đấy.
Vậy hôm nay, chúng ta sẽ tiếp tục củng cố cái sự quan tâm đối với Thread bằng cách đi cụ thể hơn về nó, chúng ta sẽ nói đến các cách thức để khai báo và khởi tạo một Thread.
Tạo Một Thread
Ở bài hôm trước bạn cũng đã làm quen với một cách để tạo ra một Thread rồi. Nhưng mình mong muốn bài hôm nay bạn hãy… quên kiến thức bài trước đi, chúng ta cùng đi lại từ đầu cho nó hệ thống nào.
Trong Java, có hai cách để bạn tạo một Thread. Tuy cả hai cách đều giúp bạn tạo ra một Thread, nhưng mỗi cách lại có những mục đích và lợi ích khác nhau. Nhiệm vụ của bạn là phải biết rõ cả hai cách này. Tại sao phải biết cả hai cách? Ngoài việc bạn phải biết hết để có thể khai báo và sử dụng, bạn còn phải biết để còn đọc hiểu source code của người khác khi họ không dùng giống bạn nữa.
Vậy hai cách để tạo ra Thread là gì.
Cách 1 – Kế Thừa Từ Lớp Thread
Cách này ở bài hôm trước… ồ mình đã kêu các bạn quên đi rồi mà. Vậy thì, với cách này bạn làm như sau.
- Bạn tạo mới một lớp và kế thừa lớp này từ lớp cha Thread.
- Trong lớp mới tạo đó, bạn override phương thức run().
- Cuối cùng, ở nơi khác, khi muốn tạo ra một Thread từ lớp này, bạn khai báo đối tượng cho nó, rồi gọi đến phương thức start() của nó để bắt đầu khởi chạy Thread.
Thật đơn giản đúng không nào. Không ngờ kiến thức về Thread lại dễ đến vậy. Như bạn đã biết sơ qua từ bài hôm trước rằng, để khai báo một lớp là Thread, thì đơn giản chỉ kế thừa nó từ lớp cha Thread, chính phương thức run() bên trong lớp đó sẽ trở thành một Luồng xử lý bởi hệ thống khi đâu đó bên ngoài gọi đến phương thức start() của lớp này.
Chúng ta cùng đến với bài thực hành để hiểu rõ hơn.
Thực Hành Tạo Một Thread Bằng Cách Kế Thừa Từ Lớp Thread
Bài thực hành này chúng ta thử nghiệm tạo một Thread đếm ngược 10 giây. Khi start, Thread sẽ bắt đầu in ra console giá trị 10, mỗi một giây trôi qua Thread sẽ giảm con số này đi một đơn vị và lại in ra console, đến khi giảm đến giá trị 0 Thread sẽ in “Hết giờ”.
Bạn có thể sử dụng lại project đã tạo từ bài hôm trước, hôm nay bạn tạo một lớp mới có tên CountDownThread. Lớp này sẽ kế thừa từ lớp Thread như những gì mình đã nói ở các gạch đầu dòng trên kia như sau.
public class CountDownThread extends Thread { @Override public void run() { // Bước sau chúng ta sẽ code thêm } }
Một khung sườn cho Thread chỉ như vậy thôi. Như đã nói, CountDownThread khi được start sẽ bắt đầu đếm ngược từ 10 giây, đến 0 giây sẽ hiển thị chuỗi “Hết giờ”. Việc hiển thị số giây ra console thì bạn biết rồi, mình chỉ bật mí là để làm cho con số này chỉ được cập nhật và hiển thị ở mỗi giây thì chúng ta sử dụng phương thức Thread.sleep(1000). Phương thức này bạn đã làm quen từ bài hôm trước rồi. Mình nhắc lại một tí, nó sẽ giúp làm cho các Thread đang chạy trở nên “ngủ” trong một khoảng thời gian được tính bằng mili giây, trong trường hợp này chúng ta truyền vào 1000 mili giây, tức là 1 giây. Sau khi ngủ hết thời lượng cho phép, Thread sẽ “thức dậy” và thực hiện tiếp tác vụ của nó. Chú ý là bạn phải try catch phương thức Thread.sleep() này với một Checked Exception có tên InteruptedException. Và mình sẽ nói rõ về phương thức Thread.Sleep() ở bài sau nhé. Còn đây là code hoàn chỉnh của CountDownThread.
public class CountDownThread extends Thread { @Override public void run() { int count = 10; for (int i = count; i > 0; i--) { System.out.println(i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("Hết giờ"); } }
Để khởi chạy Thread vừa tạo thì chúng ta sẽ gọi phương thức start() của nó như sau.
public static void main(String[] args) { CountDownThread countDownThread = new CountDownThread(); countDownThread.start(); }
Còn đây là màn hình console của “bộ đếm giờ” mà chúng ta vừa tạo. Cứ mỗi một giây sẽ có một con số xuất hiện cho đến khi chữ “Hết giờ” xuất hiện cuối cùng sẽ là lúc kết thúc chương trình (và kết thúc cả CountDownThread).
Cách 2 – Impement Từ Interface Runnable
Nếu như cách trên kia thì bạn phải kế thừa từ lớp Thread, thì cách này bạn lại implement một interface có tên Runnable. Với cách này bạn làm như sau.
- Bạn tạo mới một lớp và implement lớp này với interface có tên Runnable.
- Trong lớp mới tạo đó, bạn override phương thức run().
- Cuối cùng, ở nơi khác, khi muốn tạo ra một Thread từ lớp này, trước hết bạn khai báo đối tượng cho nó, rồi bạn khai báo thêm một đối tượng của Thread nữa và truyền đối tượng của lớp này vào hàm khởi tạo của Thread. Khi phương thức start() của lớp Thread vừa tạo được gọi đến, thì phương thức run() bên trong lớp dẫn xuất của Runnable sẽ được gọi để tạo thành một Luồng trong hệ thống.
Nghe có vẻ phức tạp hơn cách thứ nhất trên kia đúng không nào. Nhưng bạn cũng nên thử qua cho biết bằng cách đến với bài thực hành sau.
Thực Hành Tạo Một Thread Bằng Cách Implement Từ Interface Runnable
Chúng ta vẫn sẽ xây dựng lại ví dụ về một Thread đếm ngược 10 giây trên kia bằng cách thứ 2 này.
Với cách này thì bạn chỉ cần chỉnh sửa một tí ở lớp CountDownThread của bạn, sao cho từ extends Thread sang implements Runnable là xong. Bạn hãy xem code sau sẽ rõ.
public class CountDownThread implements Runnable { @Override public void run() { int count = 10; for (int i = count; i > 0; i--) { System.out.println(i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("Hết giờ"); } }
Vấn đề khai báo một Thread không khác nhau lắm giữa hai cách đúng không nào. Khác biệt nhiều hơn sẽ nằm ở cách khởi chạy Thread. Code ở phương thức main() sẽ phải thay đổi như sau.
public static void main(String[] args) { CountDownThread countDownThread = new CountDownThread(); Thread thread = new Thread(countDownThread); thread.start(); }
Bạn hãy thực thi lại chương trình. Kết quả hai cách làm này đều cho ra kết quả như nhau cả.
Áp Dụng Kiến Thức Lớp Vô Danh Trong Việc Tạo Mới Một Thread
Nếu bạn đã quên lớp Vô Danh là lớp gì rồi, thì có thể đọc lại bài học ở link này.
Còn nếu bạn thắc mắc Thread thì liên quan gì đến lớp Vô Danh? Thì mình sẽ giải thích sơ qua thế này. Cơ bản thì Thread được xem là một cách gọn nhẹ cho hệ thống (và cả chúng ta) để thực thi các tác vụ song song. Và để làm cho sự gọn nhẹ đó càng thêm gọn nhẹ (về mặt quản lý code), thì việc kết hợp giữa Thread và lớp Vô Danh sẽ là giải pháp tốt cho ý này. Vì khi đó, chúng ta sẽ không cần thiết phải khai báo rõ ràng một lớp Thread nào cả, chỉ đơn giản là dựng lên một lớp Vô Danh, và start nó thôi.
Việc kết hợp giữa Thread và lớp Vô Danh là khá phổ biến, và người ta đã gộp 2 cái tên này lại thành một tên chung, gọi là Thread Vô Danh (Anonymous Threads).
Nào chúng ta cùng xem các cách sau để “vô danh hóa” một Thread. Mình dùng lại ví dụ Thread đếm ngược trên kia để bạn xem nhé.
Tạo Một Thread Vô Danh Từ Việc Kế Thừa Lớp Thread
Nào, chúng ta cùng tạo lại một Thread từ việc kế thừa lớp Thread, nhưng “vô danh hóa” nó như sau.
public static void main(String[] args) { Thread countDownThread = new Thread() { @Override public void run() { int count = 10; for (int i = count; i > 0; i--) { System.out.println(i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("Hết giờ"); } }; countDownThread.start(); }
Đấy, trên đây là một Thread Vô Danh. Đến đây sẽ có nhiều bạn thắc mắc rằng Thread Vô Danh thực chất có giúp làm gọn hơn cho việc quản lý code hay không. Thì mình có vài ý muốn trao đổi thêm như sau.
Thực ra việc sử dụng Thread Vô Danh so với Thread bình thường có làm code trở nên gọn hay không cũng tùy vào cách nhìn code của mỗi người thôi. Bạn xem, với bài thực hành xây dựng một Thread bình thường trên kia (mình sẽ gọi tắt Thread-bình-thường là Thread), bạn phải xây dựng một lớp CountDownThread.java hẳn hoi, code này có cái hay là rất tường minh. Còn với code ví dụ ở mục này, chúng ta đã tạo ra một đối tượng countDownThread không phải từ lớp CountDownThread hay từ lớp Thread, mà là từ một lớp Vô Danh kế thừa từ lớp Thread nhé. Cách khai báo này giúp giảm đi việc phải tạo ra một file Java nào khác, chúng ta chỉ đơn giản khai báo và dùng thôi, ngoài ra thì Thread Vô Danh còn dùng được các thành viên của lớp chứa nó nữa.
Điểm khác biệt nữa giữa việc khai báo một Thread và một Thread Vô Danh là, với Thread bạn có thể xây dựng constructor cho nó, nên bạn có thể truyền vào Thread các biến nào đó phục vụ cho logic của ứng dụng. Còn Thread Vô Danh thì không có constructor, nên bạn có thể phải dùng biến toàn cục của lớp khai báo.
Nhưng bạn cũng nên cân nhắc, dù cho cách sử dụng Thread Vô Danh khá là nhanh chóng và tiện lợi, chúng có thể sẽ làm code ở lớp sử dụng này phình lên, khó quản lý hơn nếu có quá nhiều Thread Vô Danh như thế này đấy nhé.
Quay lại kiến thức của Thread Vô Danh, với code trên đây, chúng ta còn có thể viết gọn hơn lại nữa cơ. Bằng việc không cần phải khai báo tên đối tượng, mà có thể start() luôn, như thế này.
public static void main(String[] args) { new Thread() { @Override public void run() { int count = 10; for (int i = count; i > 0; i--) { System.out.println(i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("Hết giờ"); } }.start(); }
Tạo Một Thread Vô Danh Bằng Cách Implement Từ Interface Runnable
Nếu bạn hiểu Thread Vô Danh từ cách kế thừa lớp Thread trên kia, thì việc tạo một Thread Vô Danh từ interface Runnable có lẽ bạn cũng có thể tự viết được.
public static void main(String[] args) { Runnable countDownThread = new Runnable() { @Override public void run() { int count = 10; for (int i = count; i > 0; i--) { System.out.println(i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("Hết giờ"); } }; Thread thread = new Thread(countDownThread); thread.start(); }
Code này cũng có thể viết ngắn gọn hơn bằng cách bỏ đi khai báo đối tượng từ lớp Thread như sau.
public static void main(String[] args) { Runnable countDownThread = new Runnable() { @Override public void run() { int count = 10; for (int i = count; i > 0; i--) { System.out.println(i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("Hết giờ"); } }; new Thread(countDownThread).start(); }
Hoặc có thể ngắn gọn hơn nữa khi không cần khai báo đối tượng của Thread Vô Danh. Nhưng khi này bạn phải truyền lớp Vô Danh này vào Thread như là một tham số. Như sau.
public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { int count = 10; for (int i = count; i > 0; i--) { System.out.println(i); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("Hết giờ sss"); } }).start(); }
Hi vọng kiến thức về Thread Vô Danh không làm bạn quá đau đầu. Mình mời các bạn cùng đến với bài tập sau đây để có thể “quen tay” hơn trong việc tạo ra một Thread.
Bài Tập 1: Tạo Trò Chơi 2 Thread Cùng Đoán Số
Yêu cầu bài tập như sau. Bạn hãy tạo ra một trò chơi để người dùng có thể nhập vào một số nguyên trong khoảng từ 1 đến 100. Sau đó bạn xây dựng một Thread đoán số, Thread này sẽ đạo ra các con số random cũng trong khoảng 1 đến 100 đó. Cứ mỗi lần random được một số, Thread sẽ in ra console cho người chơi có thể nhìn thấy. Thread sẽ dừng lại khi random ra một số trùng với số mà người chơi vừa nhập, đồng thời in ra số lần “đoán” để ra được con số đó.
Lưu ý rằng, có 2 Thread cùng đoán số, để “thi thố” xem Thread nào “đoán” ra con số của người chơi nhanh nhất.
Để dễ hình dung hơn, mình đưa ra kết quả console dự kiến của trò chơi sẽ như thế này. Kết quả này dựa trên sự “đoán” cật lực của 2 Thread, để có thể biết được người chơi đã nhập vào con số 15. Và như hình thì Thread 2 đã thắng với 68 lần đoán. Thread 1 “kém thông minh” hơn và tiếp tục đoán cho đến lần đoán thứ 360.
Mình có hai gợi ý để bạn chỉ tập trung vào code cho Thread thôi, đỡ phải lăn tăn tìm hiểu code khác trên mạng.
- Để random một con số từ 1 đến 100, bạn code:
randomNumber = (int) (Math.random() * 100 + 1);
- Thread cha có một phương thức setName() để bạn đặt tên cho Thread đang chạy, vì vậy bạn có thể tận dụng để đặt các tên “Thread 1”, “Thread 2”. Rồi bạn có thể in ra console bằng cách gọi đến tên đã đặt bằng phương thức getName().
Xong rồi, mời bạn code.
Sau khi code xong, bạn có thể so sánh với đáp án của mình. Đầu tiên là Thread đoán số, mình đặt tên nó là GuessANumberThread.
public class GuessANumberThread extends Thread { private int guessNumber = 0; private int count = 0; public GuessANumberThread(int guessNumber) { this.guessNumber = guessNumber; } @Override public void run() { int randomNumber = 0; do { randomNumber = (int) (Math.random() * 100 + 1); count++; System.out.println(getName() + " đoán số " + randomNumber); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } while (randomNumber != guessNumber); System.out.println(getName() + " đã đoán ra số " + guessNumber + " trong " + count + " lần đếm"); } }
Còn đây là nơi kêu người dùng nhập vào một con số cần đoán, và tạo ra 2 Thread để thi thố. Nơi này chính là phương thức main().
public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("Nhập một số nguyên để các thread đoán: "); int number = scanner.nextInt(); GuessANumberThread thread1 = new GuessANumberThread(number); GuessANumberThread thread2 = new GuessANumberThread(number); thread1.setName("Thread 1"); thread2.setName("Thread 2"); thread1.start(); thread2.start(); }
Kết quả thực thi chương trình sẽ như hình trên kia.
Bài Tập 2: Làm Lại Bài Tập 1 Với Runnable
Bạn hãy thử code lại trò chơi đoán số trên đây bằng Thread implement từ Runnable nhé.
Chỉ có một lưu ý cho bạn dễ code rằng, để có thể gọi đến tên của Thread implement từ Runnable, thì không thể cứ gọi getName() như Bài tập 1 được, mà bạn phải gọi Thread.currentThread().getName().
Bài Tập 3: Làm Lại Bài Tập 1 Với Thread Vô Danh
Lần này bạn hãy code lại trò chơi đoán số này bằng Thread Vô Danh xem sao nhé.
Đây là cách mình làm, cách của các bạn thế nào.
public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("Nhập một số nguyên để các thread đoán: "); int guessNumber = scanner.nextInt(); Runnable anonymousGuessANumber = new Runnable() { @Override public void run() { int randomNumber = 0; int count = 0; do { randomNumber = (int) (Math.random() * 100 + 1); count++; System.out.println(Thread.currentThread().getName() + " đoán số " + randomNumber); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } while (randomNumber != guessNumber); System.out.println(Thread.currentThread().getName() + " đã đoán ra số " + guessNumber + " trong " + count + " lần đếm"); } }; Thread thread1 = new Thread(anonymousGuessANumber); thread1.setName("Thread 1"); Thread thread2 = new Thread(anonymousGuessANumber); thread2.setName("Thread 2"); thread1.start(); thread2.start(); }
Trên đây là tất cả các cách để bạn có thể tạo ra một Thread. Các bạn thấy thế nào, Thread có dễ dùng hay không. Hãy để lại comment bên dưới bài học nếu bạn có bất cứ thắc mắc nào nhé.
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ẽ đến với các kiến thức về vòng đời của Thread, và các cách mà Thread “liên lạc” với nhau, cũng như “nhìn nhau” mà chạy là như thế nào nhé.
Để xây dựng một chương trình từ đơn giản đến phức tạp thì mình cần làm những bước như thế nào? Làm sao để hình dung một cách dễ dàng toàn bộ chương trình?
Để xây dựng một chương trình phức tạp, thì có nhiều điều để nói lắm bạn. Nó thuộc về lĩnh vực thiết kế phần mềm rồi, bạn có thể đọc thêm nhiều sách vở có nói nhiều đến kiến thức về phát triển phần mềm này. Các bài viết của mình chỉ mang tính liệt kê các kỹ thuật cần thiết trong một phần nhỏ của quá trình phát triển phần mềm thôi. Để có thể phát triển một phần mềm phức tạp bạn phải trải qua các khâu như lên ý tưởng, thiết kế, thực hiện, test, hoàn thiện và phân phối sản phẩm. Bạn hãy tự tìm hiểu nhé.
cho em hỏi là bây giờ em muốn nó count thời gian như kiểu 2:00 …. 2:01….2:02 thì làm kiểu j ạ ? em cảm ơn
Chào bạn. Trường hợp của bạn thì cũng được, nhưng khi này sẽ cần đến lớp Calendar để chuyển từ mili giây sang định dạng giờ, phút, giây. Và SimpleDateFormat để hiển thị kết quả ra console theo đúng format chúng ta cần, chẳng hạn mm:ss như bạn muốn. Khi đó lớp CountDownThread hơi rườm rà tí như sau. Lưu ý là khi này biến count cũng chạy theo mili giây luôn nhé, để Calendar có thể chuyển đổi được các tham số.
public class CountDownThread extends Thread {
@Override
public void run() {
final Calendar cal = Calendar.getInstance();
int count = 120000; // 2 minute
for (int i = count; i > 0; i-=1000) {
cal.setTimeInMillis(i);
final String timeString = new SimpleDateFormat(“mm:ss”).format(cal.getTime());
System.out.println(timeString);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(“Hết giờ”);
}
}
Cho mình hỏi: trong bài tập bài Tập 1: Tạo Trò Chơi 2 Thread Cùng Đoán Số
Có đoạn Thread.sleep(500) mình cảm thấy đoạn này nó ko thật sự cần cho lắm có phải ko, nếu bỏ đoạn Thread.sleep(500) đi thì chương trình sẽ như thế nào? Mình cảm thấy bỏ dòng đó đi thì chương trình vẫn chạy ổn hay có gì khác ko bạn?
Bỏ dòng đó thì chương trình vẫn chạy đúng thôi bạn. Có nó để thêm phần hồi hộp thôi :))
khi nào thì nên sử dụng khởi tạo theo cách 1 và theo cách 2 vậy anh nhỉ