JVM'nin, pizzaArrived
döngü sırasında diğer evrelerin değişkeni değiştirmediğini varsaymasına izin verilir . Başka bir deyişle, pizzaArrived == false
testi optimize ederek döngünün dışına çıkarabilir :
while (pizzaArrived == false) {}
bunun içine:
if (pizzaArrived == false) while (true) {}
sonsuz bir döngüdür.
Bir iş parçacığı tarafından yapılan değişikliklerin diğer evreler tarafından görülebilmesini sağlamak için her zaman evreler arasına bir miktar senkronizasyon eklemelisiniz . Bunu yapmanın en basit yolu, paylaşılan değişkeni yapmaktır volatile
:
volatile boolean pizzaArrived = false;
Bir değişken oluşturmak volatile
, farklı iş parçacıklarının birbirlerinin değişikliklerinin etkilerini görmesini garanti eder. Bu, JVM'nin pizzaArrived
testin değerini önbelleğe almasını veya döngünün dışına kaldırmasını önler . Bunun yerine, her seferinde gerçek değişkenin değerini okumalıdır.
(Daha resmi olarak, değişkene erişimler arasında bir önceden gerçekleşir ilişkisi volatile
oluşturur . Bu , pizzayı teslim etmeden önce bir iş parçacığının yaptığı diğer tüm çalışmaların , diğer değişiklikler değişkenlerde olmasa bile pizzayı alan iş parçacığı tarafından da görülebileceği anlamına gelir .)volatile
Eşzamanlı yöntemler esas olarak karşılıklı dışlamayı uygulamak için kullanılır (aynı anda iki şeyin olmasını engeller), ancak aynı zamanda sahip olduğu tüm yan etkilere de volatile
sahiptirler. Bir değişkeni okurken ve yazarken bunları kullanmak, değişiklikleri diğer konulara görünür kılmanın başka bir yoludur:
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;
}
}
Bir baskı ifadesinin etkisi
System.out
bir PrintStream
nesnedir. Yöntemleri PrintStream
şu şekilde senkronize edilir:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
Senkronizasyon pizzaArrived
, döngü sırasında önbelleğe alınmasını engeller . Açıkçası , değişkendeki değişikliklerin görünür olmasını sağlamak için her iki iş parçacığı da aynı nesne üzerinde senkronize olmalıdır . (Örneğin, println
ayarlamadan sonra çağırmak pizzaArrived
ve okumadan önce onu tekrar çağırmak pizzaArrived
doğru olacaktır.) Belirli bir nesnede yalnızca bir iş parçacığı senkronize olursa, JVM'nin onu yok saymasına izin verilir. Uygulamada, JVM diğer iş parçacıklarının println
ayarlandıktan sonra aramayacağını kanıtlayacak kadar akıllı değildir pizzaArrived
, bu nedenle olabileceklerini varsayar. Bu nedenle, çağırırsanız döngü sırasında değişkeni önbelleğe alamaz System.out.println
. Bu nedenle bu gibi döngüler, doğru bir düzeltme olmasa da bir print deyimine sahip olduklarında çalışır.
Kullanmak System.out
, bu etkiye neden olmanın tek yolu değildir, ancak döngülerinin neden çalışmadığı konusunda hata ayıklamaya çalışırken insanların en sık keşfettiği yoldur!
Daha büyük problem
while (pizzaArrived == false) {}
meşgul bekleme döngüsüdür. Bu kötü! Beklerken, diğer uygulamaları yavaşlatan ve sistemin güç kullanımını, sıcaklığını ve fan hızını artıran CPU'yu sarar. İdeal olarak, döngü iş parçacığının beklerken uyumasını isteriz, böylece CPU'yu zorlamaz.
İşte bunu yapmanın bazı yolları:
Bekle / bildir kullanma
Düşük seviyeli bir çözüm, bekle / bildir yöntemlerini kullanmaktırObject
:
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();
}
}
}
Kodun bu sürümünde wait()
, iş parçacığını uyku moduna geçiren döngü iş parçacığı çağırır . Uyurken herhangi bir CPU döngüsü kullanmayacaktır. İkinci evre değişkeni ayarladıktan sonra, notifyAll()
o nesnede bekleyen tüm evreleri / tüm evreleri uyandırmayı çağırır . Bu, pizzacının kapı zilini çalması gibidir, böylece kapıda garip bir şekilde durmak yerine oturup beklerken dinlenebilirsiniz.
Bir nesnede bekle / bildir çağırırken, o nesnenin senkronizasyon kilidini tutmanız gerekir, bu yukarıdaki kodun yaptığı şeydir. Her iki iş parçacığı da aynı nesneyi kullandığı sürece istediğiniz herhangi bir nesneyi kullanabilirsiniz: burada kullandım this
(örneğini MyHouse
). Genellikle, iki iş parçacığı aynı nesnenin eşzamanlı bloklarına aynı anda giremez (bu eşitleme amacının bir parçasıdır), ancak burada çalışır çünkü bir iş parçacığı, wait()
yöntemin içindeyken senkronizasyon kilidini geçici olarak serbest bırakır .
BlockingQueue
A BlockingQueue
, üretici-tüketici kuyruklarını uygulamak için kullanılır. "Tüketiciler" öğeleri sıranın önünden alır ve "üreticiler" arka taraftaki öğeleri iter. Bir örnek:
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");
}
}
Not: İşlenmesi gereken kontrol edilen istisnalar olan can atma yöntemleri put
ve take
yöntemleri . Yukarıdaki kodda, basitlik açısından istisnalar yeniden belirtilmiştir. Yöntemlerdeki istisnaları yakalamayı ve başarılı olduğundan emin olmak için put veya take çağrısını yeniden denemeyi tercih edebilirsiniz. Bunun dışında bir nokta çirkinlik, kullanımı çok kolay.BlockingQueue
InterruptedException
BlockingQueue
Burada başka senkronizasyon gerekmez, çünkü BlockingQueue
öğeleri kuyruğa koymadan önce iş parçacıklarının yaptığı her şeyin bu öğeleri alan iş parçacıklarına görünür olmasını sağlar.
Yürütücüler
Executor
s, BlockingQueue
görevleri yerine getiren hazır lara benzer . Misal:
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);
Ayrıntılar için belgeye bakın için Executor
, ExecutorService
ve Executors
.
Olay işleme
Kullanıcının bir kullanıcı arayüzünde bir şeyi tıklamasını beklerken döngü yapmak yanlıştır. Bunun yerine, UI araç setinin olay işleme özelliklerini kullanın. Swing'de , örneğin:
JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
label.setText("Button was clicked");
});
Olay işleyicisi, olay gönderme iş parçacığı üzerinde çalıştığından, olay işleyicisinde uzun süre çalışmak, iş bitene kadar UI ile diğer etkileşimi engeller. Yavaş işlemler, yeni bir iş parçacığı üzerinde başlatılabilir veya yukarıdaki tekniklerden biri (bekleme / bildir, a BlockingQueue
, veya Executor
) kullanılarak bekleyen bir iş parçacığına gönderilebilir . Ayrıca SwingWorker
, tam olarak bunun için tasarlanmış olan ve otomatik olarak bir arka plan çalışan iş parçacığı sağlayan a da kullanabilirsiniz :
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();
});
Zamanlayıcılar
Periyodik eylemler gerçekleştirmek için bir java.util.Timer
. Kullanımı, kendi zamanlama döngünüzü yazmaktan daha kolaydır ve başlatıp durdurmak daha kolaydır. Bu demo, geçerli zamanı saniyede bir yazdırır:
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
};
timer.scheduleAtFixedRate(task, 0, 1000);
Her java.util.Timer
birinin, planlanan ' TimerTask
lerini yürütmek için kullanılan kendi arka plan iş parçacığı vardır . Doğal olarak, iş parçacığı görevler arasında uyur, bu nedenle CPU'yu zorlamaz.
Swing kodunda, javax.swing.Timer
benzer olan bir de vardır , ancak dinleyiciyi Swing iş parçacığında çalıştırır, böylece konuları manuel olarak değiştirmenize gerek kalmadan Swing bileşenleriyle güvenli bir şekilde etkileşime girebilirsiniz:
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);
Diğer yollar
Çok iş parçacıklı kod yazıyorsanız, nelerin mevcut olduğunu görmek için bu paketlerdeki sınıfları keşfetmeye değer:
Ayrıca Java eğitimlerinin Eş Zamanlılık bölümüne bakın . Çoklu okuma karmaşıktır, ancak pek çok yardım mevcuttur!