Hướng dẫn lập trình đa luồng trong Java - Java Multithreading
Công ty Vĩnh Cửu tuyển dụng lập trình viên Java

1- Nguyên tắc hoạt động của luồng

2- Ví dụ bắt đầu với Thread

Chúng ta cần 2 class tham gia vào ví dụ này.
  • HelloMain là một class thông thường có hàm main, nó là một luồng chính (main thread).
  • HelloThread là một class mở rộng từ class Thread. Nó được tạo và chạy kích hoạt chạy bên trong luồng chính và sẽ chạy song song với luồng chính.
package org.o7planning.tutorial.thread.hellothread;

public class HelloMain {

   public static void main(String[] args) throws InterruptedException {

       int idx = 1;

       for (int i = 0; i < 2; i++) {

           System.out.println("Main thread running " + idx++);
           // Ngủ 2101 milli giây.
           Thread.sleep(2101);
       }

       HelloThread helloThread = new HelloThread();

       // Chạy thread
       helloThread.start();

       for (int i = 0; i < 3; i++) {
           System.out.println("Main thread running " + idx++);
           // Ngủ 2101 milli giây.
           Thread.sleep(2101);
       }

       System.out.println("==> Main thread stopped");
   }
}
HelloThread.java
package org.o7planning.tutorial.thread.hellothread;

public class HelloThread extends Thread {

   // Code trong hàm run() sẽ được thực thi khi
   // thread được chạy (start)
   @Override
   public void run() {
       int index = 1;

       for (int i = 0; i < 10; i++) {
           System.out.println("  - HelloThread running " + index++);

           try {
               // Ngủ 1030 milli giây.
               Thread.sleep(1030);
           } catch (InterruptedException e) {
           }

       }
       System.out.println("  - ==> HelloThread stopped");
   }
}
Kết quả chạy class HelloMain:

3- Interface Runnable

Bạn cũng có thể tạo một Thread từ một class thi hành interface Runnable. Xem ví dụ minh họa:
RunnableDemo.java
package org.o7planning.tutorial.thread.runnable;

public class RunnableDemo implements Runnable {

   @Override
   public void run() {
       int idx = 1;
       for (int i = 0; i < 5; i++) {
           System.out.println("Hello from RunnableDemo " + idx++);
           // Ngủ 2 giây.
           try {
               Thread.sleep(2000);
           } catch (InterruptedException e) {
           }
       }
   }

}
RunnableTest.java
package org.o7planning.tutorial.thread.runnable;

public class RunnableTest {

   public static void main(String[] args) throws InterruptedException {

       System.out.println("Main thread running..");

       // Tạo một thread từ Runnable.
       Thread thread = new Thread(new RunnableDemo());

       thread.start();

       // Ngủ 5 giây.
       Thread.sleep(5000);
       System.out.println("Main thread stopped");
   }
}
Chạy class RunnableTest:

4- Luồng Deamon

Java chia Thread làm 2 loại một loại thông thường và Deamon Thread. Chúng chỉ khác nhau ở cách thức ngừng hoạt động. Trong một chương trình các luồng thông thường và Deamon chạy song song với nhau. Khi tất cả các luồng thông thường kết thúc, mọi luồng Deamon cũng sẽ bị kết thúc theo bất kể nó đang làm việc gì.
Chú ý:

Sử dụng setDeamon(boolean) để sét đặt một luồng là Deamon hoặc không. Chú ý, bạn chỉ có thể gọi hàm setDeamon(boolean) khi thread chưa được chạy. Điều đó có nghĩa là khi thread đã chạy bạn không thể chuyển luồng từ non-deamon sang deamon và ngược lại.

Khi một luồng mới được tạo ra, nó được thừa hưởng đặc tính deamon từ luồng cha.  Như vậy khi bạn tạo một luồng trong hàm main của 1 class nó vốn là luồng non-deamon, vì vậy thread tạo ra mặc định cũng là none-deamon. Như vậy nếu bạn tạo một luồng mới trong một luồng Deamon, mặc định nó cũng sẽ là Deamon.
Thread thread = new MyThread();

