logo

Lộ trình

Khóa học

Tài liệu

Mock Interview

Liên hệ

Quay lại
  • Trang chủ

    /

  • Tài liệu

    /

  • Java Concurrency (Phần 1): Thread
Tài liệu

Java Concurrency (Phần 1): Thread

Ronin Engineer

7 Tháng 9 2024

<p>by Chien</p><h1 id="1-gi%E1%BB%9Bi-thi%E1%BB%87u">1. Giới thiệu</h1><p>Lập trình đồng thời (concurrency) trong Java đề cập đến khả năng của một chương trình Java thực thi nhiều tác vụ đồng thời hoặc song song, tận dụng tối đa các bộ xử lý (CPU) đa lõi (core) hiện đại. Khi các ứng dụng ngày càng trở nên phức tạp và đòi hỏi hiệu suất cao hơn, lập trình đồng thời trở thành yếu tố thiết yếu để cải thiện hiệu năng, khả năng phản hồi và khả năng mở rộng.</p><p>Java cung cấp một bộ công cụ và các thư viện phong phú giúp các nhà phát triển tạo ra các ứng dụng đồng thời, quản lý nhiều luồng (threads) và điều phối các tác vụ một cách hiệu quả. Trong bài viết này, chúng sẽ khám phá các khái niệm cơ bản về lập trình đồng thời trong Java.</p><figure class="kg-card kg-image-card"><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcQKk_aZUMy1NburSKgsk2GQBWd6V-UuXZde9Vngm5awBj7ufbycNexr_K_u_Juh60A7yxGOtXQ7XJmU6RRqgr4A3j0TQf2s-nV3WhdpNU2vBj81ycEEQp0x4Y2Yf6QwR3t2IV1agSm9Imk3quiG598enap?key=KupFoSXfbRVp_VLkZu0Dpg" class="kg-image" alt="" loading="lazy" width="1024" height="462"></figure><h1 id="2-%C4%91%E1%BB%8Bnh-ngh%C4%A9a-thread">2. Định nghĩa Thread</h1><p>Một thread là một đơn vị thực thi nhỏ hơn một process. <strong>Một process có thể tạo ra nhiều thread trong quá trình thực thi</strong>. Tất cả các thread trong cùng một process sẽ chia sẻ, <strong>dùng chung một số vùng nhớ </strong>với nhau (<strong>heap memory</strong>, static variables, metaspace, … phần này mình sẽ chia sẻ cụ thể hơn ở một bài viết khác). Vì vậy, việc giao tiếp giữa các thread khá đơn giản và dễ dàng hơn so với giao tiếp giữa các process. Ngoài ra, việc tạo mới/hủy thread đơn giản và tốn ít công hơn so với việc tạo mới/hủy một process. Vì các lý do này, thread còn được gọi là lightweight process.</p><figure class="kg-card kg-image-card"><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfQzgrsjAGSh45TpjlQF6xUX-CD7fDm_lo_FuX9djQZifASo_A9o4oewkp9S6JFhQYz00Xsgpn592VwfJ7MTrPVSeOu9AHPED2nidIYPqdG8iVEKTczV11EebBkoVZz_4FDUePv4gCpALKHXHxqZW9JUi0?key=KupFoSXfbRVp_VLkZu0Dpg" class="kg-image" alt="" loading="lazy" width="704" height="406"></figure><h1 id="3-c%C3%A1ch-kh%E1%BB%9Fi-t%E1%BA%A1o-thread">3. Cách khởi tạo thread</h1><p>Đây là một câu hỏi thường hay gặp trong phỏng vấn. Bạn có thể tham khảo hoặc trả lời như sau:&nbsp;</p><p>Ta có thể phân loại các cách khởi tạo thread như sau:</p><h2 id="31-t%E1%BA%A1o-tr%E1%BB%B1c-ti%E1%BA%BFp-thread">3.1. Tạo trực tiếp thread</h2><p>sử dụng <code>new Thread().start()</code></p><pre><code class="language-Java">new Thread(() -&gt; resource.counter++).start();</code></pre><h2 id="32-khai-b%C3%A1o-thread-execution-method">3.2. Khai báo Thread execution method</h2><h3 id="321-k%E1%BA%BF-th%E1%BB%ABa-class-thread">3.2.1. Kế thừa class Thread</h3><p>Đây là một cách phổ biến. Chúng ta tạo ra một class mới kế thừa class Thread và ghi đè method run như sau:</p><pre><code class="language-Java">public class ExtendsThread extends Thread { &nbsp;&nbsp;&nbsp;&nbsp;@Override &nbsp;&nbsp;&nbsp;&nbsp;public void run() { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println("Do something"); &nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;public static void main(String[] args) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;new ExtendsThread().start(); &nbsp;&nbsp;&nbsp;&nbsp;} }</code></pre><h3 id="322-tri%E1%BB%83n-khai-interface-runnable">3.2.2. Triển khai interface Runnable</h3><p>Đây cũng là một cách phổ biến, implement Runnable interface và override method run, như sau:</p><pre><code class="language-Java">public class ImplementsRunnable implements Runnable { &nbsp;&nbsp;&nbsp;&nbsp;@Override &nbsp;&nbsp;&nbsp;&nbsp;public void run() { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println("Do something"); &nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;public static void main(String[] args) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ImplementsRunnable runnable = new ImplementsRunnable(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;new Thread(runnable).start(); &nbsp;&nbsp;&nbsp;&nbsp;} }</code></pre><h3 id="323-tri%E1%BB%83n-khai-interface-callable">3.2.3. Triển khai interface Callable</h3><p>Tương tự như method trước, ngoại trừ method này có thể nhận giá trị trả về sau khi Thread được thực thi, như sau:</p><pre><code class="language-Java">public class ImplementsCallable implements Callable&lt;String&gt; { &nbsp;&nbsp;&nbsp;&nbsp;@Override &nbsp;&nbsp;&nbsp;&nbsp;public String call() throws Exception { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println("Do something"); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return "test"; &nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;public static void main(String[] args) throws Exception { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ImplementsCallable callable = new ImplementsCallable(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FutureTask&lt;String&gt; futureTask = new FutureTask&lt;&gt;(callable); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;new Thread(futureTask).start(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println(futureTask.get()); &nbsp;&nbsp;&nbsp;&nbsp;} }</code></pre><h3 id="324-s%E1%BB%AD-d%E1%BB%A5ng-class-%E1%BA%A9n-danh-ho%E1%BA%B7c-bi%E1%BB%83u-th%E1%BB%A9c-lambda">3.2.4. Sử dụng class ẩn danh hoặc biểu thức Lambda</h3><pre><code class="language-Java">public class UseAnonymousClass { &nbsp;&nbsp;&nbsp;&nbsp;public static void main(String[] args) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;new Thread(new Runnable() { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@Override &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;public void run() { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println("AnonymousClass"); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}).start(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;new Thread(() -&gt;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println("Lambda") &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;).start(); &nbsp;&nbsp;&nbsp;&nbsp;} }</code></pre><h2 id="33-t%E1%BA%A1o-gi%C3%A1n-ti%E1%BA%BFp-thread">3.3. Tạo gián tiếp thread</h2><h3 id="331-s%E1%BB%AD-d%E1%BB%A5ng-thread-pool-c%E1%BB%A7a-executorservice">3.3.1. Sử dụng thread pool của ExecutorService</h3><pre><code class="language-Java">public class UseExecutorService { &nbsp;&nbsp;&nbsp;&nbsp;public static void main(String[] args) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ExecutorService poolA = Executors.newFixedThreadPool(2); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;poolA.execute(() -&gt; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println("do something"); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); }</code></pre><h3 id="332-s%E1%BB%AD-d%E1%BB%A5ng-thread-pool-ho%E1%BA%B7c-stream-song-song-parallel-stream">3.3.2. Sử dụng thread pool hoặc Stream song song (parallel stream)</h3><pre><code class="language-Java">public class UseForkJoinPool { &nbsp;&nbsp;&nbsp;&nbsp;public static void main(String[] args) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ForkJoinPool forkJoinPool = new ForkJoinPool(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;forkJoinPool.execute( () -&gt; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println("Do something"); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;List&lt;String&gt; list = Arrays.asList("e1"); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;list.parallelStream().forEach(System.out::println); &nbsp;&nbsp;&nbsp;&nbsp;} }</code></pre><h3 id="333-s%E1%BB%AD-d%E1%BB%A5ng-completablefuture">3.3.3. Sử dụng CompletableFuture</h3><pre><code class="language-Java">public class UseCompletableFuture { &nbsp;&nbsp;&nbsp;&nbsp;public static void main(String[] args) throws InterruptedException { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CompletableFuture&lt;String&gt; cf = CompletableFuture.supplyAsync(() -&gt; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println("5......"); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return "test"; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Thread.sleep(1000); &nbsp;&nbsp;&nbsp;&nbsp;} }</code></pre><h3 id="334-s%E1%BB%AD-d%E1%BB%A5ng-class-timer">3.3.4. Sử dụng class Timer</h3><pre><code class="language-Java">public class UseTimer { &nbsp;&nbsp;&nbsp;&nbsp;public static void main(String[] args) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Timer timer = new Timer(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;timer.schedule(new TimerTask() { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;@Override &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;public void run() { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println("9......"); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}, 0, 1000); &nbsp;&nbsp;&nbsp;&nbsp;} }</code></pre><p>Java chỉ có một cách để tạo thread một cách trực tiếp, đó là thông qua việc tạo new Thread().start(). Do đó, cho dù sử dụng phương thức nào thì cuối cùng nó cũng phụ thuộc vào new Thread().start(). Các đối tượng Runnable, Callable, … chỉ là phần thân của Thread, tức là tác vụ được cung cấp cho Thread để thực thi.</p><h1 id="4-tr%E1%BA%A1ng-th%C3%A1i-c%E1%BB%A7a-thread">4. Trạng thái của thread</h1><figure class="kg-card kg-image-card"><img src="https://roninhub.com/content/images/2024/09/thread-status-8.png" class="kg-image" alt="" loading="lazy" width="2000" height="1134" srcset="https://roninhub.com/content/images/size/w600/2024/09/thread-status-8.png 600w, https://roninhub.com/content/images/size/w1000/2024/09/thread-status-8.png 1000w, https://roninhub.com/content/images/size/w1600/2024/09/thread-status-8.png 1600w, https://roninhub.com/content/images/2024/09/thread-status-8.png 2000w" sizes="(min-width: 720px) 720px"></figure><p>Tại một thời điểm, một thread trong Java chỉ có thể ở một trong sáu trạng thái trong vòng đời của nó:</p><ul><li><code>NEW</code>: Khi đối tượng thread được tạo, nó sẽ chuyển sang trạng thái NEW, chẳng hạn như: <code>Thread t = new MyThread()</code>;</li><li><code>RUNNABLE</code>: Trạng thái sẵn sàng để chạy. Ta có thể hiểu, nó sẽ được chia thành 2 trường hợp nhỏ hơn: <strong>đang chạy hoặc đang chờ để chạy</strong>. Ví dụ, khi sau, ta gọi method start(), thread đó có thể chưa chạy được ngay mà phải đợi CPU schedule để chạy.</li><li><code>BLOCKED</code>: Trạng thái bị chặn, thread A đang cố giành khóa (lock) nhưng khoá đang giữa bởi thread B, thread A phải đợi, bị blocked cho đến khi khoá được giải phóng.</li><li><code>TIME_WAITING</code>: Trạng thái chờ có thời gian chờ, có thể tự động quay trở lại trạng thái RUNNABLE sau khoảng thời gian xác định.</li><li><code>WAITING</code>: Trạng thái chờ, biểu thị rằng thread A đang chờ các thread khác thực hiện một số hành động cụ thể, như (notification) thông báo cho thread A hoặc (interruption) ngắt thread A. Khác với TIME_WAITING, trạng thái WAITING không có thời gian timeout, chỉ được wakeup khi có thông báo từ thread khác.</li><li><code>TERMINATED</code>: Trạng thái kết thúc, biểu thị rằng thread đã hoàn thành công việc hoặc dừng lai do gặp exception.</li></ul><h1 id="5-c%C3%A1c-method-c%C6%A1-b%E1%BA%A3n-c%E1%BB%A7a-thread">5. Các method cơ bản của thread</h1><h2 id="51-start">5.1. start()</h2><p>Method <code>start()</code> khởi tạo việc thực thi một thread. Nó gọi phương thức <code>run()</code> được xác định trong class thread hoặc runnable object. Thread sẽ chuyển từ trạng thái NEW sang trạng thái <code>RUNNABLE</code> sau khi method này được gọi.</p><pre><code class="language-Java">public class Main { &nbsp;&nbsp;&nbsp;&nbsp;public static void main(String[] args) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Thread myThread = new Thread(new MyRunnable()); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;myThread.start(); &nbsp;&nbsp;&nbsp;&nbsp;} }</code></pre><h2 id="52-run">5.2. run()</h2><p>Method <code>run()</code> chứa mã sẽ được thực thi trong luồng.</p><pre><code class="language-Java">class MyRunnable implements Runnable { &nbsp;&nbsp;&nbsp;&nbsp;public void run() { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println("This is a runnable."); &nbsp;&nbsp;&nbsp;&nbsp;} }</code></pre><h2 id="53-sleep-v%C3%A0-wait">5.3. sleep() và wait()</h2><p>Method sleep() làm cho thread hiện đang thực thi ở chế độ ngủ (<code>TIMED_WAITING</code>) trong 1 khoảng thời gian được chỉ định (tính bằng milliseconds).</p><p>Method <code>wait()</code> khiến thread hiện tại đợi cho đến khi một thread khác gọi notify() hoặc notifyAll() trên cùng một object. Thread sẽ chuyển từ trạng thái <code>RUNNABLE</code> sang trạng thái <code>WAITING</code> nếu dùng <code>wait()</code> không truyền thêm thời gian timeout. Còn nếu truyền thêm thời gian timeout - <code>wait(timeout)</code> thì thread sẽ ở trạng thái <code>TIMED_WAITING</code>.</p><p>Sự khác biệt giữa 2 method:</p><ul><li><strong>Method wait() cần được đặt trong synchronized code, còn sleep() thì không.</strong></li><li>Method sleep() không giải phóng khóa, trong khi method wait() sẽ giải phóng khóa.&nbsp;</li><li>Method wait() thường được sử dụng cho tương tác/giao tiếp giữa các thread, còn sleep() thường được sử dụng để tạm dừng thực thi.&nbsp;</li><li>Sau khi method wait() được gọi, thread sẽ không tự động thức dậy; cần một luồng khác gọi method notify() hoặc notifyAll() trên cùng một đối tượng để đánh thức luồng đó. Sau khi method sleep() được thực thi, thread sẽ tự động thức dậy (RUNNABLE).</li><li>sleep() là một method static của class Thread, còn wait() là một method của class Object.</li></ul><h2 id="54-notify-v%C3%A0-notifyall">5.4. notify() và notifyAll()</h2><ul><li>notify(): đối với tất cả các thread đang chờ object monitor bằng cách sử dụng bất kỳ method wait() nào, method notify() thông báo cho một trong số các thread đó thức dậy. <strong>Việc lựa chọn chính xác thread nào được đánh thức là mẫu nhiên và chúng ta không thể kiểm soát được</strong> thread được đánh thức.</li><li>notifyAll(): Phương pháp này chỉ đơn giản đánh thức tất cả các thread đang chờ trên object monitor.</li></ul><p>Mình sẽ nói chi tiết hơn về các method này trong bài giao tiếp giữa các threads.&nbsp;</p><h2 id="55-yield">5.5. yield()</h2><p>Method yield() làm cho thread hiện đang thực thi tạm dừng và cho phép các thread khác thực thi.</p><p>Mọi người lưu ý, đây chỉ là hint cho scheduler tạm dừng thread, scheduler có thể bỏ qua cái hint này.</p><p>Method này có thể dùng để tái hiện bug do race condition. Tuy nhiên, method này hiếm khi được sử dụng và mình recommend <strong>không dùng method này trong production code</strong>.</p><h2 id="56-join">5.6. join()</h2><p>Method join() cho phép một thread chờ đợi một thread khác hoàn thành. Điều này có thể hữu ích khi bạn cần đảm bảo hoàn thành một số nhiệm vụ nhất định trước khi tiếp tục. Khi thread A gọi method join() của thread B,<strong> thread A sẽ chuyển sang trạng thái chờ (RUNNABLE → WAITING)</strong>. Nó vẫn ở trạng thái chờ cho đến khi thread B kết thúc.</p><p>Giả sử bạn cần thực hiện một số lệnh gọi API đến các endpoints khác nhau lấy dữ liệu đồng thời. Mỗi lệnh gọi API được thực hiện trong một thread riêng biệt và bạn muốn đợi cho đến khi tất cả các thread hoàn thành yêu cầu API của chúng trước khi tổng hợp (aggregate) kết quả.</p><pre><code class="language-Java">String[] apiEndpoints = { "https://api.example.com/data1", "https://api.example.com/data2", "https://api.example.com/data3" }; List&lt;Thread&gt; threads = new ArrayList&lt;&gt;(); List&lt;String&gt; results = new ArrayList&lt;&gt;(); for (String endpoint : apiEndpoints) { Thread thread = new Thread(() -&gt; { String response = makeApiCall(endpoint); synchronized (results) { results.add(response); } }); threads.add(thread); thread.start(); } // Wait for all threads to complete try { for (Thread thread : threads) { thread.join(); } } catch (InterruptedException e) { e.printStackTrace(); } // Process and aggregate results results.forEach(response -&gt; System.out.println("API response: " + response)); </code></pre>

by Chien

1. Giới thiệu

Lập trình đồng thời (concurrency) trong Java đề cập đến khả năng của một chương trình Java thực thi nhiều tác vụ đồng thời hoặc song song, tận dụng tối đa các bộ xử lý (CPU) đa lõi (core) hiện đại. Khi các ứng dụng ngày càng trở nên phức tạp và đòi hỏi hiệu suất cao hơn, lập trình đồng thời trở thành yếu tố thiết yếu để cải thiện hiệu năng, khả năng phản hồi và khả năng mở rộng.

Java cung cấp một bộ công cụ và các thư viện phong phú giúp các nhà phát triển tạo ra các ứng dụng đồng thời, quản lý nhiều luồng (threads) và điều phối các tác vụ một cách hiệu quả. Trong bài viết này, chúng sẽ khám phá các khái niệm cơ bản về lập trình đồng thời trong Java.

2. Định nghĩa Thread

Một thread là một đơn vị thực thi nhỏ hơn một process. Một process có thể tạo ra nhiều thread trong quá trình thực thi. Tất cả các thread trong cùng một process sẽ chia sẻ, dùng chung một số vùng nhớ với nhau (heap memory, static variables, metaspace, … phần này mình sẽ chia sẻ cụ thể hơn ở một bài viết khác). Vì vậy, việc giao tiếp giữa các thread khá đơn giản và dễ dàng hơn so với giao tiếp giữa các process. Ngoài ra, việc tạo mới/hủy thread đơn giản và tốn ít công hơn so với việc tạo mới/hủy một process. Vì các lý do này, thread còn được gọi là lightweight process.

3. Cách khởi tạo thread

Đây là một câu hỏi thường hay gặp trong phỏng vấn. Bạn có thể tham khảo hoặc trả lời như sau: 

Ta có thể phân loại các cách khởi tạo thread như sau:

3.1. Tạo trực tiếp thread

sử dụng new Thread().start()

new Thread(() -> resource.counter++).start();

3.2. Khai báo Thread execution method

3.2.1. Kế thừa class Thread

Đây là một cách phổ biến. Chúng ta tạo ra một class mới kế thừa class Thread và ghi đè method run như sau:

public class ExtendsThread extends Thread {
    @Override
    public void run() {
        System.out.println("Do something");
    }

    public static void main(String[] args) {
        new ExtendsThread().start();
    }
}

3.2.2. Triển khai interface Runnable

Đây cũng là một cách phổ biến, implement Runnable interface và override method run, như sau:

public class ImplementsRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Do something");
    }

    public static void main(String[] args) {
        ImplementsRunnable runnable = new ImplementsRunnable();
        new Thread(runnable).start();
    }
}

3.2.3. Triển khai interface Callable

Tương tự như method trước, ngoại trừ method này có thể nhận giá trị trả về sau khi Thread được thực thi, như sau:

public class ImplementsCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("Do something");
        return "test";
    }

    public static void main(String[] args) throws Exception {
        ImplementsCallable callable = new ImplementsCallable();
        FutureTask<String> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
    }
}

3.2.4. Sử dụng class ẩn danh hoặc biểu thức Lambda

public class UseAnonymousClass {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("AnonymousClass");
            }
        }).start();

        new Thread(() -> 
                System.out.println("Lambda")
        ).start();
    }
}

