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();
}
}
동기화 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 {
Object food = queue.take();
System.out.println("Eating: " + food);
}
void deliverPizza() throws InterruptedException {
queue.put("A delicious pizza");
}
}
참고 :의 put
및 take
메서드는 처리해야하는 확인 된 예외 인 s BlockingQueue
를 throw 할 수 있습니다 InterruptedException
. 위의 코드에서는 단순화를 위해 예외가 다시 발생합니다. 메소드에서 예외를 포착하고 넣기를 재 시도하거나 호출이 성공하는지 확인하는 것을 선호 할 수 있습니다. 그 하나의 추악함을 제외하고는 BlockingQueue
사용하기가 매우 쉽습니다.
는 BlockingQueue
항목을 대기열에 넣기 전에 스레드가 수행 한 모든 작업이 해당 항목을 꺼내는 스레드에 표시되는지 확인 하기 때문에 여기에서 다른 동기화가 필요 하지 않습니다.
집행자
Executor
s는 BlockingQueue
작업을 실행하는 기성품과 같습니다 . 예:
ExecutorService executor = Executors.newSingleThreadExecutor();
Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };
executor.execute(eatPizza);
executor.execute(cleanUp);
세부 사항에 대한 문서를 참조하십시오 Executor
, ExecutorService
하고 Executors
.
이벤트 처리
사용자가 UI에서 무언가를 클릭하기를 기다리는 동안 반복하는 것은 잘못되었습니다. 대신 UI 툴킷의 이벤트 처리 기능을 사용하십시오. 예를 들어, Swing 에서 :
JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
label.setText("Button was clicked");
});
이벤트 핸들러는 이벤트 디스패치 스레드에서 실행되기 때문에 이벤트 핸들러에서 장시간 작업하면 작업이 완료 될 때까지 UI와의 다른 상호 작용이 차단됩니다. 느린 작업은 새 스레드에서 시작되거나 위의 기술 (대기 / 알림, a BlockingQueue
또는 Executor
) 중 하나를 사용하여 대기중인 스레드로 디스패치 될 수 있습니다 . SwingWorker
이를 위해 정확히 설계된를 사용할 수도 있으며 자동으로 백그라운드 작업자 스레드를 제공합니다.
JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");
button.addActionListener((ActionEvent e) -> {
class MyWorker extends SwingWorker<String,Void> {
@Override
public String doInBackground() throws Exception {
Thread.sleep(5000);
return "Answer is 42";
}
@Override
protected void done() {
String result;
try {
result = get();
} catch (Exception e) {
throw new RuntimeException(e);
}
label.setText(result);
}
}
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
은 예약 된 TimerTask
s 를 실행하는 데 사용되는 자체 백그라운드 스레드를 가지고 있습니다. 당연히 스레드는 작업 사이에 휴면하므로 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 자습서 의 동시성 섹션 을 참조하십시오 . 멀티 스레딩은 복잡하지만 많은 도움이 필요합니다!