多个线程何时访问同一代码?

时间:2019-05-21 17:47:29

标签: java multithreading thread-safety

我的问题是:

  1. 默认情况下,Java程序是否仅导致创建1个线程?
  2. 如果是,并且我们创建了多线程程序,那么多线程何时会访问Java对象的相同代码?

例如,我有一个具有2种方法的Java程序-add()和sub()。在什么情况下2个或更多线程将运行'add()'方法?

代码不是总是线程安全的,因为多个线程将访问代码的不同部分?

如果没有,请显示一个示例程序,其中要考虑线程安全性。

4 个答案:

答案 0 :(得分:1)

不要考虑“代码段”,而要考虑数据所在的位置以及有多少线程正在访问该实际数据。

  • 局部变量位于使用它们的线程堆栈中,并且是线程安全的,因为它们是每个线程的不同数据“容器”。

  • 驻留在堆上的任何数据(例如实例或静态字段)本质上都不是线程安全的,因为如果有多个线程访问该数据,那么它们可能会争用。

我们可能会变得更加复杂,并讨论数据真正的位置,但是这种基本解释应该使您对正在发生的事情有个很好的了解。

下面的代码给出了一个由两个线程共享的实例的示例,在这种情况下,两个线程都访问相同的数组列表,该列表指向堆中相同的数组数据容器。运行几次,您最终会发现失败。如果您注释掉其中一个线程,则每次都会正常工作,从99开始倒数。

import java.util.ArrayList;
import java.util.List;
public class Main {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        new Thread(r).start();
        new Thread(r).start();
    }
    public static class MyRunnable implements Runnable {
        // imagine this list living out in the heap and both threads messing with it
        // this is really just a reference, but the actual data is in the heap
        private List<Integer> list = new ArrayList<>();
        {  for (int i = 0; i < 100; i++) list.add(i);  }

        @Override public void run() {
            while (list.size() > 0) System.out.println(list.remove(list.size() - 1));
        }
    }
}

答案 1 :(得分:0)

  1. 取决于实现。只有一个线程(“主线程”)将调用public static void main(String[])方法,但这并不意味着没有为其他任务启动其他线程。

  2. 如果对线程进行编程,则该线程将访问“相同代码”。我不确定您对“代码段”的想法是什么,或者两个线程永远不会同时访问同一“节”的想法来自哪里,但是创建线程不安全的代码是非常琐碎的。

    import java.util.ArrayList;
    import java.util.List;
    
    public class Main {
    
        public static void main(String[] args) throws InterruptedException {
            List<Object> list = new ArrayList<>();
    
            Runnable action = () -> {
                while (true) {
                    list.add(new Object());
                }
            };
    
            Thread thread1 = new Thread(action, "tread-1");
            thread1.setDaemon(true); // don't keep JVM alive
    
            Thread thread2 = new Thread(action, "thread-2");
            thread2.setDaemon(true); // don't keep JVM alive
    
            thread1.start();
            thread2.start();
    
            Thread.sleep(1_000L);
        }
    
    }
    

    ArrayList不是线程安全的。上面的代码有两个线程不断尝试将新的Object添加到相同的ArrayList大约一秒钟。它不是保证,但是如果您运行该代码,则可能会看到ArrayIndexOutOfBoundsException或类似内容。无论抛出任何异常,ArrayList的状态都有被破坏的危险。这是因为状态是由多个线程更新的,没有同步。

答案 2 :(得分:0)

  

1)默认情况下,Java程序是否仅导致创建1个线程?

真的取决于您的代码在做什么。一个简单的System.out.println()调用可能可能仅创建一个线程。但是,例如,当您举起一个Swing GUI窗口时,将至少有一个其他线程(“事件分配器线程”对用户输入做出反应并负责UI更新)。

  

2)如果是,并且如果我们创建一个多线程程序,那么多线程何时访问Java对象的相同代码?

对你的误解。对象没有 code 。基本上,线程将运行特定的方法。它自己的run()方法或其他可用的方法。然后线程仅执行该方法,以及从该初始方法触发的任何其他方法调用。

当然,在运行该代码时,该线程可能会创建其他对象,或操纵已经存在的对象的状态。当每个线程仅接触一组不同的对象时,就不会出现问题。但是,一旦有多个线程处理相同的对象状态,就需要采取适当的预防措施(以避免不确定的行为)。

答案 3 :(得分:0)

您的问题表明您可能不完全了解“线程”的含义。

当我们学习编程的时候,他们告诉我们计算机程序是一系列指令,并且他们告诉我们计算机执行这些指令是从一个很好的开始-定义的入口点(例如main()例程)。

好的,但是当我们谈论多线程程序时,仅仅说“计算机”执行我们的代码已不再足够。现在我们说 threads 执行我们的代码。每个线程对自己在程序中的位置都有自己的想法,如果两个或多个线程恰巧同时在同一函数中执行,则每个线程都有其函数参数和局部变量的专用副本。

所以,你问:

  

默认情况下,Java程序是否仅导致创建1个线程?

Java程序总是以执行您的代码的一个线程开始,通常是执行JVM代码的其他多个线程。您通常不需要了解JVM线程。执行您的代码的一个线程在您的main()例程的开头开始工作。

程序员经常将该初始线程称为“主线程”。他们可能会这样称呼它,因为它叫main(),但是要小心!这个名称可能会误导您:JVM在多线程Java程序中对待“主线程”的方式与其他线程没有任何区别。

  

如果我们创建一个多线程程序,那么多线程何时访问Java对象的相同代码?

线程仅执行程序告诉他们的操作。如果您为两个不同的线程编写代码以调用同一函数,那么它们将执行此操作。但是,让我们将这个问题分解一下...

...首先,我们如何创建一个多线程程序?

当您的代码告诉程序成为多线程时,程序将变为多线程。在一个简单的情况下,它看起来像这样:

class MyRunnable implements Runnable {
    public void run() {
        DoSomeUsefulThing();
        DoSomeOtherThing();
    }
}
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();
...

当程序中的某些其他线程调用t.start()时,Java将创建一个新线程。 (注意!Thread实例t不是 thread 。它只是程序可以用来启动线程的 handle 并查询其线程的状态并对其进行控制。)

新线程开始执行程序指令时,将通过调用r.run()开始。如您所见,r.run()的主体将使新线程先DoSomeUsefulThing(),然后再DoSomeOtherThing(),然后r.run()返回。

r.run()返回时,线程结束(又称“终止”,又称“死”)。

所以

  

何时多个线程访问Java对象的相同代码?

当您的代码让他们这样做时。让我们在上面的示例中添加一行:

...
Thread t = new Thread(r);
t.start();
DoSomeUsefulThing();
...

请注意,启动新线程后,主线程没有停止。它继续执行t.start()调用之后的所有操作。在这种情况下,下一步是调用DoSomeUsefulThing()。但这与程序告诉新线程的操作相同!如果DoSomeUsefulThing()需要花费大量时间才能完成,则两个线程将同时执行此操作...因为这是程序告诉他们执行的操作。

  

请显示一个示例程序,其中要考虑线程安全性

我照做了。

考虑DoSomeUsefulThing()可能正在做什么。如果它正在做有用的事情,那么几乎可以肯定它正在对某处的某些 data 做某事。但是,我没有告诉它要使用什么数据,所以有可能两个线程同时对相同的数据进行处理。

那很有可能会变得不好。

一种解决方法是告诉函数要处理哪些数据。

class MyDataClass { ... }
Class MyRunnable implements Runnable {
    private MyDataClass data;

    public MyRunnable(MyDataClass data) {
        this.data = data;
    }

    public void run() {
        DoSomeUsefulThingWITH(data);
        DoSomeOtherThingWITH(data);
    }
}
MyDataClass dat_a = new MyDataClass(...);
MyDataClass dat_b = new MyDataClass(...);
MyRunnable r = new MyRunnable(dat_a);
Thread t = new Thread(r);
t.start();
DoSomeUsefulThingWITH(dat_b);

那里!现在,两个线程正在做相同的事情,但是它们正在对不同的数据进行操作。

但是,如果您想要对它们使用相同的数据怎么办?

这是另一个问题的话题。 Google开始“互斥”。