3.3. Tạo gián tiếp thread

3.3.1. Sử dụng thread pool của ExecutorService

public class UseExecutorService {
    public static void main(String[] args) {
        ExecutorService poolA = Executors.newFixedThreadPool(2);
        poolA.execute(() -> {
            System.out.println("do something");
        });
}

3.3.2. Sử dụng thread pool hoặc Stream song song (parallel stream)

public class UseForkJoinPool {
    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        forkJoinPool.execute( () -> {
            System.out.println("Do something");
        });

        List<String> list = Arrays.asList("e1");
        list.parallelStream().forEach(System.out::println);
    }
}

3.3.3. Sử dụng CompletableFuture

public class UseCompletableFuture {
    public static void main(String[] args) throws InterruptedException {
        CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
            System.out.println("5......");
            return "test";
        });
        Thread.sleep(1000);
    }
}

3.3.4. Sử dụng class Timer

public class UseTimer {
    public static void main(String[] args) {
        Timer timer = new Timer();

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("9......");
            }
        }, 0, 1000);
    }
}

Java chỉ có một cách để tạo thread một cách trực tiếp, đó là thông qua việc tạo new Thread().start(). Do đó, cho dù sử dụng phương thức nào thì cuối cùng nó cũng phụ thuộc vào new Thread().start(). Các đối tượng Runnable, Callable, … chỉ là phần thân của Thread, tức là tác vụ được cung cấp cho Thread để thực thi.