// sét luồng này là deamon.
// Chỉ gọi được method này khi thread chưa start.
// Trong trường hợp start rồi sẽ bị một ngoại lệ.
thread.setDeamon(true);

// Sét luồng này là non-deamon.
// Chỉ gọi được method này khi thread chưa start.
// Trong trường hợp start rồi sẽ bị một ngoại lệ.
thread.setDeamon(false);
Để dễ hiểu chúng ta xem ví dụ sau, chúng ta cần 3 class tham gia vào minh họa:
NoneDeamonThread.java
package org.o7planning.tutorial.thread.deamon;

public class NoneDeamonThread extends Thread {

  @Override
  public void run() {
      int i = 0;

      // Vòng lặp 10 lần. Luồng này sẽ kết thúc.
      while (i < 10) {
          System.out.println("  - Hello from None Deamon Thread " + i++);
          try {
              // Ngủ 1 giây.
              Thread.sleep(1000);
          } catch (InterruptedException e) {
          }
      }

      // Ghi ra thông báo luồng này kết thúc.
      System.out.println("\n==> None Deamon Thread ending\n");
  }
}
DeamonThread.java
package org.o7planning.tutorial.thread.deamon;

class DeamonThread extends Thread {

  @Override
  public void run() {
      int count = 0;
     
      // Vòng lặp vô tận.
      while (true) {
          System.out.println("+ Hello from Deamon Thread " + count++);
          try {
              // Ngủ 2 giây.
              sleep(2000);
          } catch (InterruptedException e) {
          }
      }
  }
}
DaemonTest.java
package org.o7planning.tutorial.thread.deamon;

public class DaemonTest {

   public static void main(String[] args) {
       System.out.println("==> Main Thread running..\n");
       // Tạo một Thread
       Thread deamonThread = new DeamonThread();
       // Sét nó là Deamon Thread.
       deamonThread.setDaemon(true);
       deamonThread.start();

       // Tạo một Thread khác
       new NoneDeamonThread().start();

       try {
           // Ngủ 5 giây.
           Thread.sleep(5000);
       } catch (InterruptedException e) {
       }
       
       // Ghi ra thông báo luồng main này kết thúc.
       System.out.println("\n==> Main Thread ending\n");
   }

}
Kết quả chạy class DeamonTest:
Hình minh họa trên cho thấy rằng luồng Deamon đã bị dừng lại khi tất cả các luồng thông thường đã dừng. Mặc dù code của nó là chạy vô tận.

Luồng deamon thường dùng làm gì?

Một trong các luồng Deamon quan trọng của Java đó là luồng gom rác, nghĩa là gom các tài nguyên không còn sử dụng để giải phóng bộ nhớ. Khi tất cả các luồng người dùng không còn hoạt động nữa luồng gom rác cũng bị dừng theo.

5- Sử dụng join() & join(long)

Thread.join() là một method thông báo rằng hãy chờ thread này hoàn thành rồi thread cha mới được tiếp tục chạy.
// Thread cha cần phải đợi cho tới khi luồng này kết thúc
// mới được chạy tiếp.
// (Nó tương đương với gọi join(0) )
public final void join() throws InterruptedException;

// Thread cha cần phải đợi 'millis' milli giây mới được tiếp tục chạy.
// kể từ lúc gọi join(long).
// Nếu tham số millis = 0 nghĩa là đợi cho tới khi luồng này kết thúc.
public final synchronized void join(long millis) throws InterruptedException;


// Thread cha cần phải đợi 'millis' milli giây và 'nanos' nano giây  mới được tiếp tục chạy.
// kể từ lúc gọi join(long,int).
// Nếu tham số millis = 0 & nanos = 0 nghĩa là đợi cho tới khi luồng này kết thúc.
// 1 giây = 1000000 nano giây.
public final synchronized void join(long millis, int nanos) throws InterruptedException;
Hãy xem một ví dụ minh họa:
JoinThread.java
package org.o7planning.tutorial.thread.join;

public class JoinThread extends Thread {
  private String threadName;
  private int count;

  public JoinThread(String threadName, int count) {
      this.threadName = threadName;
      this.count = count;
  }

  @Override
  public void run() {

      for (int i = 1; i < count + 1; i++) {
          System.out.println("Hello from " + this.threadName + " " + i);
          try {
              Thread.sleep(2000);
          } catch (InterruptedException e) {
          }
      }
      System.out.println("\n==> Thread " + threadName + " end!\n");
  }
}
JoinTest.java
package org.o7planning.tutorial.thread.join;

public class JoinTest {

   public static void main(String[] args) throws InterruptedException {

       System.out.println("\n==> Main thread starting..\n");

       Thread joinThreadA = new JoinThread("A*", 2);
       Thread joinThreadB = new JoinThread("B*", 3);

       // Thread thông thường, sẽ không sử dụng join().
       Thread noJoinThreadC = new JoinThread("C", 5);

       joinThreadA.start();
       joinThreadB.start();
       noJoinThreadC.start();
       // Sử dụng join()
       joinThreadA.join();
       joinThreadB.join();

       // Đoạn code dưới đây sẽ phải chờ cho tới khi 2
       // joinThread A,B hoàn thành, mới được chạy tiếp.
       System.out.println("Hello from main thread...");

       System.out.println("Thread A isLive? " + joinThreadA.isAlive());
       System.out.println("Thread B isLive? " + joinThreadB.isAlive());
       System.out.println("Thread C isLive? " + noJoinThreadC.isAlive());

       System.out.println("\n==> Main Thread end!\n");
   }
}
Kết quả chạy class JoinTest:
Ví dụ sử dụng join(long millis):
JoinTest2.java
package org.o7planning.tutorial.thread.join;

public class JoinTest2 {

   public static void main(String[] args) throws InterruptedException {

       System.out.println("\n==> Main thread starting..\n");

       Thread joinThreadA = new JoinThread("A*", 5);

       joinThreadA.start();
       // Luồng cha (main) phải chờ 5000 milli giây
       // mới được tiếp tục chạy. (Không nhất thiết phải A kết thúc)
       joinThreadA.join(5000);

       System.out.println("Main thread after 5000 milli second");
       System.out.println("Hello from main thread...");

       System.out.println("Thread A isLive? " + joinThreadA.isAlive());

       System.out.println("\n==> Main Thread end!\n");
   }

}
Kết quả chạy ví dụ:

6- Sử lý ngoại lệ cho luồng

Phương thức Thread.setDefaultUncaughtExceptionHandler() thiết lập mặc định xử lý khi luồng đột ngột chấm dứt do một ngoại lệ còn tự do, mà không có xử lý khác đã được xác định cho luồng đó.
ThreadExceptionDemo.java
package org.o7planning.tutorial.thread.exception;

import java.util.Random;

public class ThreadExceptionDemo {

   public static class RunnableTest implements Runnable {

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

           while (true) {
               Random r = new Random();
               // Một số ngẫu nhiên từ 0 - 99
               int i = r.nextInt(100);
               System.out.println("Next value " + i);

               try {
                   Thread.sleep(2000);
               } catch (InterruptedException e) {
               }

               if (i > 70) {
                   // Mô phỏng một ngoại lệ đã không được sử lý trong luồng.
                   throw new RuntimeException("Have a problem...");
               }
           }
       }

   }

   public static void main(String[] args) {
       System.out.println("==> Main thread running...");

       Thread thread = new Thread(new RunnableTest());
       Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {

           @Override
           public void uncaughtException(Thread t, Throwable e) {
               System.out.println("#Thread: " + t);
               System.out.println("#Thread exception message: " + e.getMessage());
           }
       });

       thread.start();
       System.out.println("==> Main thread end...");
   }

}
Kết quả chạy ví dụ:

7- Sử dụng yield()

Về mặt lý thuyết, "yield" có nghĩa là để cho đi, từ bỏ, đầu hàng. Một luồng yield nói với máy ảo là nó sẵn sàng để cho các thread khác được sắp xếp ở vị trí của nó. Điều này cho thấy rằng nó không phải làm một cái gì đó quá quan trọng. Lưu ý rằng nó chỉ là một gợi ý, mặc dù, và không đảm bảo có hiệu lực ở tất cả.

yield() được định nghĩa như sau trong Thread.java


public static native void yield();

8- So sánh sleep() và wait()

TODO.