openplanning

Hướng dẫn lập trình đa luồng trong Java - Java Multithreading

  1. Nguyên tắc hoạt động của Thread
  2. Ví dụ bắt đầu với Thread
  3. Runnable Interface
  4. Luồng Deamon (Deamon Thread)
  5. Sử dụng join() & join(long)
  6. Xử lý ngoại lệ cho Thread
  7. Sử dụng yield()
  8. So sánh sleep() và wait()

1. Nguyên tắc hoạt động của Thread

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. Runnable Interface

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:
Main thread running..
Hello from RunnableDemo 1
Hello from RunnableDemo 2
Hello from RunnableDemo 3
Main thread stopped
Hello from RunnableDemo 4
Hello from RunnableDemo 5

4. Luồng Deamon (Deamon Thread)

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à luồng 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 (Garbage Collection Thread), 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:
==> Main thread starting..

Hello from A* 1
Hello from C 1
Hello from B* 1
Hello from A* 2
Hello from C 2
Hello from B* 2

==> Thread A* end!

Hello from B* 3
Hello from C 3

==> Thread B* end!

Hello from C 4
Hello from main thread...
Thread A isLive? false
Thread B isLive? false
Thread C isLive? true

==> Main Thread end!

Hello from C 5

==> Thread C end!
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ụ:
==> Main thread starting..

Hello from A* 1
Hello from A* 2
Hello from A* 3
Main thread after 5000 milli second
Hello from main thread...
Thread A isLive? true

==> Main Thread end!

Hello from A* 4
Hello from A* 5

==> Thread A* end!

6. Xử lý ngoại lệ cho Thread

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ụ:
==> Main thread running...
==> Main thread end...
Thread running ..
Next value 21
Next value 42
Next value 67
Next value 18
Next value 34
Next value 40
Next value 11
Next value 1
Next value 92
#Thread: Thread[Thread-0,5,main]
#Thread exception message: Have a problem...

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();
Như vậy phương thức yield() được sử dụng khi bạn bạn thấy rằng thread đó đang rảnh rỗi, nó không phải làm việc gì quan trọng, nên nó gợi ý hệ điều hành tạm thời nhường quyền ưu tiên cho các luồng khác.
Ví dụ dưới đây, có 2 luồng, mỗi luồng in ra một dòng văn bản 100K lần (con số đủ lớn để thấy sự khác biệt). Một luồng được sét độ ưu tiên cao nhất và một luồng được sét độ ưu tiên ít nhất. Đo khoảng thời gian kết thúc của 2 luồng.
YieldThreadExample.java
package org.o7planning.tutorial.thread.yield;

import java.util.Date;

public class YieldThreadExample {

	private static Date importantEndTime;
	private static Date unImportantEndTime;

	public static void main(String[] args) {
		importantEndTime = new Date();
		unImportantEndTime = new Date();

		System.out.println("Create thread 1");

		Thread importantThread = new ImportantThread();

		// Sét đặt độ ưu tiên cao nhất cho thread này.
		importantThread.setPriority(Thread.MAX_PRIORITY);

		System.out.println("Create thread 2");

		Thread unImportantThread = new UnImportantThread();

		// Sét đặt độ ưu tiên thấp nhất cho thread này.
		unImportantThread.setPriority(Thread.MIN_PRIORITY);

		// Start threads.
		unImportantThread.start();
		importantThread.start();

	}

	// Một nhiệm vụ quan trọng, yêu cầu độ ưu tiên cao.
	static class ImportantThread extends Thread {

		@Override
		public void run() {
			for (int i = 0; i < 100000; i++) {
				System.out.println("\n Important work " + i);

				// Thông báo với hệ điều hành,
				// thread này nhường quyền ưu tiên cho các luồng khác.
				Thread.yield();
			}

			// Thời điểm kết thúc của thread này.
			importantEndTime = new Date();
			printTime();
		}

	}

	static class UnImportantThread extends Thread {
		@Override
		public void run() {
			for (int i = 0; i < 100000; i++) {
				System.out.println("\n  -- UnImportant work " + i);
			}
			// Thời điểm kết thúc của thread này.
			unImportantEndTime = new Date();
			printTime();
		}
	}

	private static void printTime() {
		// Khoảng thời gian (Mili giây).
		long interval = unImportantEndTime.getTime() - importantEndTime.getTime();

		System.out.println("UnImportant Thread - Important Thread = " //
				+ interval + " milliseconds");
	}

}
Kết quả: thread có ưu tiên thấp hơn đã hoàn thành công việc trước 51 mili giây so với thread có ưu tiên cao hơn.
...

 Important work 99995

 Important work 99996

 Important work 99997

 Important work 99998

 Important work 99999
UnImportant Thread - Important Thread = -269 milliseconds

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

TODO.

Java cơ bản

Show More