0

Java Thread và Executor Service

I. Java Thread:

  • cho phép chương trình chạy hiệu quả hơn cùng 1 lúc khi thực hiện nhiều việc.
  • Các luồng (threads) có thể được sử dụng để thực hiện các tác vụ phức tạp ở chế độ nền mà không làm gián đoạn chương trình chính.
  1. Tạo luồng: có 2 cách Cách 1: tạo bằng cách kế thừa lớp Thread và ghi đè(override) phương thức run của nó: public class Main extends Thread{ @Override public void run(){ System.out.println("This code is running in a thread"); } } Cách 2: Tạo bằng cách implement Runnable interface: public class Main implements Runnable{ public void run(){ System.out.println("This code is running in a thread"); } }
  2. Running Threads:
  • Nếu như 1 lớp kế thừa lớp Thread thì luồng có thể chạy bằng cách tạo 1 instance (thể hiện) của lớp đó và gọi phương thức start(): public class Main extends Thread{ public static void main(String [] args){ Main thread = new Main(); thread.start(); System.out.println("This code is outside of the thread"); @Override public void run(){ System.out.println("This code is running in a thread"); } } }
    • Nếu một lớp cài đặt (implements) giao diện Runnable, thì luồng (thread) có thể được chạy bằng cách truyền một đối tượng của lớp đó vào hàm tạo (constructor) của đối tượng Thread, rồi gọi phương thức start() của luồng đó. public class Main implements Runnable{ public static void main(String [] args){ Main obj = new Main(); Thread thread = new Thread(obj); thread.start(); System.out.println("This code is outside of the thread"); public void run(){ System.out.println("This code is running in a thread"); } } }
  1. Sự khác nhau giữa extend và implement Threads:
  • Sự khác biệt chính đó là khi 1 lớp extend lớp Thread thì bạn không thể extend được lớp khác nữa nhưng bằng cách implements Runnable interface thì nó có thể extend ( kế thừa ) tiếp các lớp khác.
  1. Concurrency Problems ( Các vấn đề đồng thời ~ Vấn đề khi chạy song song):
  • Vì các luồng chạy cùng lúc với các phần khác của chương trình, không thể biết trước được thứ tự mà code sẽ thực thi. Khi các luồng và chương trình chính cùng đọc và ghi các biến giống nhau, giá trị của chúng trở nên không thể đoán trước. Những vấn đề phát sinh từ tình huống này được gọi là các vấn đề đồng thời (concurrency problems). -> Cách để tránh các vấn đề đồng thời: chia sẻ càng ít các thuộc tính giữa các luồng các tốt. Nếu bắt buộc phải chia sẻ thuộc tính, một giải pháp có thể là sử dụng phương thức isAlive() của luồng để kiểm tra xem luồng đó đã kết thúc chạy hay chưa trước khi sử dụng bất kì thuộc tính nào mà luồng đó có thể thay đổi. II. Java Executor Service: 1.Tổng quan:
  • Executor Service là 1 API của JDK giúp đơn giản hóa việc chạy các tác vụ ở chế độ bất đồng bộ (asynchronous). Nói chung, ES tự động cung cấp 1 nhóm luồng ( thread pool ) và 1 API để gán các tác vụ cho nhóm này.
  1. Khởi tạo ES: 2.1. Factory Methods of the EXECUTOR CLASS:
  • Ở đây “Factory Methods” chỉ những phương thức đặc biệt để tạo ra đối tượng, trong trường hợp này là các thể hiện của ExecutorService hoặc các loại thread pool khác nhau.
  • Cách dễ nhất để tạo ES là sử dụng 1 trong các factory methods của lớp Executor. Ví dụ về cách tạo ra 1 nhóm luồng có 10 luồng: ExecutorService executor = Executoors.newFixedThreadPool(10);
  • Còn có một số phương thức factory khác để tạo ra ExecutorService đã được định nghĩa sẵn nhằm phục vụ các trường hợp sử dụng cụ thể. Để tìm phương thức phù hợp nhất với nhu cầu của bạn, hãy tham khảo tài liệu chính thức của Oracle(https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Executors.html).

