Vòng lặp không thấy giá trị được thay đổi bởi chuỗi khác mà không có câu lệnh in


89

Trong mã của tôi, tôi có một vòng lặp chờ một số trạng thái được thay đổi từ một luồng khác. Luồng khác hoạt động, nhưng vòng lặp của tôi không bao giờ thấy giá trị đã thay đổi. Nó đợi mãi. Tuy nhiên, khi tôi đặt một System.out.printlncâu lệnh trong vòng lặp, nó đột nhiên hoạt động! Tại sao?


Sau đây là một ví dụ về mã của tôi:

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        while (pizzaArrived == false) {
            //System.out.println("waiting");
        }

        System.out.println("That was delicious!");
    }

    void deliverPizza() {
        pizzaArrived = true;
    }
}

Trong khi vòng lặp while đang chạy, tôi gọi deliverPizza()từ một luồng khác để đặt pizzaArrivedbiến. Nhưng vòng lặp chỉ hoạt động khi tôi bỏ ghi chú System.out.println("waiting");câu lệnh. Chuyện gì vậy?

Câu trả lời:


148

JVM được phép giả định rằng các luồng khác không thay đổi pizzaArrivedbiến trong vòng lặp. Nói cách khác, nó có thể nâng pizzaArrived == falsekiểm tra ra bên ngoài vòng lặp, tối ưu hóa điều này:

while (pizzaArrived == false) {}

vào cái này:

if (pizzaArrived == false) while (true) {}

đó là một vòng lặp vô hạn.

Để đảm bảo rằng các thay đổi được thực hiện bởi một luồng sẽ hiển thị với các luồng khác, bạn phải luôn thêm một số đồng bộ hóa giữa các luồng. Cách đơn giản nhất để làm điều này là tạo biến chia sẻ volatile:

volatile boolean pizzaArrived = false;

Việc tạo một biến volatileđảm bảo rằng các luồng khác nhau sẽ thấy tác động của những thay đổi lẫn nhau đối với nó. Điều này ngăn JVM lưu vào bộ nhớ đệm giá trị của pizzaArrivedhoặc nâng thử nghiệm ra bên ngoài vòng lặp. Thay vào đó, nó phải đọc giá trị của biến thực mọi lúc.

(Chính thức hơn, volatiletạo mối quan hệ xảy ra trước khi truy cập vào biến. Điều này có nghĩa là tất cả các công việc khác mà một chuỗi đã thực hiện trước khi giao bánh pizza cũng hiển thị với chuỗi nhận bánh pizza, ngay cả khi những thay đổi khác đó không phải đối với volatilebiến.)

Các phương pháp đồng bộ được sử dụng chủ yếu để thực hiện loại trừ lẫn nhau (ngăn chặn hai điều xảy ra cùng một lúc), nhưng chúng cũng có tất cả các tác dụng phụ giống nhau volatile. Sử dụng chúng khi đọc và ghi một biến là một cách khác để làm cho các thay đổi hiển thị với các chuỗi khác:

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        while (getPizzaArrived() == false) {}
        System.out.println("That was delicious!");
    }

    synchronized boolean getPizzaArrived() {
        return pizzaArrived;
    }

    synchronized void deliverPizza() {
        pizzaArrived = true;
    }
}

Tác dụng của một câu lệnh in

System.outlà một PrintStreamđối tượng. Các phương thức của PrintStreamđược đồng bộ hóa như thế này:

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

Các ngăn chặn đồng bộ hóa pizzaArrivedđược lưu trữ trong vòng lặp. Nói một cách chính xác, cả hai luồng phải đồng bộ hóa trên cùng một đối tượng để đảm bảo rằng các thay đổi đối với biến có thể nhìn thấy được. (Ví dụ: gọi printlnsau khi thiết lập pizzaArrivedvà gọi lại trước khi đọc pizzaArrivedsẽ đúng.) Nếu chỉ một luồng đồng bộ hóa trên một đối tượng cụ thể, JVM được phép bỏ qua nó. Trong thực tế, JVM không đủ thông minh để chứng minh rằng các luồng khác sẽ không gọi printlnsau khi thiết lập pizzaArrived, vì vậy nó giả định rằng chúng có thể. Do đó, nó không thể cache biến trong vòng lặp nếu bạn gọi System.out.println. Đó là lý do tại sao các vòng lặp như thế này hoạt động khi chúng có một câu lệnh in, mặc dù nó không phải là một bản sửa lỗi chính xác.

Sử dụng System.outkhông phải là cách duy nhất để gây ra hiệu ứng này, nhưng nó là cách mà mọi người thường phát hiện ra nhất, khi họ đang cố gắng gỡ lỗi tại sao vòng lặp của họ không hoạt động!


