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

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.

Cache strategies - Lựa chọn chiến lược nào cho dự án của bạn?

I. Giới thiệu Bạn hẳn đã quen thuộc với khái niệm cache rồi nhỉ? Khi ứng dụng chạy chậm, giải pháp thường nghĩ đến là dùng cache – nghe có vẻ đơn giản. Nhưng triển khai cache như thế nào để vừa đạt hiệu quả cao, vừa đảm bảo tính chính xác của dữ liệu lại là một bài toán không hề dễ. Trong bài viết này, chúng ta sẽ cùng tìm hiểu 5 chiến lược caching phổ biến, phân tích ưu nhược điểm của từng chiến lược và khám phá cách áp dụng chúng vào các tình huống thực tế để tối ưu hiệu suất hệ thống nhé.

Lost Update: Tồn kho còn 1, nhiều người cùng order thì xử lý thế nào?

by @HieuHocCode I. Giới thiệu Hãy tưởng tượng bạn đang xây dựng một sàn thương mại điện tử và gặp phải tình huống sản phẩm chỉ còn 1, nhưng có đến 2 khách hàng đặt hàng cùng lúc. Làm thế nào để hệ thống xử lý tình huống này một cách chính xác, tránh sai sót? Đây chính là một thách thức phổ biến khi xử lý nhiều transaction đồng thời. Vấn đề này thường liên quan đến khái niệm race condition, trong đó các giao dịch song song sẽ tranh chấp quyền thao tác trên dữ liệu, dẫn đến những tình trạng sa

Distributed Lock Là Gì? Tại Sao Nó Quan Trọng và Cách Triển Khai Với Redis

by @ AnhDH Mục lục 1. Đặt vấn đề 2. Distributed Lock 3. Triển khai Distributed Lock với Redis 4. Best practice 5. Tổng kết 6. Tham khảo Trong các hệ thống phân tán, việc đảm bảo tính nhất quán của dữ liệu (data consistency) và ngăn chặn tranh chấp tài nguyên (race condition) là một thách thức lớn, đặc biệt khi nhiều tiến trình hoặc service truy cập đồng thời vào các tài nguyên dùng chung. Một trong những giải pháp quan trọng để giải quyết vấn đề này chính là sử dụng distributed lock (k

Hướng dẫn xây dựng Video Call Webapp đơn giản với WebRTC

by @thanbv1510 Mục lục 1. Giới thiệu về WebRTC 2. Sequence Diagram 3. Hướng dẫn code chi tiết 4. Demo 5. Tổng kết và hướng phát triển 1. Giới thiệu về WebRTC WebRTC là gì? WebRTC (Web Real-Time Communication) là một công nghệ mã nguồn mở cho phép truyền thông theo thời gian thực (real-time) giữa các trình duyệt web và ứng dụng di động thông qua giao thức peer-to-peer (P2P), không cần cài đặt plugin hay phần mềm bổ sung. Kiến trúc WebRTC bao gồm các thành phần chính sau: 1. Signali

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