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

Posted

技术标签:

【中文标题】为每个线程生成一个新的对象实例是线程安全的操作吗?【英文标题】:Is spawning a new object instance for each thread a thread-safe operation? 【发布时间】:2014-06-14 19:13:37 【问题描述】:

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

现在,虽然这很清楚,但当您不使用单个对象而是使用多个线程处理 MULTIPLE 对象时,它对我来说有点灰暗。所以我试着测试一下。我有一个 ExecuterService 有 50 个线程。它产生了一个新的 Responder 线程(它本身就是一个 NEW OBJECT):

    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 list 是不是线程间共享数据?我的直觉说线程安全在这里不是问题,因为每个线程都使用自己的实例、自己的成员而不是共享数据,当我尝试运行这个示例时,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% 确定我的初始示例是否存在线程安全问题,因为我似乎无法生成线程不安全的等价物。

【问题讨论】:

@user2864740 你能改写一下吗?我不确定“executor.execute(new Responder());”是如何导致共享可变状态的。 @user2864740 所以当我运行“executor.execute(new Responder());”时,这意味着每个 Responder 都在使用自己的数据,因此线程安全不会有问题,对吗? @user2864740 我刚刚阅读了您编辑的评论。所以您说第一个示例没有线程安全问题,因为每个线程都处理自己的对象,而第二个示例可能存在线程安全问题,因为它们具有共享的可变状态。出于好奇,我将第二个示例运行了 50 次,它总是给我正确的值,如果它被破坏了就不应该是这种情况。 【参考方案1】:

仅仅因为线程代码似乎可以在给定的环境中工作意味着它保证可以工作。

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

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

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

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

然而,由于使用了Thread.sleep,具体示例使这个问题更难检测(至少在 x86 系统上),这实际上确保了线程大部分时间什么都不做。例如,每个线程每隔几毫秒才“添加一个项目” - 对 CPU 来说是 eons! - 减少负面互动的机会。

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

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 为了改进示例,在运行方法的开头添加CountDownLatch,以便所有任务或多或少同时开始运行。在当前示例中,第一个线程可以在最后一个线程启动之前完成(创建和启动线程也需要时间)。 我不明白为什么每个人都说第一个示例是线程安全的。主题启动器在主线程(创建Responders)上创建ArrayList,然后ArrayList 被派生线程使用。共享状态 (ArrayList) 确实存在。它可能仅因为在某个时间点创建的线程没有缓存而正确工作。但是由于Executors.newFixedThreadPool(),线程a实际上被重用了。有人可以解释我错在哪里吗?【参考方案2】:

在第二个示例中,两个线程都引用同一个列表,但没有同步,这不是线程安全的。事实上,在我的旧笔记本电脑上运行这个示例(在带有 Intel Core 2 Duo CPU 的 Windows 7 上使用 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 的列表大小的结果。每个执行器任务都引用自己的对象及其字段成员。

【讨论】:

我正在运行 Oracle Java 8,我运行了第二个示例 50 多次,它总是给我 2000。 我正在使用 JDK1.6.0_21 32 位 Windows 7,使用 Intel Core 2 Duo @2.4GHz。 @JohnMerlino 您无法通过经验测试证明线程安全的假设。你只能拒绝它。【参考方案3】:

没有。不是。

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

但是(现在 - 简短回答):总的来说,您提到的方法不是线程安全的!那就是“生成新对象并不能保证线程安全”。

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

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

【讨论】:

以上是关于为每个线程生成一个新的对象实例是线程安全的操作吗?的主要内容,如果未能解决你的问题,请参考以下文章

C#随机数生成器线程安全吗?

spring生成bean对象的生命周期都有哪些种类?

并发遍历实现线程安全遍历

java多线程访问同一个数组,存在并发问题吗,每个线程访问的是数组的不同部分,不存在冲突

PHP在单例模式中,是多个线程只有一个实例还是一个线程只有一个实例

Servlet和Struts2的线程安全问题