将对象分配给在同步块外部定义的字段 - 它是否是线程安全的?

时间:2010-09-23 19:05:55

标签: java multithreading concurrency thread-safety

这个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.");
    }
}

4 个答案:

答案 0 :(得分:8)

它看起来对我来说是安全的。这是我的推理。

每次访问list时,都会同步。这很棒。即使您在list中提取了item的一部分,但多个线程也无法访问item

只要您在同步时只访问list,就应该很好(在您当前的设计中)。

答案 1 :(得分:6)

您的同步很好,不会导致任何无序执行问题。

但是,我注意到一些问题。

首先,如果您在waitUntilEmpty list.notifyAll()之后添加list.remove(0),则removeAndDouble方法将更加及时。这将消除wait(10000)中最多10秒的延迟。

其次,list.notify中的add(Integer)应该是notifyAll,因为notify只唤醒一个主题,并且可能会唤醒一个主题正在waitUntilEmpty而不是waitUntilNotEmpty内等待。

第三,以上都不是你的应用程序的活跃终结,因为你使用了有限的等待,但是如果你做了上面两个更改,你的应用程序将有更好的线程性能(waitUntilEmpty)和有限的等待成为不必要的,可以变成普通的老式等待。

答案 2 :(得分:3)

您的代码实际上是线程安全的。这背后的原因是两部分。

首先是相互排斥。您的同步正确确保一次只有一个线程将修改集合。

第二个问题与您对编译器重新排序的关注有关。你担心编译实际上可以重新命令它不是线程安全的分配。在这种情况下你不必担心它。在列表上进行同步会创建一个先发生过的关系。在写入Integer item之前,所有从列表中删除的内容都会发生。这告诉编译器它无法重新命令写入该方法中的项目。

答案 3 :(得分:1)

您的代码是线程安全的,但不是并发的(并行)。由于在单个互斥锁下访问所有内容,您将序列化所有访问权限,实际上对结构的访问是单线程的。

如果您需要生产代码中描述的功能,java.util.concurrent包已经提供了BlockingQueue具有(固定大小)数组和(可增长)链接列表的实现。至少研究实施思路非常有趣。