为每个线程生成一个新的对象实例是一个线程安全的操作吗?

时间:2014-06-14 19:13:37

标签: java multithreading

当运行多个线程时,我读到交错成为一个问题,其中一个线程不会考虑另一个线程对象的更改。 Jave提供同步方法,同步状态,Lock对象和新的Concurrency类对象,以确保每个线程在多个线程影响单个对象时,在其他线程影响对象字段之前获得其独有的转向以影响对象字段。

现在,虽然这很清楚,但是当你不使用单个对象但是为MULTIPLE对象使用多个线程时,它会变得有点灰色。所以我试着测试一下。我有一个包含50个线程的ExecuterService。它产生一个新的Responder线程(它本身就是一个新的对象):

    ExecutorService executor=Executors.newFixedThreadPool(50);
    for(int i=0;i<50;i++){
        executor.execute(new Responder()); 
    }   

因为每个线程本身都是一个实例化对象,所以如果我的Responder类看起来像这样:

public class Responder implements Runnable {
    private ArrayList<Integer> list=new ArrayList<Integer>();
    private Random random = new Random();

    @Override
    public void run() {
        for(int i=0;i<1000;i++){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(random.nextInt(100));          
        }
        System.out.println("The list size: " + list.size());
    }

}

每个线程是否都使用自己的Responder实例,以便线程安全不成问题?例如,ArrayList列出了线程之间的共享数据吗?我的直觉是说线程安全不是问题,因为每个线程都使用自己的实例,自己的成员,而不是共享数据,当我尝试运行这个例子时,size()调用输出相同的(1000)所有线程。所以它似乎是线程安全的,但我尝试了一种假想的无线安全方式:

public static void main(String[] args) {
    int i;
    //checking if instantiating new object into executor is thread safe
    ExecutorService executor=Executors.newFixedThreadPool(50);
    for(i=0;i<50;i++){
        executor.execute(new Responder()); 
    }
    // checking that running multiple threads with shared object is not thread safe
    UnsafeResponder unsafeResp = new UnsafeResponder();
    unsafeResp.execute();
}

public class UnsafeResponder {
    private ArrayList<Integer> list=new ArrayList<Integer>();
    private Random random = new Random();

    private void processData(){
        for(int i=0;i<1000;i++){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(random.nextInt(100));
        }
    }

    public void execute(){
        Thread t1 = new Thread(new Runnable(){
            public void run() {
                processData();
            }           
        });

        Thread t2 = new Thread(new Runnable(){
            public void run() {
                processData();
            }           
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        //print out value of shared data
        System.out.println("The list size of shared data: " + list.size());
    }
}

即使我没有使用锁或同步语句,每当我调用它时,这种无线安全方式正确地打印出2000。所以这就是为什么我甚至不能100%确定我的初始示例是否存在线程安全问题,因为我似乎无法生成线程不安全的等价物。

3 个答案:

答案 0 :(得分:4)

仅仅因为线程代码出现才能在给定环境中工作,意味着保证能够正常工作。

无法保证的线程编码是不是线程安全的代码。相反,通过不违反任何基本保证/公理/规则,程序必须推理是正确的。一般执行代码只能拒绝线程安全的断言(当它失败时),但它不能证明线程安全。

在第一个示例中,没有共享可变状态且每个任务都是自己的对象,其拥有独立状态(即不同的ArrayList对象),它可以被简单地推理为线程安全的。线程根本不与彼此的数据进行交互。

但是,假设第二个示例是线程安全的是不正确的,因为它没有使用共享状态/数据的正确线程安全访问;实际上,它是已损坏,因为ArrayList 保证是线程安全的:

  

请注意,[ArrayList]未同步。 如果多个线程同时访问ArrayList实例,并且至少有一个线程在结构上修改了列表,则必须在外部进行同步。 (结构修改是添加或删除一个或多个元素的任何操作。)

然而,具体示例使得此问题更难以检测(至少在x86系统上),因为使用了Thread.sleep,这实际上确保了线程在大多数时间都不执行任何操作。例如,每个线程只添加一个项目&#34;每隔几毫秒 - 一次CPU! - 减少了负面互动的机会。

这是一个[更多]可能产生不一致结果的例子:

public class UnsafeResponder implements Runnable {
    private ArrayList<Integer> list = new ArrayList<Integer>();
    private int LIMIT = 1000000; // More operations
    private int THREADS = 10;    // More threads

    public void run()
        // Less delays
        for(int i = 0; i < LIMIT; i++){
            list.add(i);
        }
    }

    public void execute(){
        List<Thread> threads = new ArrayList<Thread>();
        for (int t = 0; t < THREADS; t++) {
            Thread th = new Thread(this);
            th.start();
            threads.add(th);
        }

        try {
            for (Thread th : threads) {
                th.join();
            }
        } catch (InterruptedException e) {
            throw new RuntimeException("Test failed!", e);
        }

        System.out.println("Expected: " + (THREADS * LIMIT));
        System.out.println("  Actual: " + list.size());
    }
}

答案 1 :(得分:0)

在第二个示例中,两个线程都引用相同的列表而没有同步,这不是线程安全的。实际上,在我的旧笔记本电脑上运行此示例(在Windows 7上使用带有Intel Core 2 Duo CPU的JDK1.6)并未显示相同的结果。有时它会将列表的最终大小显示为2000,但在其他情况下,1999年甚至是1992年。我还尝试让第二个线程从列表中删除(如果它不是空的)而不是添加,它也会产生不一致的结果。

private void processData2(){
    for(int i=0;i<1000;i++){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(!list.isEmpty()) {
            list.remove(0);
        }
    }
}

...

Thread t1 = new Thread(new Runnable(){
    public void run() {
        processData();
    }           
});

Thread t2 = new Thread(new Runnable(){
    public void run() {
        processData2();
    }           
});
t1.start();
t2.start();

这清楚地表明它不是线程安全的。

为了进一步确认,我切换到同步列表:

private List<Integer> list=Collections.synchronizedList(new ArrayList<Integer>());

这次运行示例总是产生2000的大小。

另一方面,运行第一个示例总是给出列表大小为1000的结果。每个执行程序任务都引用自己的对象及其字段成员。

答案 2 :(得分:0)

否。事实并非如此。

如果在单个线程中运行非线程安全代码并且保证没有其他线程运行它 - 那么这是“线程安全的”。这或多或少与同步有关。因此,在许多情况下,创建新对象将是线程安全的。

然而(现在 - 简短回答):总的来说,你提到的方法不是安全的!这是“生成新对象不保证线程安全”。

有一个简单的反例:该类可以有一个非线程安全的静态字段,每个“新”对象都可以访问它。

(实际上,在许多其他情况下它将是非线程安全的(例如,新对象共享对其他对象的引用或使用其他非线程安全的静态方法(如访问文件))。