+16

Thread, Thread Pool, Thread Notify trong Java

Giới thiệu

Trong bài viết này chúng ta cùng tìm hiểu về Thread, Multi Thread, Daemon Thread, Deadlock, Life Cycle.... hiểu và vận dụng thread vô thực tế. GetGoooo.....

  • Nội dung của bài viết được hỗ trợ 1 phần bởi ChatGPT

Định nghĩa

Thread là một thuộc tính duy nhất của Java. Nó là đơn vị nhỏ nhất của đoạn mã có thể thi hành được mà thực hiện một công việc riêng biệt. Ngôn ngữ Java và máy ảo Java cả hai là các hệ thống đươc phân luồng.

Multi Thread

  • Java hổ trợ đa luồng, mà có khả năng làm việc với nhiều luồng. Một ứng dụng có thể bao hàm nhiều luồng
  • Đa luồng giữ thời gian nhàn rỗi của hệ thống thành nhỏ nhất( tức vắt kiệt luôn =))). Điều này cho phép bạn viết các chương trình có hiệu quả cao với sự tận dụng CPU là tối đa. Mỗi phần của chương trình được gọi một luồng, mỗi luồng định nghĩa một đường dẫn khác nhau của sự thực hiện. Đây là một thiết kế chuyên dùng của sự đa nhiệm.
  • Trong sự đa nhiệm, nhiều chương chương trình chạy đồng thời, mỗi chương trình có ít nhất một luồng trong nó. Một vi xử lý thực thi tất cả các chương trình. Cho dù nó có thể xuất hiện mà các chương trình đã được thực thi đồng thời, trên thực tế bộ vi xử lý nhảy qua lại giữa các tiến trình.

Lý thuyết sẽ là như thế, mình cùng vô thực hành để hiểu hơn về Thread nhen!

Tạo và quản lý Thread

Khi các chương trình Java được thực thi, luồng(Thread) chính luôn luôn đang được thực hiện, nó được tạo ra một cách tự động khi bạn start chương trình, các luồng con thường sẽ được tạo thông qua nó, nó cũng là luồng kết thuốc cuối cùng của chương trình

để tạo ra Thread, thường sẽ có 2 cách

  1. Sử dụng Thread
Class HanhGa extends Thread {
   public void run() {
   //thực thi 
   }
}

2.Sử dụng Runnable

Class HanhGa implements Runnable {
   public void run() {
   //thực thi 
   }
}

Có 1 vài sự khác biệt khi bạn sử dụng Runnable và Thread

  1. Runnable là một interface trong Java, còn Thread là một class implement từ Runnable. Nếu muốn tạo một luồng mới, bạn có thể implement interface Runnable hoặc kế thừa từ class Thread.
  2. Runnable chỉ cung cấp một phương thức run() để chạy một tác vụ, trong khi Thread cung cấp nhiều phương thức khác để điều khiển luồng, chẳng hạn như start(), interrupt(), join() và sleep().
  3. Runnable có thể được sử dụng trong nhiều trường hợp, trong khi Thread chỉ có thể sử dụng trong một số trường hợp cụ thể. Do đó, sử dụng Runnable là một lựa chọn tốt hơn trong trường hợp muốn chạy nhiều luồng với cùng một tác vụ.

Ok giờ cùng thực thực hiện 1 chương trình và phân tích về nó

public class HanhGa extends Thread {
    public static void main(String[] args) {
        Thread t = Thread.currentThread();
        System.out.println("The current Thread is :" + t);
        t.setName("HanhGa");
        System.out.println("The thread is now named: " + t);
    }
}

image.png

Trích xuất từ kết quả image.png

Mỗi luồng trong chương trình Java được đăng ký cho một quyền ưu tiên. Máy ảo Java không bao giờ thay đổi quyền ưu tiên của luồng. Quyền ưu tiên vẫn còn là hằng số cho đến khi luồng bị ngắt.

Mỗi luồng có một giá trị ưu tiên nằm trong khoảng từ Thread.MIN_PRIORITY đến Thread.MAX_PRIORITY tức 1-10. Mỗi luồng phụ thuộc vào một nhóm luồng, và mỗi nhóm luồng có quyền ưu tiên riêng thường thì bằng 5.

image.png Lượn lờ trên mạng thì mình tìm được chiếc hình cũ kỹ này thể hiện được vòng đời của Thread.

Trạng thái của Thread

1 Thread trong Java thường tồn tại 6 trạng thái

New: Thread đã được tạo nhưng chưa được chạy.

