루프는 print 문없이 다른 스레드에 의해 변경된 값을 보지 못함


91

내 코드에는 일부 상태가 다른 스레드에서 변경 될 때까지 기다리는 루프가 있습니다. 다른 스레드는 작동하지만 내 루프는 변경된 값을 볼 수 없습니다. 영원히 기다립니다. 그러나 System.out.println문을 루프에 넣으면 갑자기 작동합니다! 왜?


다음은 내 코드의 예입니다.

class MyHouse {
    boolean pizzaArrived = false;

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

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

    void deliverPizza() {
        pizzaArrived = true;
    }
}

while 루프가 실행되는 동안 deliverPizza()다른 스레드에서 호출 하여 pizzaArrived변수 를 설정합니다 . 그러나 루프는 System.out.println("waiting");명령문의 주석 처리를 제거 할 때만 작동 합니다. 무슨 일이야?

답변:


152

JVM은 다른 스레드가 pizzaArrived루프 중에 변수를 변경하지 않는다고 가정 할 수 있습니다 . 즉, pizzaArrived == false테스트를 루프 외부로 끌어 올려 다음을 최적화 할 수 있습니다 .

while (pizzaArrived == false) {}

이것으로 :

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

무한 루프입니다.

한 스레드에 의해 변경된 내용이 다른 스레드에 표시되도록하려면 항상 스레드간에 동기화 를 추가해야합니다 . 이를 수행하는 가장 간단한 방법은 공유 변수를 만드는 것입니다 volatile.

volatile boolean pizzaArrived = false;

변수를 만들면 volatile서로 다른 스레드가 서로의 변경 효과를 볼 수 있습니다. 이렇게하면 JVM pizzaArrived이 루프 외부에서 테스트 값을 캐싱 하거나 끌어 올릴 수 없습니다. 대신 매번 실제 변수의 값을 읽어야합니다.

(더 공식적으로 는 변수에 대한 액세스 사이에 발생 전 관계를 volatile생성 합니다. 이는 피자를 전달하기 전에 스레드가 수행 한 다른 모든 작업이 다른 변경 사항이 변수 가 아닌 경우에도 피자를받는 스레드에도 표시됨을 의미합니다 .)volatile

동기화 된 메서드 는 주로 상호 배제를 구현하는 데 사용되지만 (동시에 발생하는 두 가지 일을 방지) 모두 동일한 부작용이 volatile있습니다. 변수를 읽고 쓸 때이를 사용하는 것은 변경 사항을 다른 스레드에 표시하는 또 다른 방법입니다.

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;
    }
}

print 문의 효과

System.outA는 PrintStream객체. 의 메서드는 PrintStream다음과 같이 동기화됩니다.

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

동기화 pizzaArrived는 루프 중에 캐시되는 것을 방지 합니다. 엄밀히 말하면 스레드는 변수의 변경 사항이 표시되도록 동일한 객체 에서 동기화해야합니다 . (예를 들어, println설정 후 pizzaArrived호출하고 읽기 전에 다시 호출하는 pizzaArrived것이 정확합니다.) 특정 객체에서 하나의 스레드 만 동기화되는 경우 JVM은이를 무시할 수 있습니다. 실제로 JVM은 다른 스레드가을 println설정 한 후 호출하지 않는다는 것을 증명할만큼 똑똑하지 pizzaArrived않으므로 그럴 수 있다고 가정합니다. 따라서을 호출하면 루프 중에 변수를 캐시 할 수 없습니다 System.out.println. 이것이 올바른 수정은 아니지만 print 문이있을 때 이와 같은 루프가 작동하는 이유입니다.

사용 System.out하는 것이이 효과를 일으키는 유일한 방법은 아니지만, 루프가 작동하지 않는 이유를 디버깅하려고 할 때 사람들이 가장 자주 발견하는 방법입니다!


더 큰 문제

while (pizzaArrived == false) {}바쁜 대기 루프입니다. 그 나쁜! 기다리는 동안 CPU를 소모하여 다른 애플리케이션의 속도를 저하시키고 시스템의 전력 사용량, 온도 및 팬 속도를 증가시킵니다. 이상적으로는 루프 스레드가 대기하는 동안 잠자기 상태가되기를 원하므로 CPU를 잡아 먹지 않습니다.

이를 수행하는 몇 가지 방법은 다음과 같습니다.

대기 / 알림 사용

낮은 수준의 솔루션은 다음의 wait / notify 메서드Object사용하는 것입니다 .

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();
        }
    }
}

이 코드 버전에서 루프 스레드는를 호출 wait()하여 스레드를 절전 모드로 전환합니다. 수면 중에는 CPU주기를 사용하지 않습니다. 두 번째 스레드가 변수를 설정 한 후 notifyAll()해당 개체에서 대기중인 모든 스레드를 깨우기 위해 호출 합니다. 이것은 피자 녀석이 초인종을 울리는 것과 같으므로 문 앞에 서서 어색하게서는 대신 앉아서 기다리면서 휴식을 취할 수 있습니다.