4. Trạng thái của thread

Tại một thời điểm, một thread trong Java chỉ có thể ở một trong sáu trạng thái trong vòng đời của nó:

  • NEW: Khi đối tượng thread được tạo, nó sẽ chuyển sang trạng thái NEW, chẳng hạn như: Thread t = new MyThread();
  • RUNNABLE: Trạng thái sẵn sàng để chạy. Ta có thể hiểu, nó sẽ được chia thành 2 trường hợp nhỏ hơn: đang chạy hoặc đang chờ để chạy. Ví dụ, khi sau, ta gọi method start(), thread đó có thể chưa chạy được ngay mà phải đợi CPU schedule để chạy.
  • BLOCKED: Trạng thái bị chặn, thread A đang cố giành khóa (lock) nhưng khoá đang giữa bởi thread B, thread A phải đợi, bị blocked cho đến khi khoá được giải phóng.
  • TIME_WAITING: Trạng thái chờ có thời gian chờ, có thể tự động quay trở lại trạng thái RUNNABLE sau khoảng thời gian xác định.
  • WAITING: Trạng thái chờ, biểu thị rằng thread A đang chờ các thread khác thực hiện một số hành động cụ thể, như (notification) thông báo cho thread A hoặc (interruption) ngắt thread A. Khác với TIME_WAITING, trạng thái WAITING không có thời gian timeout, chỉ được wakeup khi có thông báo từ thread khác.
  • TERMINATED: Trạng thái kết thúc, biểu thị rằng thread đã hoàn thành công việc hoặc dừng lai do gặp exception.