Runnable: Thread đang chạy hoặc đang chờ CPU để được thực thi.

Blocked: Thread bị chặn khi đang chờ một đối tượng được sử dụng bởi một thread khác.

Waiting: Thread đang chờ một sự kiện xảy ra hoặc một thread khác gửi một thông báo cho nó.

Timed waiting: Thread đang chờ một số thời gian nhất định.

Terminated: Thread đã kết thúc hoặc bị tắt bất cẩn.

Một luồng khi được tạo ra sẽ không tự động chạy mà cần được được gọi qua phương thức start().

Khi phương thức sleep() được gọi, nó sẽ được tạm ngưng và quay về trạng thái Waiting

Một số hàm thông dụng của Thread.

getName() lấy tên Thread.

isAlive() trả về True nếu Thread còn tồn tại.

getPriority() trả về điểm ưu tiên của Thread.

setName() set tên cho Thread.

join() hàm này sẽ chờ cho đến khi Thread của bạn chết.

isDaemon() có phải Thread Daemon(Luồng Hiếm) hay không.

resume() đánh dấu luồng là luồng hiếm(Daemon).

sleep() tạm ngưng thực thi.

start() Gọi phương thức run() để bắt đầu một luồng.

Luồng hiếm (Daemon Thread)

Daemon Thread hay còn có cái tên thân thiện hơn và luồng hiếm được chỉ định như là luồng "background" (nền). Người sử dụng tạo ra các luồng người sử dụng và cung cấp các "dịch vụ" cho luồng khác.

Trong Java thì luôn có ít nhất 1 luồng hiếm tồn tại nó được biết đến như là luồng "garbage collection" (thu lượm những dữ liệu vô nghĩa - dọn rác)

Bạn có thể dùng phương thức setDaemon để chỉ định 1 Thread là Thead hiếm. 😄

Ví dụ Thread Notify, Thread Pool đơn giản

Chúng ta cùng làm một ví dụ đơn giản về Thread nha.

Trong ví dụ này chúng ta sẽ tạo ra 1 Thread Pool, và khi Thread hoàn thành chúng ta sẽ bắn notify để thông báo.

Ok! bắt đầu thôi....

Để Thread có thể đăng ký các Listener, mình sẽ tạo ra một Thread mới implements từ Runnable

import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

public abstract class NotifyingThread implements Runnable {
 private final Set<ThreadCompleteListener> listeners
         = new CopyOnWriteArraySet<ThreadCompleteListener>();

 public final void addListener(final ThreadCompleteListener listener) {
   listeners.add(listener);
 }

 public final void removeListener(final ThreadCompleteListener listener) {
   listeners.remove(listener);
 }

 private void notifyListeners() {
   for (ThreadCompleteListener listener : listeners) {
     listener.notifyOfThreadComplete(this);
   }
 }

 @Override
 public final void run() {
   try {
     doRun();
   } finally {
     notifyListeners();
   }
 }

 public abstract void doRun();
 public abstract void startThread();
 public abstract Thread getThread();
}

Tạo interface ThreadCompleteListener để các Listener implements lại.

public interface ThreadCompleteListener {
  void notifyOfThreadComplete(final Runnable thread);
}

Tạo một class để quản lý Thread

public class ThreadStart extends NotifyingThread {

  Supplier<Object> supplier;
  VoidSupplier supplierVoid;
  String supplierMode = "object";

  Thread t;
  public ThreadStart(Supplier<Object> func) {
    t = new Thread(this, func.getClass().getName() + new Random().nextInt());
    supplier = func;
    supplierMode = "object";
  }
  public ThreadStart(VoidSupplier func) {
    t = new Thread(this, func.getClass().getName() + new Random().nextInt());
    supplierVoid = func;
    supplierMode = "void";
  }
  @Override
  public void startThread(){
    t.start();
  }

