Döngü, bir yazdırma ifadesi olmadan başka bir iş parçacığı tarafından değiştirilen değeri görmüyor


91

Kodumda, bazı durumların farklı bir iş parçacığından değiştirilmesini bekleyen bir döngü var. Diğer iş parçacığı çalışıyor, ancak döngüm değişen değeri asla görmüyor. Sonsuza kadar bekler. Ancak, System.out.printlndöngüye bir ifade koyduğumda aniden çalışıyor! Neden?


Aşağıdaki kodumun bir örneğidir:

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 döngüsü çalışırken, değişkeni deliverPizza()ayarlamak için farklı bir iş parçacığından arıyorum pizzaArrived. Ancak döngü yalnızca System.out.println("waiting");ifadeyi açıklamadığımda çalışıyor. Neler oluyor?

Yanıtlar:


153

JVM'nin, pizzaArriveddöngü sırasında diğer evrelerin değişkeni değiştirmediğini varsaymasına izin verilir . Başka bir deyişle, pizzaArrived == falsetesti 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 pizzaArrivedtestin 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 volatileoluş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 volatilesahiptirler. 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.outbir PrintStreamnesnedir. 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, printlnayarlamadan sonra çağırmak pizzaArrivedve okumadan önce onu tekrar çağırmak pizzaArriveddoğ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 printlnayarlandı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 {
        // 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");
    }
}

Not: İşlenmesi gereken kontrol edilen istisnalar olan can atma yöntemleri putve takeyö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.BlockingQueueInterruptedExceptionBlockingQueue

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

Executors, BlockingQueuegörevleri yerine getiren hazır lara benzer . Misal:

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

Ayrıntılar için belgeye bakın için Executor, ExecutorServiceve 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) -> {
    // This event listener is run when the button is clicked.
    // We don't need to loop while waiting.
    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");

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

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.Timerbirinin, planlanan ' TimerTasklerini 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.Timerbenzer 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!


Çok profesyonel cevap, bunu okuduktan sonra aklımda hiçbir yanlış anlama kalmadı, teşekkür ederim
Humoyun Ahmad

1
Harika cevap. Bir süredir Java iş parçacıklarıyla çalışıyorum ve hala burada bir şeyler öğreniyorum ( wait()senkronizasyon kilidini kaldırıyor!).
brimborium

Teşekkür ederim Boann! Harika cevap, örneklerle dolu bir makale gibi! Evet, "wait () senkronizasyon kilidini
kaldırır

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, bu kod pizzaArrived == falsetesti döngünün dışına çıkarmaz ve döngü ana iş parçacığı tarafından değiştirilen bayrağı görebilir, neden?
gaussclb

1
@gaussclb Bir sınıf dosyasını yeniden derlediğinizi kastediyorsanız, düzeltin. Java derleyicisi neredeyse hiç optimizasyon yapmaz. Kaldırma, JVM tarafından yapılır. Yerel makine kodunu sökmeniz gerekir. Deneyin: wiki.openjdk.java.net/display/HotSpot/PrintAssembly
Boann
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.