5. Các method cơ bản của thread

5.1. start()

Method start() khởi tạo việc thực thi một thread. Nó gọi phương thức run() được xác định trong class thread hoặc runnable object. Thread sẽ chuyển từ trạng thái NEW sang trạng thái RUNNABLE sau khi method này được gọi.

public class Main {
    public static void main(String[] args) {
        Thread myThread = new Thread(new MyRunnable());
        myThread.start();
    }
}

5.2. run()

Method run() chứa mã sẽ được thực thi trong luồng.

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("This is a runnable.");
    }
}

5.3. sleep() và wait()

Method sleep() làm cho thread hiện đang thực thi ở chế độ ngủ (TIMED_WAITING) trong 1 khoảng thời gian được chỉ định (tính bằng milliseconds).

Method wait() khiến thread hiện tại đợi cho đến khi một thread khác gọi notify() hoặc notifyAll() trên cùng một object. Thread sẽ chuyển từ trạng thái RUNNABLE sang trạng thái WAITING nếu dùng wait() không truyền thêm thời gian timeout. Còn nếu truyền thêm thời gian timeout - wait(timeout) thì thread sẽ ở trạng thái TIMED_WAITING.

Sự khác biệt giữa 2 method:

  • Method wait() cần được đặt trong synchronized code, còn sleep() thì không.
  • Method sleep() không giải phóng khóa, trong khi method wait() sẽ giải phóng khóa. 
  • Method wait() thường được sử dụng cho tương tác/giao tiếp giữa các thread, còn sleep() thường được sử dụng để tạm dừng thực thi. 
  • Sau khi method wait() được gọi, thread sẽ không tự động thức dậy; cần một luồng khác gọi method notify() hoặc notifyAll() trên cùng một đối tượng để đánh thức luồng đó. Sau khi method sleep() được thực thi, thread sẽ tự động thức dậy (RUNNABLE).
  • sleep() là một method static của class Thread, còn wait() là một method của class Object.

5.4. notify() và notifyAll()

  • notify(): đối với tất cả các thread đang chờ object monitor bằng cách sử dụng bất kỳ method wait() nào, method notify() thông báo cho một trong số các thread đó thức dậy. Việc lựa chọn chính xác thread nào được đánh thức là mẫu nhiên và chúng ta không thể kiểm soát được thread được đánh thức.
  • notifyAll(): Phương pháp này chỉ đơn giản đánh thức tất cả các thread đang chờ trên object monitor.

Mình sẽ nói chi tiết hơn về các method này trong bài giao tiếp giữa các threads. 

5.5. yield()

Method yield() làm cho thread hiện đang thực thi tạm dừng và cho phép các thread khác thực thi.

Mọi người lưu ý, đây chỉ là hint cho scheduler tạm dừng thread, scheduler có thể bỏ qua cái hint này.

Method này có thể dùng để tái hiện bug do race condition. Tuy nhiên, method này hiếm khi được sử dụng và mình recommend không dùng method này trong production code.

5.6. join()

Method join() cho phép một thread chờ đợi một thread khác hoàn thành. Điều này có thể hữu ích khi bạn cần đảm bảo hoàn thành một số nhiệm vụ nhất định trước khi tiếp tục. Khi thread A gọi method join() của thread B, thread A sẽ chuyển sang trạng thái chờ (RUNNABLE → WAITING). Nó vẫn ở trạng thái chờ cho đến khi thread B kết thúc.

Giả sử bạn cần thực hiện một số lệnh gọi API đến các endpoints khác nhau lấy dữ liệu đồng thời. Mỗi lệnh gọi API được thực hiện trong một thread riêng biệt và bạn muốn đợi cho đến khi tất cả các thread hoàn thành yêu cầu API của chúng trước khi tổng hợp (aggregate) kết quả.

String[] apiEndpoints = {
    "https://api.example.com/data1",
    "https://api.example.com/data2",
    "https://api.example.com/data3"
};

List<Thread> threads = new ArrayList<>();

List<String> results = new ArrayList<>();

for (String endpoint : apiEndpoints) {
    Thread thread = new Thread(() -> {
        String response = makeApiCall(endpoint);
        synchronized (results) {
            results.add(response);
        }
    });
    threads.add(thread);
    thread.start();
}

// Wait for all threads to complete
try {
    for (Thread thread : threads) {
        thread.join();
    }
} catch (InterruptedException e) {
    e.printStackTrace();
}

// Process and aggregate results
results.forEach(response -> System.out.println("API response: " + response));
java
middle

Bài viết liên quan

Java Virtual Thread: Cuộc cách mạng cho lập trình đồng thời

By @wuan.580 Bạn đã bao giờ viết một ứng dụng xử lý hàng ngàn request cùng lúc, và cảm thấy như mình đang chiến đấu với chính Java? Bạn từng dùng ThreadPoolExecutor và vắt óc cân chỉnh số lượng thread cho "vừa đủ dùng", tránh thiếu nhưng cũng không dám dư vì sợ OutOfMemoryError? Bạn từng nhăn mặt khi phải viết những dòng code callback chằng chịt, chỉ để tránh block một luồng? Và rồi đau đầu gỡ bug vì stacktrace rối như tơ vò? Nếu câu trả lời là "có", thì bạn không đơn độc. Và bạn cũng sắp có