  @Override
  public Thread getThread(){
    return t;
  }
  @Override
  public void doRun() {
    try {
      if (supplierMode.equals("object"))
        supplier.apply();
      else
        supplierVoid.apply();
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}
  • VoidSupplier thì cũng là một FunctionalInterface tương từ Supplier thôi nhé.

Ok giờ mình cùng test thôi.

NotifyingThread thread = new ThreadStart(() -> new ClassRunThread().process());
thread.addListener(new ClassReceiveNotify());
thread.startThread();

trong đó ClassRunThread là class chúng đa muốn chạy trong Thread mới, ClassReceiveNotify được implements ThreadCompleteListener để nhận thông báo.

Screenshot 2023-02-05 at 21.37.29.png

và đây là kết quả chúng ta nhận được.

Nâng cao hơn 1 tí nào, chúng ta cùng tạo thêm cho nó cái Pool để quản lý lượng Thread được tạo ra nha.

*Lưu ý: Việc tạo này để giúp các bạn hiêu hơn về Thread, trên thực tế các ThreadPool có độ phức tạp cao hơn hoặc bạn có thể sử dụng các lớp được cung cấp bởi java.util.concurrent như ExecutorService, Semaphore hoặc CountDownLatch để kiểm soát luông tốt hơn.

public class ThreadPool implements ThreadCompleteListener {

  public ThreadPool(int maxThread) {
    ThreadPool.maxThread = maxThread;
  }
  public ThreadPool(int maxThread, ThreadCompleteListener... event) {
    ThreadPool.maxThread = maxThread;
    ThreadPool.event = event;
  }

  public void run(Supplier<Object> supplier) {
    try {
      while (ThreadPool.currentThread >= ThreadPool.maxThread){
        Thread.sleep(100);
      }
      setCurrentThread(1);
      NotifyingThread thread1 = new ThreadStart(supplier);
      thread1.addListener(this);
      if (event != null) {
        for (var e : event){
          thread1.addListener(e);
        }
      }
      thread1.startThread();
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }

  public void run(VoidSupplier supplier) {
    try {
      while (ThreadPool.currentThread >= ThreadPool.maxThread){
        Thread.sleep(300);
      }
      setCurrentThread(1);
      NotifyingThread thread1 = new ThreadStart(supplier);
      thread1.addListener(this);
      if (event != null) {
        for (var e : event){
          thread1.addListener(e);
        }
      }
      thread1.startThread();
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }

  protected static volatile int maxThread = 5;
  protected static volatile ThreadCompleteListener event[] = null;
  protected static volatile int currentThread = 0;
  private static synchronized int threadCount() {
    return maxThread;
  }
  private static synchronized int setCurrentThread(int t) {
    return currentThread = currentThread + t;
  }

  public static int getCurrentThread() {
    return currentThread;
  }

  public static int getMaxThread() {
    return maxThread;
  }
  @Override
  public void notifyOfThreadComplete(Runnable thread) {
    setCurrentThread(-1);
  }
}

Trong class phía trên mình sẽ lưu số lượng Thread đang chạy trong currentThread, mình coi ThreadPool như là một Listener để nhận được thông báo khi Thread hoàn thành và điều chỉnh currentThread lại.

Trong ví dụ trên, mình dùng hàm sleep để tạm ngưng 100ms khi currentThread > maxThread. (sẽ có thêm nhiều cách khác để bạn làm, ví dụ như đưa vào Queue.... trong ví dụ đơn này này mình sleep cho lẹ nhé =)) )

Ok! tiếp theo mình tạo thêm một cái Builder nữa cho nó chuyên nghiệp nha 😄

public class ThreadBuilder {

  int maxThread;
  ThreadCompleteListener event[];

  public static ThreadBuilder newBuilder() {
    return new ThreadBuilder();
  }

  public ThreadBuilder withMaxThread(int maxThread) {
    this.maxThread = maxThread;
    return this;
  }

  public ThreadBuilder withMaxThread(ThreadCompleteListener... event) {
    this.event = event;
    return this;
  }

  public ThreadPool build() {
    if (event == null)
      return new ThreadPool(maxThread);
    return new ThreadPool(maxThread, event);
  }
}

Giờ mình cùng xem thành quả nha.

ThreadPool run = ThreadBuilder.newBuilder().withMaxThread(2).withMaxThread(new ClassReceiveNotify()).build();
run.run(() -> new ClassRunThread().process(1));
run.run(() -> new ClassRunThread().process(2));
run.run(() -> new ClassRunThread().process(3));
run.run(() -> new ClassRunThread().process(4));

Mình tạo ThreadPool với số max là 2, nhưng mình chạy cùng lúc 4 Thread và hình dưới là kết quả

Screenshot 2023-02-05 at 21.55.09.png

như các bạn đã thấy process 1process 2 được xử lý cùng lúc, trong khi process 3process 4 thì cần phải chờ vì Pool size chỉ cho phép 2.

Kết luận

Trong bài viết này mình đã đi qua lý thuyết về Thread, làm ví dụ đơn giản về Thread được mình chích ra từ các dự án thực tế và làm đơn giản lại để các bạn dễ hiểu.

Cảm ơn các bạn đã xem bài, nếu có sai xót gì trong content, mình mong nhận được ý kiến đóng góp từ mọi người


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí