루프는 print 문없이 다른 스레드에 의해 변경된 값을 보지 못함
내 코드에는 일부 상태가 다른 스레드에서 변경 될 때까지 기다리는 루프가 있습니다. 다른 스레드는 작동하지만 내 루프는 변경된 값을 볼 수 없습니다. 영원히 기다립니다. 그러나 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");
명령문의 주석 처리를 제거 할 때만 작동 합니다. 무슨 일이야?
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.out
A는 PrintStream
객체. 의 메서드는 PrintStream
다음과 같이 동기화됩니다.
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
The synchronization prevents pizzaArrived
being cached during the loop. Strictly speaking, both threads must synchronize on the same object to guarantee that changes to the variable are visible. (For example, calling println
after setting pizzaArrived
and calling it again before reading pizzaArrived
would be correct.) If only one thread synchronizes on a particular object, the JVM is allowed to ignore it. In practice, the JVM is not smart enough to prove that other threads won't call println
after setting pizzaArrived
, so it assumes that they might. Therefore, it cannot cache the variable during the loop if you call System.out.println
. That's why loops like this work when they have a print statement, although it is not a correct fix.
Using System.out
is not the only way to cause this effect, but it is the one people discover most often, when they are trying to debug why their loop doesn't work!
The bigger problem
while (pizzaArrived == false) {}
is a busy-wait loop. That's bad! While it waits, it hogs the CPU, which slows down other applications, and increases the power usage, temperature, and fan speed of the system. Ideally, we would like the loop thread to sleep while it waits, so it does not hog the CPU.
Here are some ways to do that:
Using wait/notify
A low-level solution is to use the wait/notify methods of 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();
}
}
}
In this version of the code, the loop thread calls wait()
, which puts the thread the sleep. It will not use any CPU cycles while sleeping. After the second thread sets the variable, it calls notifyAll()
to wake up any/all threads which were waiting on that object. This is like having the pizza guy ring the doorbell, so you can sit down and rest while waiting, instead of standing awkwardly at the door.
When calling wait/notify on an object you must hold the synchronization lock of that object, which is what the above code does. You can use any object you like so long as both threads use the same object: here I used this
(the instance of MyHouse
). Usually, two threads would not be able to enter synchronized blocks of the same object simultaneously (which is part of the purpose of synchronization) but it works here because a thread temporarily releases the synchronization lock when it is inside the wait()
method.
BlockingQueue
A BlockingQueue
is used to implement producer-consumer queues. "Consumers" take items from the front of the queue, and "producers" push items on at the back. An example:
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");
}
}
Note: The put
and take
methods of BlockingQueue
can throw InterruptedException
s, which are checked exceptions which must be handled. In the above code, for simplicity, the exceptions are rethrown. You might prefer to catch the exceptions in the methods and retry the put or take call to be sure it succeeds. Apart from that one point of ugliness, BlockingQueue
is very easy to use.
No other synchronization is needed here because a BlockingQueue
makes sure that everything threads did before putting items in the queue is visible to the threads taking those items out.
Executors
Executor
s are like ready-made BlockingQueue
s which execute tasks. Example:
// 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
For details see the doc for Executor
, ExecutorService
, and Executors
.
Event handling
Looping while waiting for the user to click something in a UI is wrong. Instead, use the event handling features of the UI toolkit. In Swing, for example:
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");
});
Because the event handler runs on the event dispatch thread, doing long work in the event handler blocks other interaction with the UI until the work is finished. Slow operations can be started on a new thread, or dispatched to a waiting thread using one of the above techniques (wait/notify, a BlockingQueue
, or Executor
). You can also use a SwingWorker
, which is designed exactly for this, and automatically supplies a background worker thread:
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();
});
Timers
To perform periodic actions, you can use a java.util.Timer
. It is easier to use than writing your own timing loop, and easier to start and stop. This demo prints the current time once per second:
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
};
timer.scheduleAtFixedRate(task, 0, 1000);
Each java.util.Timer
has its own background thread which is used to execute its scheduled TimerTask
s. Naturally, the thread sleeps between tasks, so it does not hog the CPU.
In Swing code, there is also a javax.swing.Timer
, which is similar, but it executes the listener on the Swing thread, so you can safely interact with Swing components without needing to manually switch threads:
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);
Other ways
If you are writing multithreaded code, it is worth exploring the classes in these packages to see what is available:
And also see the Concurrency section of the Java tutorials. Multithreading is complicated, but there is lots of help available!
'Programing' 카테고리의 다른 글
Python에서 호출 함수 모듈의 __name__ 가져 오기 (0) | 2020.09.16 |
---|---|
Swift에서 슬라이스는 무엇입니까? (0) | 2020.09.16 |
MySQL 샤딩 접근 방식? (0) | 2020.09.16 |
PHPMyAdmin 언어 설정 (0) | 2020.09.16 |
스팬 또는 div를 가로로 정렬하려면 어떻게합니까? (0) | 2020.09.16 |