Unit Testing (Phần 1)

Unit Test giúp dự án phần mềm phát triển một cách bền vững.

Materialized view với PostgreSQL

Khái niệm view trong relational database chắc hẳn đã quá quen thuộc với anh em dev. View chỉ lưu trữ câu lệnh truy vấn, không lưu kết quả truy vấn và thực hiện truy vấn trên (các) bảng gốc mỗi khi view được truy cập. Vậy nếu truy vấn của view là một truy vấn phức tạp cho thống kế, cần JOIN nhiều bảng, nhiều điều kiện FILTER, tập dữ liệu ở bảng gốc lớn, có nhiều phép tổng hợp như SUM, AVG… Thì các bạn có đoán được vấn đề gì sẽ xuất hiện không? Nếu câu trả lời của các bạn là “hiệu suất của truy

DFS & BFS - Khi dữ liệu không được lưu trữ một cách tuyến tính

By Bùi Đức Toàn “Muốn tìm trân châu nơi đáy biển, ắt phải lặn sâu. Muốn nhìn toàn cảnh rừng xanh, phải leo lên đỉnh núi.” - Cổ nhân. Từ thuở khai hoang, con người khi thì đào sâu xuống lòng đất để tìm vàng, khi thì lại lần theo dòng sông để biết nó chảy về đâu. Trong thế giới khoa học máy tính, tinh thần khai hoang đó được tái hiện qua hai chiến lược kinh điển trong việc duyệt đồ thị là: 1. Tìm kiếm theo chiều sâu (Depth-First Search – DFS). 2. Tìm kiếm theo chiều rộng (Breadth-First Search

Stack & Queue - Khi array không chỉ để lưu trữ

Stack & Queue là hai cấu trúc dữ liệu cơ bản và phổ biến trong con đường của một lập trình viên, hẳn rằng những lập trình viên có kinh nghiệm đều đã nghe, đã biết đến cấu trúc dữ liệu này, vậy cuối cùng thì, Stack & Queue là gì và nó có tác dụng thế nào? QUEUE - HÀNG ĐỢI Để bắt đầu nhẹ nhàng, chúng ta bắt đầu với Queue trước. Trước tiên thì Queue là một cấu trúc dữ liệu có dạng FIFO (First In Frist Out) tức là vào trước thì ra trước. Một ví dụ dễ thấy trong cuộc sống hàng ngày là việc xếp

Tất cả bài viết
logo

HỘ KINH DOANH LẬP VƯƠNG

Giấy chứng nhận đăng ký doanh nghiệp số: 8656162915-001. Cấp ngày 21/02/2024. Nơi cấp: Sở Kế hoạch và Đầu tư TP. Hà Nội

PHƯƠNG THỨC THANH TOÁN

vnpay

LIÊN HỆ

roninengineer88@gmail.com

0362228388

26 ngõ 156 Hồng Mai, Hai Bà Trưng, Hà Nội

THEO DÕI CHÚNG TÔI

Facebook

Youtube

Tiktok

CHÍNH SÁCH

Chính sách bảo mật

Chính sách thanh toán

Đổi trả/Hoàn tiền

Hướng dẫn thanh toán VNPAY

PHƯƠNG THỨC THANH TOÁN

vnpay

Ronin Engineer 2024