Vấn đề lớn hơn

while (pizzaArrived == false) {}là một vòng lặp chờ đợi bận rộn. Điều đó thật xấu! Trong khi chờ đợi, nó sẽ làm kẹt CPU, làm chậm các ứng dụng khác và làm tăng mức sử dụng năng lượng, nhiệt độ và tốc độ quạt của hệ thống. Lý tưởng nhất, chúng tôi muốn chuỗi vòng lặp ở trạng thái ngủ trong khi nó chờ, vì vậy nó không làm hỏng CPU.

Dưới đây là một số cách để làm điều đó:

Sử dụng chờ / thông báo

Một giải pháp cấp thấp là sử dụng các phương pháp chờ / thông báo củaObject :

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        synchronized (this) {
            while (!pizzaArrived) {
                try {
                    this.wait();
                } catch (InterruptedException e) {}
            }
        }

        System.out.println("That was delicious!");
    }

    void deliverPizza() {
        synchronized (this) {
            pizzaArrived = true;
            this.notifyAll();
        }
    }
}

Trong phiên bản mã này, chuỗi lặp sẽ gọi wait(), điều này đặt chuỗi ở trạng thái ngủ. Nó sẽ không sử dụng bất kỳ chu kỳ CPU nào trong khi ngủ. Sau khi luồng thứ hai thiết lập biến, nó sẽ gọi notifyAll()để đánh thức bất kỳ / tất cả các luồng đang đợi trên đối tượng đó. Điều này giống như việc anh chàng bán pizza bấm chuông cửa, vì vậy bạn có thể ngồi xuống và nghỉ ngơi trong khi chờ đợi, thay vì đứng lúng túng trước cửa.

Khi gọi đợi / thông báo trên một đối tượng, bạn phải giữ khóa đồng bộ hóa của đối tượng đó, đó là những gì đoạn mã trên thực hiện. Bạn có thể sử dụng bất kỳ đối tượng nào bạn thích miễn là cả hai luồng sử dụng cùng một đối tượng: ở đây tôi đã sử dụng this(ví dụ của MyHouse). Thông thường, hai luồng sẽ không thể nhập các khối được đồng bộ hóa của cùng một đối tượng đồng thời (là một phần của mục đích đồng bộ hóa) nhưng nó hoạt động ở đây vì một luồng tạm thời giải phóng khóa đồng bộ hóa khi nó ở bên trong wait()phương thức.

BlockingQueue

A BlockingQueueđược sử dụng để triển khai hàng đợi của người sản xuất-người tiêu dùng. "Người tiêu dùng" lấy các mặt hàng từ phía trước của hàng đợi, và "nhà sản xuất" đẩy các mặt hàng ở phía sau. Một ví dụ:

class MyHouse {
    final BlockingQueue<Object> queue = new LinkedBlockingQueue<>();

    void eatFood() throws InterruptedException {
        // take next item from the queue (sleeps while waiting)
        Object food = queue.take();
        // and do something with it
        System.out.println("Eating: " + food);
    }

    void deliverPizza() throws InterruptedException {
        // in producer threads, we push items on to the queue.
        // if there is space in the queue we can return immediately;
        // the consumer thread(s) will get to it later
        queue.put("A delicious pizza");
    }
}

Lưu ý: Các puttakephương thức của BlockingQueuecó thể ném InterruptedExceptions, là các ngoại lệ đã được kiểm tra và phải được xử lý. Trong đoạn mã trên, để đơn giản, các trường hợp ngoại lệ được phát triển lại. Bạn có thể muốn nắm bắt các ngoại lệ trong các phương thức và thử lại lệnh đặt hoặc nhận để đảm bảo nó thành công. Ngoài một điểm xấu, BlockingQueuerất dễ sử dụng.

Không cần đồng bộ hóa khác ở đây vì a BlockingQueueđảm bảo rằng mọi thứ mà các chuỗi đã thực hiện trước khi đưa các mục vào hàng đợi sẽ hiển thị cho các chuỗi lấy các mục đó ra.

Người thừa hành

Executors giống như BlockingQueues làm sẵn thực thi các tác vụ. Thí dụ:

// A "SingleThreadExecutor" has one work thread and an unlimited queue
ExecutorService executor = Executors.newSingleThreadExecutor();

Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };

// we submit tasks which will be executed on the work thread
executor.execute(eatPizza);
executor.execute(cleanUp);
// we continue immediately without needing to wait for the tasks to finish

Để biết chi tiết xem doc cho Executor, ExecutorServiceExecutors.

Xử lý sự kiện

