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

Java 为每个线程生成一个新的对象实例是线程安全的操作吗?,java,multithreading,Java,Multithreading,当运行多个线程时,我了解到交错成为一个问题,其中一个线程不考虑另一个线程对对象的更改。Jave提供了同步方法、同步状态、锁对象和新的并发类对象,以确保当多个线程影响单个对象时,每个线程都能在其他线程影响对象字段之前,以独占方式影响对象字段 现在,虽然这很清楚,但当您不使用单个对象,而是使用多个线程处理多个对象时,我觉得它有点灰色。所以我试着测试这个。我有一个50线程的ExecuterService。它生成一个新的响应程序线程,该线程本身就是一个新对象: ExecutorService e

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

现在,虽然这很清楚,但当您不使用单个对象,而是使用多个线程处理多个对象时,我觉得它有点灰色。所以我试着测试这个。我有一个50线程的ExecuterService。它生成一个新的响应程序线程,该线程本身就是一个新对象:

    ExecutorService executor=Executors.newFixedThreadPool(50);
    for(int i=0;i<50;i++){
        executor.execute(new 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());
    }

}
每个线程是否都与自己的响应程序实例一起工作,这样就不会出现线程安全问题?例如,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%确定我的初始示例是否存在线程安全问题,因为我似乎无法生成线程不安全的等价物

在第二个示例中,两个线程都引用同一个列表,没有同步,这是不安全的。事实上,在我的旧笔记本电脑上使用JDK1.6和英特尔Core 2 Duo CPU在Windows 7上运行此示例并没有显示相同的结果。有时,它显示名单的最终规模为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。每个executor任务都引用自己的对象及其字段成员。

在第二个示例中,两个线程都引用相同的列表,没有同步,这是线程安全的。事实上,在我的旧笔记本电脑上使用JDK1.6和英特尔Core 2 Duo CPU在Windows 7上运行此示例并没有显示相同的结果。有时,它显示名单的最终规模为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。每个executor任务都引用自己的对象及其字段成员。

仅仅因为线程化代码似乎在给定环境中工作并不意味着它一定能工作

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

在第一个示例中,如果没有共享的可变状态,并且每个任务都是自己的对象,具有自己的独立状态,即不同的ArrayList对象,则可以简单地推断它是线程安全的。这些线程只是不与其他数据交互

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

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

但是,由于使用了Thread.sleep,这个特定的示例使得这个问题更难检测,至少在x86系统上是如此,因为Thread.sleep实际上确保了线程在大多数时间都不做任何事情。例如,每个线程每几毫秒只向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());
    }
}
< p> 仅仅因为线程化代码似乎在给定的环境中工作并不意味着它可以保证工作

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

在第一个示例中,如果没有共享的可变状态,并且每个任务都是自己的对象,具有自己的独立状态,即不同的ArrayList对象,则可以简单地推断它是线程安全的。这些线程只是不与其他数据交互

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

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

但是,由于使用了Thread.sleep,这个特定的示例使得这个问题更难检测,至少在x86系统上是如此,因为Thread.sleep实际上确保了线程在大多数时间都不做任何事情。例如,每个线程每几毫秒只向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());
    }
}
不,不是

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

但是现在——简单地回答一下:一般来说,您提到的方法不是线程安全的!也就是说,生成新对象并不能保证线程安全

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

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

否。它不是

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

但是现在——简单地回答一下:一般来说,您提到的方法不是线程安全的!也就是说,生成新对象并不能保证线程安全

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


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

@user2864740您能重新表述一下吗?我不知道那个执行者是怎么做的;导致共享可变状态。@user2864740所以当我运行executor.executenew Responder,这意味着每个响应者都在处理自己的数据,因此线程安全不会有问题,对吗?@user2864740我刚刚读了你编辑的评论。您是说第一个示例没有线程安全问题,因为每个线程都处理自己的对象,而第二个示例可能有线程安全问题,因为它们有一个共享的可变状态。出于好奇,我将第二个示例运行了50次,它总是给我正确的值,如果它被破坏,就不应该出现这种情况。@user2864740您能重新表述一下吗?我不知道那个执行者是怎么做的;导致共享可变状态。@user2864740所以当我运行executor.executenew Responder,这意味着每个响应者都在处理自己的数据,因此线程安全不会有问题,对吗?@user2864740我刚刚读了你编辑的评论。您是说第一个示例没有线程安全问题,因为每个线程都处理自己的对象,而第二个示例可能有线程安全问题,因为它们有一个共享的可变状态。出于好奇,我将第二个示例运行了50次,它总是给出正确的值,如果它坏了,就不应该是这样。我正在运行Oracle Java 8,第二个例子我运行了50多次,它总是给我2000次。我正在使用JDK1.6.0_21 32位Windows 7,使用Intel Core 2 Duo@2.4GHz。@JohnMerlino你不能通过实证测试来证明线程安全的假设。你只能拒绝它。我正在运行Oracle Java 8,第二个示例运行了50多次,它总是给我2000次。我正在使用JDK1.6.0_21 32位Windows 7,使用Intel Core
2 Duo@2.4GHz@JohnMerlino你无法通过经验测试证明螺纹安全性假设。你只能拒绝它。仅仅因为线程代码在给定的环境中可以工作并不意味着它可以保证工作+1要改进此示例,请在run方法的开头添加CountDownLatch,以便所有任务或多或少同时开始运行。在当前示例中,第一个线程可能在最后一个线程开始创建之前完成,启动线程也需要时间。我不明白为什么每个人都说第一个示例是线程安全的。主题启动程序在主线程上创建ArrayList,主线程创建响应程序,然后生成的线程使用ArrayList。共享状态ArrayList不存在。它可能工作正常,只是因为在某个时间点创建的线程没有缓存。但是由于Executors.newFixedThreadPool,线程实际上被重用了。有人能解释一下我错在哪里吗?仅仅因为线程代码在给定的环境中工作并不意味着它一定能工作+1要改进此示例,请在run方法的开头添加CountDownLatch,以便所有任务或多或少同时开始运行。在当前示例中,第一个线程可能在最后一个线程开始创建之前完成,启动线程也需要时间。我不明白为什么每个人都说第一个示例是线程安全的。主题启动程序在主线程上创建ArrayList,主线程创建响应程序,然后生成的线程使用ArrayList。共享状态ArrayList不存在。它可能工作正常,只是因为在某个时间点创建的线程没有缓存。但是由于Executors.newFixedThreadPool,线程实际上被重用了。有人能解释一下我错在哪里吗?