2.2. Directly Create an ES:

  • Vì ExecutorService là một interface, nên bạn có thể sử dụng bất kỳ thể hiện (instance) nào của các lớp triển khai(implements) nó. Trong gói java.util.concurrent có sẵn nhiều lớp triển khai để bạn lựa chọn, hoặc bạn cũng có thể tự tạo lớp triển khai riêng.
  • Ví dụ, lớp ThreadPoolExecutor có một vài hàm tạo (constructors) mà chúng ta có thể sử dụng để cấu hình một executor service và nhóm luồng bên trong của nó. ExecutorService executorService = new ThreadPoolExecutor(1,1,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

Bạn có thể nhận thấy rằng mã nguồn ở trên rất giống với mã nguồn của phương thức factory newSingleThreadExecutor(). Trong hầu hết các trường hợp, việc cấu hình chi tiết thủ công là không cần thiết.

  • ThreadPoolExecutor là một thread pool có thể quản lý nhiều luồng để thực hiện các tác vụ.
  • Tham số của ThreadPoolExecutor: • 1 (corePoolSize): số luồng cơ bản luôn được giữ sẵn trong pool, dù đang idle (rảnh hay không làm gì) hay không. • 1 (maximumPoolSize): số luồng tối đa có thể chạy đồng thời. Ở đây vì cả core và max đều là 1, pool chỉ có 1 luồng duy nhất. • 0L (keepAliveTime): thời gian tối đa mà luồng thừa (trên core) sẽ được giữ khi idle trước khi bị terminate. 0 nghĩa là ngay lập tức terminate nếu vượt core size. • TimeUnit.MILLISECONDS: đơn vị đo của keepAliveTime. Ở đây là mili giây. • new LinkedBlockingQueue<Runnable>(): hàng đợi tác vụ dùng để lưu các Runnable chờ thực thi. Khi pool bận, các task mới sẽ xếp vào queue. 2.3. Gán Tasks cho ES:
  • Executor Service có thể thực thi các tác vụ Runnable và Callable. Để đơn giản hóa trong bài viết này, hai tác vụ cơ bản sẽ được sử dụng. Lưu ý rằng ở đây chúng ta sử dụng biểu thức lambda thay vì các lớp ẩn danh (anonymous inner classes). 2.3. Gán Tasks cho ES:
  • Executor Service có thể thực thi các tác vụ Runnable và Callable. Để đơn giản hóa trong bài viết này, hai tác vụ cơ bản sẽ được sử dụng. Lưu ý rằng ở đây chúng ta sử dụng biểu thức lambda thay vì các lớp ẩn danh (anonymous inner classes). Chúng ta có thể giao các tác vụ (task) cho ExecutorService bằng nhiều phương thức khác nhau, bao gồm: • execute() — được kế thừa từ interface Executor, • và các phương thức khác như submit(), invokeAny() và invokeAll(). Phương thức execute() có kiểu trả về là void, nên không cho phép bạn lấy kết quả sau khi tác vụ thực thi, hoặc kiểm tra trạng thái của tác vụ (ví dụ: nó còn đang chạy hay đã xong). executorService.execute(runnableTask); Phương thức submit() dùng để gửi (submit) một tác vụ dạng Callable hoặc Runnable đến ExecutorService, và nó trả về một kết quả thuộc kiểu Future. Future<String> future = executorService.submit(callableTask); Phương thức invokeAny() dùng để giao một tập hợp các tác vụ (collection of tasks) cho ExecutorService — khiến tất cả các tác vụ đó được chạy. Nó sẽ trả về kết quả của tác vụ đầu tiên thực thi thành công (nếu có ít nhất một tác vụ hoàn thành thành công). String result = executorService.invokeAny(callableTasks); Phương thức invokeAll() dùng để giao một tập hợp các tác vụ (collection of tasks) cho ExecutorService, khiến tất cả các tác vụ được thực thi. Sau đó, nó sẽ trả về kết quả của toàn bộ các tác vụ dưới dạng một danh sách (List) các đối tượng kiểu Future. List <Future<String>> futures = executorService.invokeAll(callableTasks); Trước khi đi tiếp, chúng ta cần tìm hiểu thêm hai nội dung quan trọng:
  1. Cách tắt (shutdown) một ExecutorService, và
  2. Cách xử lý các giá trị trả về kiểu Future. 2.4. Shutting Down an ExecutorService: Thông thường, ExecutorService sẽ không tự động bị hủy khi không còn tác vụ nào cần xử lý. Nó sẽ vẫn tồn tại và chờ đợi công việc mới để thực hiện. Trong một số trường hợp, điều này rất hữu ích, chẳng hạn như khi ứng dụng cần xử lý các tác vụ xuất hiện không đều đặn hoặc số lượng tác vụ không xác định trước tại thời điểm biên dịch (compile time). Tuy nhiên, ở chiều ngược lại, có thể xảy ra tình huống ứng dụng đã hoàn thành tất cả công việc, nhưng vẫn không dừng lại — bởi vì ExecutorService đang trong trạng thái chờ tác vụ mới, khiến JVM tiếp tục chạy. Để tắt ExecutorService một cách đúng đắn, ta có thể dùng hai phương thức: -> shutdown() và shutdownNow(). Phương thức shutdown() không hủy ngay lập tức ExecutorService. Nó sẽ ngừng nhận thêm tác vụ mới, và chỉ thực sự tắt sau khi tất cả các luồng hiện tại hoàn thành công việc của chúng.

executorService.shutdown(); Phương thức shutdownNow() cố gắng tắt (hủy) ExecutorService ngay lập tức, nhưng không đảm bảo rằng tất cả các luồng (threads) đang chạy sẽ dừng lại cùng một lúc. List<Runnable> notExecutedTasks = executorService.shutdownNow(); Phương thức này (shutdownNow()) sẽ trả về một danh sách (List) chứa các tác vụ đang chờ được xử lý. Lúc này, lập trình viên sẽ tự quyết định nên làm gì với các tác vụ đó. Một cách tốt và được Oracle khuyến nghị để tắt ExecutorService một cách an toàn, là kết hợp cả hai phương thức (shutdown() và shutdownNow()) cùng với phương thức awaitTermination(). Với cách tiếp cận này, ExecutorService sẽ:

  1. Ngừng nhận thêm các tác vụ mới,
  2. Sau đó chờ trong một khoảng thời gian nhất định để tất cả các tác vụ hiện tại hoàn thành. Nếu hết thời gian chờ mà vẫn còn tác vụ đang chạy, thì việc thực thi sẽ bị dừng ngay lập tức. 2.5. The Future Interface: Các phương thức submit() và invokeAll() sẽ trả về một đối tượng hoặc một tập hợp các đối tượng kiểu Future, nhờ đó ta có thể lấy kết quả thực thi của tác vụ hoặc kiểm tra trạng thái của tác vụ (ví dụ: nó có đang chạy hay không). Giao diện Future cung cấp một phương thức đặc biệt có tính chặn (blocking) là get(), phương thức này sẽ trả về kết quả thực tế của tác vụ Callable, hoặc trả về null nếu tác vụ đó là Runnable. Gọi phương thức get() trong khi tác vụ vẫn đang chạy sẽ khiến luồng thực thi bị chặn (block) cho đến khi tác vụ hoàn tất và kết quả sẵn sàng. Nếu thời gian chờ quá lâu do get() gây ra, hiệu năng của ứng dụng có thể bị giảm sút. Trong trường hợp kết quả không quá quan trọng, ta có thể tránh vấn đề này bằng cách sử dụng cơ chế giới hạn thời gian (timeout). String result = future.get(200, TimeUnit.MILLISECONDS); Nếu thời gian thực thi vượt quá khoảng thời gian được chỉ định (trong ví dụ này là 200 mili-giây), thì một ngoại lệ TimeoutException sẽ được ném ra. Chúng ta có thể sử dụng phương thức isDone() để kiểm tra xem tác vụ đã được xử lý xong hay chưa. Giao diện Future cũng cung cấp khả năng hủy việc thực thi tác vụ bằng phương thức cancel(), và kiểm tra xem tác vụ đó có bị hủy hay chưa bằng phương thức isCancelled().
    boolean cancled = future.cancel(true); boolean isCancelled = future.isCancelled();

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í