【问题标题】:When do multiple threads access the same code?多个线程何时访问相同的代码?
【发布时间】:2019-05-21 17:47:29
【问题描述】:

我的问题是:

  1. Java 程序是否默认只创建 1 个线程?
  2. 如果是,并且如果我们创建一个多线程程序,多个线程何时访问 Java 对象的相同代码?

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

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

如果不是,请展示一个关注线程安全的示例程序。

【问题讨论】:

    标签: java multithreading thread-safety


    【解决方案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));
            }
        }
    }
    

    【讨论】:

      【解决方案2】:

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

      真的取决于你的代码在做什么。一个简单的System.out.println() 调用可能 可能只是创建一个线程。但是,例如,只要您打开一个 Swing GUI 窗口,就会出现至少一个其他线程(对用户输入做出反应并负责 UI 更新的“事件调度程序线程”)。

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

      你的误解。对象没有代码。基本上,一个线程将运行一个特定的方法;要么是它自己的run() 方法,要么是它可用的其他方法。然后线程只执行该方法,以及从该初始方法触发的任何其他方法调用。

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

      【讨论】:

        【解决方案3】:

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

        当我们学习编程时,他们告诉我们计算机程序是一系列指令,他们告诉我们计算机执行这些指令,从一些良好的开始定义的入口点(例如,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 不是线程。它只是一个句柄,您的程序可以使用它来启动线程并查询其线程的状态并控制它。)

        当新线程开始执行程序指令时,它将通过调用r.run()开始。如您所见,r.run() 的主体将导致新线程到DoSomeUsefulThing(),然后在r.run() 返回之前DoSomeOtherThing()

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

        所以,

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

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

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

        请注意,主线程在启动新线程后没有停止。它继续执行t.start() 调用之后的任何内容。在这种情况下,它接下来要做的就是调用DoSomeUsefulThing()。但这与程序告诉新线程要做的事情一样!如果 DoSomeUsefulThing() 需要很长时间才能完成,那么两个线程将同时执行此操作...因为这是程序告诉它们执行的操作。

        请展示一个关注线程安全的示例程序

        我刚刚做了。

        想想DoSomeUsefulThing() 可能在做什么。如果它在做一些有用的事情,那么它几乎肯定是在对某处的某些数据 做一些事情。但是,我没有告诉它对什么数据进行操作,所以很有可能,两个线程同时对相同的数据做某事。

        这很有可能不会很好。

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

        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);
        

        那里!现在这两个线程在做同样的事情,但是他们对不同的数据做这件事。

        但是如果您希望它们对相同的数据进行操作怎么办?

        这是一个不同问题的主题。谷歌“互斥”开始。

        【讨论】:

          【解决方案4】:
          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 的状态都有被破坏的危险。这是因为状态是由多个线程更新的,没有同步。

          【讨论】:

          • 非常感谢,再问一个问题。当线程获取具有同步和“正常”方法的对象的监视器时,为什么它只会阻止其他线程对同步方法的访问。为什么仍然允许其他线程访问“正常”方法?
          • @Spires 请不要那样做。一旦您最初的问题得到回答,就不要再提出更多问题。考虑接受答案,或要求澄清。但不是“另一件事”评论乒乓球。那么:请理解,这个社区不是你学习java基础的替代品。
          猜你喜欢
          • 2011-12-31
          • 1970-01-01
          • 1970-01-01
          • 2010-11-30
          • 1970-01-01
          • 2013-02-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多