Trợ thủ của đa luồng

Trong lập trình đa luồng, có 2 vấn đề tương đối quan trọng đó là thông báo và đồng bộ dữ liệu. Nắm được điều này, các kỹ sư đã đưa ra 2 công cụ rất hữu ích đó là volatileatomic để giúp chúng ta giải quyết vấn đề nhanh chóng hơn và sử dụng ít dòng code hơn.

Volatile

Mình thường sử dụng volatile giống như một biến thông báo (mặc dù cơ chế bên trong có thể khác), nghĩa là nó được dùng để các thread thông báo cho nhau rằng biến có sự thay đổi giá trị, tuy nhiên nó không đảm bảo được là giá trị của biến sẽ được đồng bộ.

Ví dụ thông báo

Ở ví dụ này chúng ta sẽ có 2 thread, 1 thread làm nhiệm vụ chuẩn bị (prepare), và một thread làm nhiệu vụ thực thi (start), luồng chuẩn bị sau khi thực hiện xong sẽ cần thông báo cho luồng thực thi để nó bắt đầu (Ví dụ đầy đủ bạn có thể xem tại đây)

public class VolatileExample {

    private volatile boolean active;

    public void prepare() throws InterruptedException {
        new Thread(() -> {
            System.out.println("application preparing ...");
            sleep(3);
            active = true;
        })
        .start();
    }

    public void start() throws Exception {
        new Thread(() -> {
            while(!active);
            System.out.println("application started");
        })
        .start();
    }

    private static void sleep(int second) {
        try {
            Thread.sleep(second * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        VolatileExample example = new VolatileExample();
        example.prepare();
        example.start();
        sleep(10);
    }
}

Ở trong ví dụ này, nếu bạn bỏ từ khoá volatile thì hàm System.out.println("application started"); sẽ không bao giờ được in ra, vì sao vậy? Vì bản chất nếu chúng ta không khai báo volatile thì luồng thực thi sẽ không biết đến sự thay đổi giá trị active của luồng chuẩn bị, từ đó dẫn đến việc luồng chuẩn bị thay đổi giá trị active = true nhưng luồng thực thi cũng không thể nào biết được.

Một cách thay thế

Ngoài cách dùng trực tiếp từ khoá volatile, chúng ta cũng có thể tự tạo ra một lớp với chức năng tương tự, nó sẽ kiểu thế này:

  1. Trạng thái ban đầu được set là chưa thay đổi
  2. Khi có một luồng gọi đến, nó sẽ kiểm tra xem giá trị có sự thay đổi nào không, nếu không thì block cái luồng gọi lại và chờ đợi cho đến khi có sự thay đổi
  3. Khi có 1 luồng thay đổi nó sẽ thông báo đến các luồng đang chờ hãy thức dậy, vừa có sự thay đổi giá trị đó
  4. Các luồng đang bị block sẽ thức dậy và thực thi tiếp, và source code sẽ kiểu thế này (ví dụ đẩy đủ bạn có thể xem tại đây:
class Volatile<T> {
    private boolean changed;
    private T value;

    public Volatile(T intValue) {
        this.value = intValue;
    }

    public synchronized void set(T newValue) {
        value = newValue;
        changed = true;
        notifyAll();
    }

    public synchronized T get() {
        while (!changed) {
            try { wait(); } catch (Exception e) {}
        }
        changed = false;
        return value;
    }
}

Ví dụ không đảm bảo đồng bộ giá trị

Trong ví dụ này chúng ta sẽ tăng dần giá trị của biến count lên đến 1000000, và thống kê lại các giá trị thu được, nếu tổng số giá trị thu được là 1000000 thì nghĩa là violate có thể đồng bộ được giá trị, ngược lại thì không (ví dụ đầy đủ bạn có thể xem tại đây).

public class VolatileExample {

    private volatile int count;
    private final Map map = new ConcurrentHashMap();

    public void start() {
        Thread[] threads = new Thread[10];
        for (int i = 0 ; i < threads.length ; ++i) {
            threads[i] = new Thread(() -> {
                while(count <= 1000000) {
                    ++ count;
                    map.put(Integer.valueOf(count), Integer.valueOf(count));
                }
            });
        }
        for (int i = 0 ; i < threads.length ; ++i) {
            threads[i].start();
        }
    }

    public static void main(String[] args) throws Exception {
        VolatileExample example = new VolatileExample();
        example.start();
        Thread.sleep(3000);
        System.out.println(example.map.size());
    }
}

Kết quả là ví dụ sẽ in ra ngẫu nhiên, có thể là 979638 hay 979630, như vậy chúng ta có thể thấy volatile không thể dùng để đồng bộ dữ liệu được.

Atomic

Atomic là một phần trong bộ thư viện java.concurrent kể từ java 6, mình cũng dùng nó cho mục đích thông báo và đảm bảo giá trị sẽ được đồng bộ, vì bên trong nó sử dụng kiểu private volatile int value;. Nên bạn có thể yên tâm sử dụng để thay thế cho volatile nếu muốn. Chúng ta hãy cùng sử dụng Atomic cho các vụ dụ ở trên nhé.

Ví dụ thông báo

Vẫn là ví dụ thông báo giữ luồng chuẩn bị và luồng thực thi, nhưng lần này chúng ta sẽ sử dụng AtomicBoolean (ví dụ đầy đủ bạn có thể xem tại đây).

public class AtomicExample {

    private AtomicBoolean active = new AtomicBoolean(false);

    public void prepare() throws InterruptedException {
        new Thread(() -> {
            System.out.println("application preparing ...");
            sleep(3);
            active.set(true);
        })
        .start();
    }

    public void start() throws Exception {
        new Thread(() -> {
            while(!active.get());
            System.out.println("application started");
        })
        .start();
    }

    private static void sleep(int second) {
        try {
            Thread.sleep(second * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        AtomicExample example = new AtomicExample();
        example.prepare();
        example.start();
        sleep(10);
    }
}

Kếu quả chúng ta nhận được sẽ tương tự như khi sử dụng volatile.

Ví dụ đồng bộ dữ liệu

Cũng vẫn sử dụng lại vị với volatile ở trên nhưng lần này chúng ta sẽ sử dụng AtomicInteger (ví dụ đầy đủ bạn có thể xem tại đây).

public class AtomicIntExample {

    private AtomicInteger count = new AtomicInteger(0);
    private final Map map = new ConcurrentHashMap<>();

    public void start() {
        Thread[] threads = new Thread[10];
        for (int i = 0 ; i < threads.length ; ++i) {
            threads[i] = new Thread(() -> {
                int value;
                while((value = count.incrementAndGet()) <= 1000000) {
                    map.put(Integer.valueOf(value), Integer.valueOf(value));
                }
            });
        }
        for (int i = 0 ; i < threads.length ; ++i) {
            threads[i].start();
        }
    }

    public static void main(String[] args) throws Exception {
        AtomicIntExample example = new AtomicIntExample();
        example.start();
        Thread.sleep(3000);
        System.out.println(example.map.size());
    }
}

Kết quả chúng ta sẽ nhận được đúng con số 1000000, như vậy chúng ta có thể sử dụng Atomic để đồng bộ dữ liệu được.

Khi nào nên sử dụng Volatile

Bởi vì volatile không đảm bảo được việc đồng bộ giá trị nên mình cũng chủ yếu dùng để làm thông báo, như ví dụ ở trên, mình dùng để thông báo khi việc chuẩn bị đã hoàn tất và chương trình có thể thực thi.

Khi nào nên dùng Atomic

Về cơ bản thì Atomic có thể thay thế được cho volatile, tuy nhiên chúng ta cũng không nên lạm dụng nó, ví dụ:

class Service {
    public void start() {
        while(active) {
            // thực thi
        }
    }

    public void stop() {
        active = false;
    }
}

Trong ví dụ này chúng ta chỉ đơn thuần là dừng lại vòng lặp để kết thúc service, nên sử dụng volatile sẽ giúp code của chúng ta ngắn gọn hơn.

Tổng kết

Volatile và Atomic là 2 công cụ rất hữu ích khi chúng ta lập trình với đa luồng. Nếu cảm thấy khó hiểu quá bạn có thể sử đụng Atomic và quên volatile đi, hoặc nhớ là với boolean thì có thể sử dụng volatile, còn trường hợp khác thì Atomic mà dùng nhé.

Tham khảo

  1. Ví dụ trong bài viết