JVM może założyć, że inne wątki nie zmieniają pizzaArrived
zmiennej podczas pętli. Innymi słowy, może przenieść pizzaArrived == false
test poza pętlę, optymalizując to:
while (pizzaArrived == false) {}
zaangażowany w to:
if (pizzaArrived == false) while (true) {}
która jest nieskończoną pętlą.
Aby upewnić się, że zmiany wprowadzone przez jeden wątek są widoczne dla innych wątków, należy zawsze dodać synchronizację między wątkami. Najprostszym sposobem na to jest utworzenie wspólnej zmiennej volatile
:
volatile boolean pizzaArrived = false;
Utworzenie zmiennej volatile
gwarantuje, że różne wątki zobaczą efekty wzajemnych zmian w niej. Zapobiega to buforowaniu wartości pizzaArrived
testu przez maszynę JVM lub przenoszeniu go poza pętlę. Zamiast tego musi za każdym razem odczytywać wartość zmiennej rzeczywistej.
(Bardziej formalnie, volatile
tworzy relację dzieje się przed dostępami do zmiennej. Oznacza to, że cała inna praca wykonana przez wątek przed dostarczeniem pizzy jest również widoczna dla wątku otrzymującego pizzę, nawet jeśli te inne zmiany nie dotyczą volatile
zmiennych).
Zsynchronizowane metody są stosowane głównie do wdrażania wzajemnego wykluczania (zapobiegania dwóm rzeczom występującym w tym samym czasie), ale mają również te same skutki uboczne, które volatile
mają. Używanie ich podczas czytania i pisania zmiennej to kolejny sposób na pokazanie zmian innym wątkom:
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;
}
}
Efekt oświadczenia drukowanego
System.out
jest PrintStream
przedmiotem. Metody PrintStream
są zsynchronizowane w następujący sposób:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
Synchronizacja zapobiega pizzaArrived
buforowaniu podczas pętli. Ściśle mówiąc, oba wątki muszą zsynchronizować się na tym samym obiekcie, aby zagwarantować, że zmiany w zmiennej są widoczne. (Na przykład wywołanie println
po ustawieniu pizzaArrived
i ponowne wywołanie przed odczytem pizzaArrived
byłoby poprawne). Jeśli tylko jeden wątek synchronizuje się z określonym obiektem, maszyna JVM może go zignorować. W praktyce JVM nie jest wystarczająco inteligentny, aby udowodnić, że inne wątki nie będą dzwonić println
po ustawieniu pizzaArrived
, więc zakłada, że tak. Dlatego nie może buforować zmiennej podczas pętli, jeśli wywołasz System.out.println
. Dlatego pętle takie jak ta działają, gdy mają instrukcję print, chociaż nie jest to poprawna poprawka.
Używanie System.out
nie jest jedynym sposobem wywołania tego efektu, ale jest to ten, który ludzie odkrywają najczęściej, kiedy próbują debugować, dlaczego ich pętla nie działa!
Większy problem
while (pizzaArrived == false) {}
to pętla zajętego oczekiwania. To źle! Podczas oczekiwania obciąża procesor, co spowalnia inne aplikacje i zwiększa zużycie energii, temperaturę i prędkość wentylatora systemu. W idealnym przypadku chcielibyśmy, aby wątek pętli spał podczas oczekiwania, aby nie obciążał procesora.
Oto kilka sposobów, aby to zrobić:
Korzystanie z funkcji czekaj / powiadamiaj
Niskopoziomowym rozwiązaniem jest użycie metod czekania / powiadamianiaObject
:
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();
}
}
}
W tej wersji kodu wywołania wątku pętli wait()
, co powoduje uśpienie wątku. Podczas snu nie będzie używać żadnych cykli procesora. Po ustawieniu zmiennej przez drugi wątek wywołuje notifyAll()
ona obudzenie wszystkich / wszystkich wątków, które czekały na ten obiekt. To tak, jakby facet od pizzy dzwonił do drzwi, abyś mógł usiąść i odpocząć podczas oczekiwania, zamiast stać niezgrabnie przy drzwiach.
Podczas wywoływania funkcji wait / notification na obiekcie musisz przytrzymać blokadę synchronizacji tego obiektu, co robi powyższy kod. Możesz użyć dowolnego obiektu, który ci się podoba, o ile oba wątki używają tego samego obiektu: tutaj użyłem this
(wystąpienie MyHouse
). Zwykle dwa wątki nie byłyby w stanie jednocześnie wprowadzić zsynchronizowanych bloków tego samego obiektu (co jest częścią celu synchronizacji), ale działa to tutaj, ponieważ wątek tymczasowo zwalnia blokadę synchronizacji, gdy znajduje się wewnątrz wait()
metody.
BlockingQueue
A BlockingQueue
służy do implementowania kolejek producent-konsument. „Konsumenci” pobierają pozycje z początku kolejki, a „producenci” przesuwają pozycje z tyłu. Przykład:
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");
}
}
Uwaga: metody put
i mogą zgłaszać s, które są sprawdzanymi wyjątkami, które muszą być obsługiwane. W powyższym kodzie dla uproszczenia wyjątki zostały ponownie zgłoszone. Możesz chcieć przechwycić wyjątki w metodach i ponowić wywołanie put lub take, aby upewnić się, że się powiedzie. Poza tym jednym punktem brzydoty, jest bardzo łatwy w użyciu.take
BlockingQueue
InterruptedException
BlockingQueue
Żadna inna synchronizacja nie jest tutaj potrzebna, ponieważ BlockingQueue
zapewnia, że wszystko, co zrobiły wątki przed umieszczeniem elementów w kolejce, jest widoczne dla wątków pobierających te elementy.
Egzekutorzy
Executor
są jak gotowe, BlockingQueue
które wykonują zadania. Przykład:
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);
Szczegółowe informacje można znaleźć na doc Executor
, ExecutorService
i Executors
.
Obsługa zdarzeń
Zapętlanie się podczas oczekiwania na kliknięcie czegoś w interfejsie użytkownika jest błędne. Zamiast tego użyj funkcji obsługi zdarzeń z zestawu narzędzi interfejsu użytkownika. Na przykład w Swingu :
JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
label.setText("Button was clicked");
});
Ponieważ program obsługi zdarzeń działa w wątku wysyłania zdarzeń, wykonywanie długiej pracy w programie obsługi zdarzeń blokuje inne interakcje z interfejsem użytkownika do momentu zakończenia pracy. Powolne operacje można rozpocząć w nowym wątku lub wysłać do oczekującego wątku przy użyciu jednej z powyższych technik (czekaj / powiadamiaj, a BlockingQueue
lub Executor
). Możesz również użyć SwingWorker
, który jest zaprojektowany dokładnie do tego i automatycznie dostarcza wątek roboczy w tle:
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();
});
Timery
Aby wykonywać czynności okresowe, możesz użyć pliku java.util.Timer
. Jest łatwiejszy w użyciu niż pisanie własnej pętli czasowej i łatwiejszy do rozpoczęcia i zakończenia. To demo drukuje bieżący czas raz na sekundę:
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
};
timer.scheduleAtFixedRate(task, 0, 1000);
Każdy java.util.Timer
ma swój własny wątek w tle, który jest używany do wykonywania zaplanowanych TimerTask
operacji. Oczywiście wątek śpi między zadaniami, więc nie obciąża procesora.
W kodzie Swing istnieje również javax.swing.Timer
, który jest podobny, ale wykonuje nasłuchiwanie w wątku Swing, dzięki czemu można bezpiecznie współdziałać z komponentami Swing bez konieczności ręcznego przełączania wątków:
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);
Inaczej
Jeśli piszesz kod wielowątkowy, warto zapoznać się z klasami w tych pakietach, aby zobaczyć, co jest dostępne:
Zobacz także sekcję Współbieżność w samouczkach Java. Wielowątkowość jest skomplikowana, ale dostępna jest duża pomoc!