Việc lặp lại trong khi chờ người dùng nhấp vào thứ gì đó trong giao diện người dùng là sai. Thay vào đó, hãy sử dụng các tính năng xử lý sự kiện của bộ công cụ UI. Trong Swing , ví dụ:

JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
    // This event listener is run when the button is clicked.
    // We don't need to loop while waiting.
    label.setText("Button was clicked");
});

Bởi vì trình xử lý sự kiện chạy trên chuỗi điều phối sự kiện, thực hiện công việc dài trong trình xử lý sự kiện sẽ chặn các tương tác khác với giao diện người dùng cho đến khi công việc kết thúc. Các hoạt động chậm có thể được bắt đầu trên một luồng mới hoặc gửi đến một luồng đang chờ bằng một trong các kỹ thuật trên (chờ / thông báo, a BlockingQueue, hoặc Executor). Bạn cũng có thể sử dụng một SwingWorker, được thiết kế chính xác cho việc này và tự động cung cấp một luồng nhân viên nền:

JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");

// Add a click listener for the button
button.addActionListener((ActionEvent e) -> {

    // Defines MyWorker as a SwingWorker whose result type is String:
    class MyWorker extends SwingWorker<String,Void> {
        @Override
        public String doInBackground() throws Exception {
            // This method is called on a background thread.
            // You can do long work here without blocking the UI.
            // This is just an example:
            Thread.sleep(5000);
            return "Answer is 42";
        }

        @Override
        protected void done() {
            // This method is called on the Swing thread once the work is done
            String result;
            try {
                result = get();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            label.setText(result); // will display "Answer is 42"
        }
    }

    // Start the worker
    new MyWorker().execute();
});

Bộ hẹn giờ

Để thực hiện các hành động định kỳ, bạn có thể sử dụng a java.util.Timer. Nó dễ sử dụng hơn là viết vòng lặp thời gian của riêng bạn, và dễ dàng hơn để bắt đầu và dừng lại. Bản trình diễn này in thời gian hiện tại một lần mỗi giây:

Timer timer = new Timer();
TimerTask task = new TimerTask() {
    @Override
    public void run() {
        System.out.println(System.currentTimeMillis());
    }
};
timer.scheduleAtFixedRate(task, 0, 1000);

Mỗi java.util.Timerluồng có một luồng nền riêng được sử dụng để thực thi các chuỗi theo lịch trình của nó TimerTask. Đương nhiên, luồng ngủ giữa các tác vụ, vì vậy nó không ảnh hưởng đến CPU.

Trong mã Swing, cũng có một javax.swing.Timer, tương tự, nhưng nó thực thi trình lắng nghe trên luồng Swing, vì vậy bạn có thể tương tác một cách an toàn với các thành phần của Swing mà không cần chuyển đổi thủ công các luồng:

JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Timer timer = new Timer(1000, (ActionEvent e) -> {
    frame.setTitle(String.valueOf(System.currentTimeMillis()));
});
timer.setRepeats(true);
timer.start();
frame.setVisible(true);

Những cách khác

Nếu bạn đang viết mã đa luồng, bạn nên khám phá các lớp trong các gói này để xem những gì có sẵn:

Và cũng có thể xem phần Concurrency trong các hướng dẫn Java. Đa luồng rất phức tạp, nhưng có rất nhiều trợ giúp!


Câu trả lời rất chuyên nghiệp, sau khi đọc không này quan niệm sai lầm là trái vào tâm trí của tôi, cảm ơn bạn
Humoyun Ahmad

1
Câu trả lời tuyệt vời. Tôi đang làm việc với các luồng Java trong một thời gian khá dài và vẫn học được điều gì đó ở đây ( wait()giải phóng khóa đồng bộ hóa!).
brimborium

Cảm ơn bạn, Boann! Câu trả lời tuyệt vời, nó giống như một bài báo đầy đủ với các ví dụ! Vâng, cũng thích "wait () ra mắt khóa đồng bộ"
Kiryl Ivanou

java public class ThreadTest { private static boolean flag = false; private static class Reader extends Thread { @Override public void run() { while(flag == false) {} System.out.println(flag); } } public static void main(String[] args) { new Reader().start(); flag = true; } } @Boann, mã này không treo pizzaArrived == falsethử nghiệm bên ngoài vòng lặp và vòng lặp có thể thấy cờ được thay đổi bởi luồng chính, tại sao?
gaussclb

1
@gaussclb Nếu ý bạn là bạn đã dịch ngược một tệp lớp, hãy sửa lại. Trình biên dịch Java hầu như không tối ưu hóa. Việc cẩu được thực hiện bởi JVM. Bạn cần phải tháo rời mã máy gốc. Hãy thử: wiki.openjdk.java.net/display/HotSpot/PrintAssembly
Boann
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.