簡體   English   中英

將對象分配給在同步塊外部定義的字段 - 它是否是線程安全的?

[英]Assigning a object to a field defined outside a synchronized block - is it thread safe?

這個java代碼的線程安全性有什么問題嗎? 線程1-10通過sample.add()添加數字,而線程11-20調用removeAndDouble()並將結果打印到stdout。 我記得在我的腦海里有人說過,以同樣的方式在removeAndDouble()中使用它來分配項目可能不是線程安全的。 編譯器可以優化指令,使它們不按順序發生。 這是這種情況嗎? 我的removeAndDouble()方法不安全嗎?

從這個代碼的並發角度來看還有什么問題嗎? 我試圖用java(1.6向上)更好地理解並發性和內存模型。

import java.util.*;
import java.util.concurrent.*;

public class Sample {

    private final List<Integer> list = new ArrayList<Integer>();

    public void add(Integer o) {
        synchronized (list) {
            list.add(o);
            list.notify();
        }
    }

    public void waitUntilEmpty() {
        synchronized (list) {
            while (!list.isEmpty()) {
                try { 
                    list.wait(10000);  
                 } catch (InterruptedException ex) { }
            }
        }
    }

    public void waitUntilNotEmpty() {
        synchronized (list) {
            while (list.isEmpty()) {
                try { 
                    list.wait(10000);  
                 } catch (InterruptedException ex) { }
            }
        }
    }

    public Integer removeAndDouble() {
        // item declared outside synchronized block
        Integer item; 
        synchronized (list) { 
            waitUntilNotEmpty();
            item = list.remove(0);
        }
        // Would this ever be anything but that from list.remove(0)?
        return Integer.valueOf(item.intValue() * 2);
    }

    public static void main(String[] args) {
        final Sample sample = new Sample();

        for (int i = 0; i < 10; i++) {
            Thread t = new Thread() {
                public void run() {
                    while (true) {
                        System.out.println(getName()+" Found: " + sample.removeAndDouble());
                    }
                }
            };
            t.setName("Consumer-"+i);
            t.setDaemon(true);
            t.start();
        }

        final ExecutorService producers = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            final int j = i * 10000;
            Thread t = new Thread() {
                public void run() {
                    for (int c = 0; c < 1000; c++) {
                        sample.add(j + c);
                    }
                }
            };
            t.setName("Producer-"+i);
            t.setDaemon(false);
            producers.execute(t);
        }

        producers.shutdown();
        try {
            producers.awaitTermination(600, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        sample.waitUntilEmpty();        
        System.out.println("Done.");
    }
}

它看起來對我來說是安全的。 這是我的推理。

每次訪問list時,都會同步進行。 這很棒。 即使您在item提取list的一部分,多個線程也不會訪問該item

只要你只在同步時訪問list ,你應該是好的(在你當前的設計中。)

您的同步很好,不會導致任何亂序執行問題。

但是,我注意到一些問題。

if you add a list.notifyAll() after the list.remove(0) in removeAndDouble . 首先,如果在removeAndDoublelist.remove(0)之后添加list.notifyAll() ,那么waitUntilEmpty方法會更加 這將消除wait(10000)最多10秒的延遲。

其次,你的list.notify in add(Integer)應該是一個notifyAll ,因為notify只喚醒一個線程,它可能喚醒一個在waitUntilEmptywaitUntilEmpty而不是waitUntilNotEmpty

第三,以上都不是你的應用程序的活躍終結,因為你使用了有限的等待,但是如果你做了上面的兩個更改,你的應用程序將有更好的線程性能( waitUntilEmpty )和有限的等待變得不必要並且可以變得普通老了沒有-arg等待。

您的代碼實際上是線程安全的。 這背后的原因是兩部分。

首先是互斥。 您的同步正確確保一次只有一個線程將修改集合。

第二個問題與您對編譯器重新排序的關注有關。 你擔心編譯實際上可以重新命令它不是線程安全的分配。 在這種情況下你不必擔心它。 在列表上進行同步會創建一個先發生過的關系。 在寫入Integer item之前,將從列表中刪除所有內容。 這告訴編譯器它無法重新命令該方法中的寫入項。

您的代碼是線程安全的,但不是並發的(並行)。 由於在單個互斥鎖下訪問所有內容,您將序列化所有訪問權限,實際上對結構的訪問是單線程的。

如果您需要生產代碼中描述的功能,則java.util.concurrent包已經提供了具有(固定大小)數組和(可增長的)基於鏈表的實現的BlockingQueue 至少在研究實施思路時,這些非常有趣。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM