CodeGym /Blog Java /Ngẫu nhiên /Cùng nhau tốt hơn: Java và lớp Thread. Phần III — Tương t...
John Squirrels
Mức độ
San Francisco

Cùng nhau tốt hơn: Java và lớp Thread. Phần III — Tương tác

Xuất bản trong nhóm
Tổng quan ngắn gọn về các chi tiết cụ thể về cách các luồng tương tác. Trước đây, chúng ta đã xem cách các luồng được đồng bộ hóa với nhau. Lần này chúng ta sẽ đi sâu vào các vấn đề có thể phát sinh khi các luồng tương tác với nhau và chúng ta sẽ nói về cách tránh chúng. Chúng tôi cũng sẽ cung cấp một số liên kết hữu ích để nghiên cứu sâu hơn. Cùng nhau tốt hơn: Java và lớp Thread.  Phần III — Tương tác - 1

Giới thiệu

Vì vậy, chúng ta biết rằng Java có luồng. Bạn có thể đọc về điều đó trong bài đánh giá có tựa đề Cùng nhau tốt hơn: Java và lớp Chủ đề. Phần I - Chủ đề thực hiện . Và chúng tôi đã khám phá ra thực tế rằng các luồng có thể đồng bộ hóa với nhau trong bài đánh giá có tiêu đề Cùng nhau tốt hơn: Java và lớp Chủ đề. Phần II — Đồng bộ hóa . Đã đến lúc nói về cách các luồng tương tác với nhau. Làm thế nào để họ chia sẻ tài nguyên được chia sẻ? Những vấn đề gì có thể phát sinh ở đây? Cùng nhau tốt hơn: Java và lớp Thread.  Phần III — Tương tác - 2

Bế tắc

Vấn đề đáng sợ nhất là bế tắc. Bế tắc là khi hai hoặc nhiều luồng đang chờ luồng kia mãi mãi. Chúng tôi sẽ lấy một ví dụ từ trang web Oracle mô tả bế tắc :

public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s bowed to me!%n",
                    this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s bowed back to me!%n",
                    this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse = new Friend("Alphonse");
        final Friend gaston = new Friend("Gaston");
        new Thread(() -> alphonse.bow(gaston)).start();
        new Thread(() -> gaston.bow(alphonse)).start();
    }
}
Bế tắc có thể không xảy ra ở đây lần đầu tiên, nhưng nếu chương trình của bạn bị treo, thì đã đến lúc chạy jvisualvm: Cùng nhau tốt hơn: Java và lớp Thread.  Phần III — Tương tác - 3Với plugin JVisualVM được cài đặt (thông qua Công cụ -> Plugin), chúng ta có thể thấy nơi xảy ra bế tắc:

"Thread-1" - Thread t@12
   java.lang.Thread.State: BLOCKED
	at Deadlock$Friend.bowBack(Deadlock.java:16)
	- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
Chủ đề 1 đang chờ khóa từ chủ đề 0. Tại sao điều đó lại xảy ra? Thread-1bắt đầu chạy và thực thi Friend#bowphương thức. Nó được đánh dấu bằng synchronizedtừ khóa, có nghĩa là chúng tôi đang lấy màn hình cho this(đối tượng hiện tại). Đầu vào của phương thức là một tham chiếu đến Friendđối tượng khác. Bây giờ, Thread-1muốn thực thi phương thức trên phương thức kia Friendvà phải lấy khóa của nó để thực hiện. Nhưng nếu luồng khác (trong trường hợp này Thread-0) quản lý để nhập bow()phương thức, thì khóa đã được lấy và Thread-1đợiThread-0, và ngược lại. Đây là bế tắc không thể giải quyết được, và chúng tôi gọi nó là bế tắc. Giống như một cái kìm chết chóc không thể thoát ra, bế tắc là sự ngăn chặn lẫn nhau không thể phá vỡ. Để biết cách giải thích khác về bế tắc, bạn có thể xem video này: Giải thích về bế tắc và bế tắc .

ổ khóa

Nếu có bế tắc, thì cũng có livelock? Vâng, có :) Livelock xảy ra khi các chủ đề bên ngoài dường như vẫn còn sống, nhưng chúng không thể làm bất cứ điều gì, bởi vì (các) điều kiện cần thiết để chúng tiếp tục công việc của mình không thể được đáp ứng. Về cơ bản, livelock tương tự như bế tắc, nhưng các luồng không "treo" chờ màn hình. Thay vào đó, họ mãi mãi làm một cái gì đó. Ví dụ:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class App {
    public static final String ANSI_BLUE = "\u001B[34m";
    public static final String ANSI_PURPLE = "\u001B[35m";
    
    public static void log(String text) {
        String name = Thread.currentThread().getName(); // Like "Thread-1" or "Thread-0"
        String color = ANSI_BLUE;
        int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
        if (val != 0) {
            color = ANSI_PURPLE;
        }
        System.out.println(color + name + ": " + text + color);
        try {
            System.out.println(color + name + ": wait for " + val + " sec" + color);
            Thread.currentThread().sleep(val * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Lock first = new ReentrantLock();
        Lock second = new ReentrantLock();

        Runnable locker = () -> {
            boolean firstLocked = false;
            boolean secondLocked = false;
            try {
                while (!firstLocked || !secondLocked) {
                    firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
                    log("First Locked: " + firstLocked);
                    secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
                    log("Second Locked: " + secondLocked);
                }
                first.unlock();
                second.unlock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        new Thread(locker).start();
        new Thread(locker).start();
    }
}
Sự thành công của mã này phụ thuộc vào thứ tự mà bộ lập lịch luồng Java bắt đầu các luồng. Nếu Thead-1bắt đầu trước, thì chúng tôi nhận được livelock:

Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
Như bạn có thể thấy từ ví dụ, cả hai luồng đều cố gắng lấy cả hai khóa lần lượt, nhưng chúng không thành công. Nhưng, họ không bế tắc. Bề ngoài, mọi thứ đều ổn và họ đang làm công việc của mình. Cùng nhau tốt hơn: Java và lớp Thread.  Phần III — Tương tác - 4Theo JVisualVM, chúng ta thấy các khoảng thời gian ngủ và khoảng thời gian dừng (đây là khi một luồng cố gắng đạt được khóa — nó chuyển sang trạng thái dừng, như chúng ta đã thảo luận trước đó khi nói về đồng bộ hóa luồng ) . Bạn có thể xem một ví dụ về livelock tại đây: Java - Thread Livelock .

chết đói

Ngoài bế tắc và bế tắc, có một vấn đề khác có thể xảy ra trong quá trình đa luồng: chết đói. Hiện tượng này khác với các hình thức chặn trước đó ở chỗ các luồng không bị chặn — đơn giản là chúng không có đủ tài nguyên. Kết quả là, trong khi một số luồng chiếm toàn bộ thời gian thực hiện, thì một số luồng khác không thể chạy: Cùng nhau tốt hơn: Java và lớp Thread.  Phần III — Tương tác - 5

https://www.logicbig.com/

Bạn có thể xem một siêu ví dụ ở đây: Java - Thread Starvation and Fairness . Ví dụ này cho thấy điều gì sẽ xảy ra với các luồng trong quá trình bỏ đói và cách một thay đổi nhỏ từ Thread.sleep()việc Thread.wait()cho phép bạn phân phối tải đồng đều. Cùng nhau tốt hơn: Java và lớp Thread.  Phần III — Tương tác - 6

điều kiện cuộc đua

Trong đa luồng, có một thứ gọi là "điều kiện chủng tộc". Hiện tượng này xảy ra khi các luồng chia sẻ tài nguyên, nhưng mã được viết theo cách không đảm bảo chia sẻ chính xác. Hãy xem một ví dụ:

public class App {
    public static int value = 0;

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                int oldValue = value;
                int newValue = ++value;
                if (oldValue + 1 != newValue) {
                    throw new IllegalStateException(oldValue + " + 1 = " + newValue);
                }
            }
        };
        new Thread(task).start();
        new Thread(task).start();
        new Thread(task).start();
    }
}
Mã này có thể không tạo ra lỗi lần đầu tiên. Khi nó xảy ra, nó có thể trông như thế này:

Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
	at App.lambda$main$0(App.java:13)
	at java.lang.Thread.run(Thread.java:745)
Như bạn có thể thấy, đã xảy ra sự cố khi newValueđược gán một giá trị. newValueto quá. Do điều kiện tương tranh, một trong các chuỗi đã quản lý để thay đổi biến valuegiữa hai câu lệnh. Nó chỉ ra rằng có một cuộc chạy đua giữa các chủ đề. Bây giờ hãy nghĩ về tầm quan trọng của việc không mắc các lỗi tương tự với các giao dịch tiền tệ... Bạn cũng có thể xem các ví dụ và sơ đồ tại đây: Mã mô phỏng điều kiện chủng tộc trong chuỗi Java .

Bay hơi

Nói về sự tương tác của các chủ đề, từ volatilekhóa đáng nói. Hãy xem xét một ví dụ đơn giản:

public class App {
    public static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Runnable whileFlagFalse = () -> {
            while(!flag) {
            }
            System.out.println("Flag is now TRUE");
        };

        new Thread(whileFlagFalse).start();
        Thread.sleep(1000);
        flag = true;
    }
}
Thú vị nhất, điều này rất có thể không hoạt động. Chủ đề mới sẽ không thấy sự thay đổi trong flagtrường. Để khắc phục điều này cho flagtrường, chúng ta cần sử dụng volatiletừ khóa. Như thế nào và tại sao? Bộ xử lý thực hiện tất cả các hành động. Nhưng kết quả tính toán phải được lưu trữ ở đâu đó. Đối với điều này, có bộ nhớ chính và bộ nhớ cache của bộ xử lý. Bộ nhớ cache của bộ xử lý giống như một đoạn bộ nhớ nhỏ được sử dụng để truy cập dữ liệu nhanh hơn so với khi truy cập bộ nhớ chính. Nhưng mọi thứ đều có nhược điểm: dữ liệu trong bộ đệm có thể không được cập nhật (như trong ví dụ trên, khi giá trị của trường cờ không được cập nhật). Nênvolatiletừ khóa cho JVM biết rằng chúng tôi không muốn lưu trữ biến của mình. Điều này cho phép nhìn thấy kết quả cập nhật trên tất cả các luồng. Đây là một lời giải thích rất đơn giản. Đối với volatiletừ khóa, tôi thực sự khuyên bạn nên đọc bài viết này . Để biết thêm thông tin, tôi cũng khuyên bạn nên đọc Java Memory ModelJava Volatile Keyword . Ngoài ra, điều quan trọng cần nhớ volatilelà về khả năng hiển thị chứ không phải về tính nguyên tử của các thay đổi. Xem mã trong phần "Điều kiện chủng tộc", chúng ta sẽ thấy chú giải công cụ trong IntelliJ IDEA: Cùng nhau tốt hơn: Java và lớp Thread.  Phần III — Tương tác - 7Kiểm tra này đã được thêm vào IntelliJ IDEA như một phần của vấn đề IDEA-61117 , được liệt kê trong Ghi chú phát hành vào năm 2010.