객체에 대해 wait / notify를 호출 할 때 해당 객체의 동기화 잠금을 유지해야합니다. 이는 위 코드가 수행하는 작업입니다. 두 스레드가 동일한 객체를 사용하는 한 원하는 객체를 사용할 수 있습니다. 여기서는 this(의 인스턴스 MyHouse)를 사용했습니다. 일반적으로 두 스레드는 동일한 객체의 동기화 된 블록에 동시에 들어갈 수 없지만 (동기화 목적의 일부) 스레드가 wait()메서드 내부에있을 때 일시적으로 동기화 잠금을 해제하기 때문에 여기서 작동합니다 .

BlockingQueue

A BlockingQueue는 생산자-소비자 대기열을 구현하는 데 사용됩니다. "소비자"는 대기열의 앞쪽에서 항목을 가져오고 "생산자"는 뒤쪽에서 항목을 밀어 넣습니다. 예 :

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");
    }
}

참고 :의 puttake메서드는 처리해야하는 확인 된 예외 인 s BlockingQueue를 throw 할 수 있습니다 InterruptedException. 위의 코드에서는 단순화를 위해 예외가 다시 발생합니다. 메소드에서 예외를 포착하고 넣기를 재 시도하거나 호출이 성공하는지 확인하는 것을 선호 할 수 있습니다. 그 하나의 추악함을 제외하고는 BlockingQueue사용하기가 매우 쉽습니다.

BlockingQueue항목을 대기열에 넣기 전에 스레드가 수행 한 모든 작업이 해당 항목을 꺼내는 스레드에 표시되는지 확인 하기 때문에 여기에서 다른 동기화가 필요 하지 않습니다.

집행자

Executors는 BlockingQueue작업을 실행하는 기성품과 같습니다 . 예:

// 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

세부 사항에 대한 문서를 참조하십시오 Executor, ExecutorService하고 Executors.

이벤트 처리

사용자가 UI에서 무언가를 클릭하기를 기다리는 동안 반복하는 것은 잘못되었습니다. 대신 UI 툴킷의 이벤트 처리 기능을 사용하십시오. 예를 들어, Swing 에서 :

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");
});

이벤트 핸들러는 이벤트 디스패치 스레드에서 실행되기 때문에 이벤트 핸들러에서 장시간 작업하면 작업이 완료 될 때까지 UI와의 다른 상호 작용이 차단됩니다. 느린 작업은 새 스레드에서 시작되거나 위의 기술 (대기 / 알림, a BlockingQueue또는 Executor) 중 하나를 사용하여 대기중인 스레드로 디스패치 될 수 있습니다 . SwingWorker이를 위해 정확히 설계된를 사용할 수도 있으며 자동으로 백그라운드 작업자 스레드를 제공합니다.

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();
});

타이머

정기적 인 작업을 수행하려면 java.util.Timer. 자체 타이밍 루프를 작성하는 것보다 사용하기 쉽고 시작 및 중지하기가 더 쉽습니다. 이 데모는 현재 시간을 초당 한 번씩 인쇄합니다.

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

각각 java.util.Timer은 예약 된 TimerTasks 를 실행하는 데 사용되는 자체 백그라운드 스레드를 가지고 있습니다. 당연히 스레드는 작업 사이에 휴면하므로 CPU를 잡아 먹지 않습니다.

Swing 코드에는 javax.swing.Timer유사하지만 Swing 스레드에서 리스너를 실행하므로 수동으로 스레드를 전환 할 필요없이 Swing 구성 요소와 안전하게 상호 작용할 수 있습니다.

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);

다른 방법들

다중 스레드 코드를 작성하는 경우 다음 패키지의 클래스를 탐색하여 사용 가능한 것을 확인하는 것이 좋습니다.

또한 Java 자습서 의 동시성 섹션 을 참조하십시오 . 멀티 스레딩은 복잡하지만 많은 도움이 필요합니다!


매우 전문적인 답변,이 글을 읽은 후 내 마음에 오해가 남아 있지 않습니다. 감사합니다
Humoyun Ahmad 2015-08-30

1
멋진 대답입니다. 나는 꽤 오랫동안 Java 스레드로 작업하고 있으며 여기에서 뭔가를 배웠습니다 ( wait()동기화 잠금 해제!).
brimborium

감사합니다, Boann! 훌륭한 대답입니다. 예제가있는 전체 기사와 같습니다! 예, 또한 좋아 "대기 ()는 동기화 잠금 해제"
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,이 코드는 pizzaArrived == false루프 외부 에서 테스트를 호이스트하지 않으며 루프는 메인 스레드에 의해 변경된 플래그를 볼 수 있습니다.
gaussclb

1
@gaussclb 클래스 파일을 디 컴파일 한 것을 의미한다면 수정하십시오. Java 컴파일러는 거의 최적화하지 않습니다. 호이 스팅은 JVM에 의해 수행됩니다. 네이티브 기계어 코드를 분해해야합니다. 시도 : wiki.openjdk.java.net/display/HotSpot/PrintAssembly
Boann에게
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.