nguyên tử

Phép toán nguyên tử là phép toán không thể chia nhỏ. Ví dụ, thao tác gán giá trị cho một biến phải là nguyên tử. Thật không may, thao tác gia tăng không phải là nguyên tử, bởi vì gia số yêu cầu tới ba thao tác CPU: lấy giá trị cũ, thêm một giá trị vào đó, sau đó lưu giá trị. Tại sao tính nguyên tử lại quan trọng? Với thao tác gia tăng, nếu xảy ra tình trạng chạy đua thì tài nguyên dùng chung (tức là giá trị dùng chung) có thể đột ngột thay đổi bất cứ lúc nào. Ngoài ra, các hoạt động liên quan đến cấu trúc 64 bit, chẳng hạn như longdouble, không phải là nguyên tử. Bạn có thể đọc thêm chi tiết tại đây: Đảm bảo tính nguyên tử khi đọc và ghi các giá trị 64 bit . Các vấn đề liên quan đến tính nguyên tử có thể được nhìn thấy trong ví dụ này:

public class App {
    public static int value = 0;
    public static AtomicInteger atomic = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                value++;
                atomic.incrementAndGet();
            }
        };
        for (int i = 0; i < 3; i++) {
            new Thread(task).start();
        }
        Thread.sleep(300);
        System.out.println(value);
        System.out.println(atomic.get());
    }
}
Lớp học đặc biệt AtomicIntegersẽ luôn cho chúng tôi 30.000, nhưng sẽ valuethay đổi theo thời gian. Có một tổng quan ngắn về chủ đề này: Giới thiệu về các biến nguyên tử trong Java . Thuật toán "so sánh và hoán đổi" nằm ở trung tâm của các lớp nguyên tử. Bạn có thể đọc thêm về nó ở đây trong So sánh các thuật toán không khóa - CAS và FAA trên ví dụ về JDK 7 và 8 hoặc trong bài viết So sánh và hoán đổi trên Wikipedia. Cùng nhau tốt hơn: Java và lớp Thread.  Phần III — Tương tác - 9

http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html

xảy ra trước

Có một khái niệm thú vị và bí ẩn được gọi là "xảy ra trước". Là một phần của nghiên cứu của bạn về chủ đề, bạn nên đọc về nó. Mối quan hệ xảy ra trước hiển thị thứ tự các hành động giữa các luồng sẽ được nhìn thấy. Có nhiều cách giải thích và bình luận. Đây là một trong những bài thuyết trình gần đây nhất về chủ đề này: Các mối quan hệ "Xảy ra trước" của Java .

Bản tóm tắt

Trong bài đánh giá này, chúng tôi đã khám phá một số chi tiết cụ thể về cách các luồng tương tác. Chúng tôi đã thảo luận các vấn đề có thể phát sinh, cũng như các cách để xác định và loại bỏ chúng. Danh sách các tài liệu bổ sung về chủ đề: Kết hợp tốt hơn: Java và lớp Thread. Phần I - Các luồng thực thi Cùng nhau tốt hơn: Java và lớp Thread. Phần II — Đồng bộ hóa Tốt hơn khi kết hợp với nhau: Java và lớp Thread. Phần IV - Có thể gọi được, Tương lai và bạn bè Tốt hơn cùng nhau: Java và lớp Chủ đề. Phần V — Executor, ThreadPool, Fork/Join Together tốt hơn: Java và lớp Thread. Phần VI — Bắn đi!
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION