JavaRush /Java 博客 /Random-ZH /Java 中的多线程:本质、优点和常见陷阱

Java 中的多线程:本质、优点和常见陷阱

已在 Random-ZH 群组中发布
你好!首先,恭喜您:您已经到达了 Java 多线程的主题!这是一项重大成就,还有很长的路要走。但做好准备:这是本课程中最困难的主题之一。重点不在于这里使用了复杂的类或许多方法:相反,甚至没有两打。更重要的是你需要稍微改变一下你的想法。以前,您的程序是按顺序执行的。有些代码行跟随其他代码行,有些方法跟随其他代码行,总体来说一切都很清楚。首先,计算一些东西,然后将结果显示在控制台上,然后终止程序。要理解多线程,最好从并发的角度来思考。让我们从一些非常简单的事情开始:)Java 中的多线程:本质、优点和常见陷阱 - 1想象一下您的家人正在从一所房子搬到另一所房子。搬家的一个重要部分是收拾你的书。你积累了很多书,你需要把它们装进盒子里。现在只有你是自由的。妈妈在准备食物,哥哥在收衣服,妹妹去商店了。至少你可以独自完成任务,而且迟早你甚至会自己完成任务,但这会花费很多时间。然而,20 分钟后你姐姐就会从商店回来,她无事可做。这样她就可以加入你了。任务还是一样:把书放进盒子里。它的运行速度只是原来的两倍。为什么?因为工作是并行完成的。两个不同的“线程”(你和你的妹妹)同时执行相同的任务,如果没有任何变化,与你独自完成所有事情的情况相比,时间差将非常大。如果你的兄弟很快完成了他的任务,他可以帮助你,事情会进展得更快。

Java中多线程解决的问题

本质上,Java 多线程的发明是为了解决两个主要问题:
  1. 同时执行多个操作。

    在上面的示例中,不同的线程(即家庭成员)并行执行多个操作:洗碗、去商店、折叠东西。

    可以举一个更“程序员”的例子。想象一下您有一个带有用户界面的程序。单击“继续”按钮时,程序内应进行一些计算,用户应看到以下界面屏幕。如果按顺序执行这些操作,则单击“继续”按钮后,程序将直接冻结。用户将看到相同的屏幕,并带有“继续”按钮,直到所有内部计算完成并且程序到达开始绘制界面的部分。

    好吧,让我们等几分钟!

    Java 中的多线程:本质、优点和常见陷阱 - 3

    我们还可以重新编写我们的程序,或者正如程序员所说,“并行化”。让必要的计算在一个线程中执行,并在另一个线程中进行界面渲染。大多数计算机都有足够的资源来执行此操作。这样的话,程序就不会“傻”了,用户会从容地在界面屏幕之间移动,不用担心里面发生了什么。它不会干扰:)

  2. 加快计算速度。

    这里一切都简单得多。如果我们的处理器有多个核心,并且现在大多数处理器都是多核的,那么我们的任务列表可以由多个核心并行解决。显然,如果我们需要解决 1000 个问题,并且每个问题都在一秒钟内解决,则一个核心将在 1000 秒内处理该列表,两个核心将在 500 秒内处理,三个核心将在 333 秒多一点的时间内处理,依此类推。

但是,正如您在讲座中已经读到的那样,现代系统非常智能,即使在一个计算核心上,当任务交替执行时,它们也能够实现并行性或伪并行性。让我们从一般的事情转向具体的事情,并熟悉与多线程相关的 Java 库中的主类 - java.lang.Thread。严格来说,Java 中的线程是由类的实例表示的Thread。也就是说,要创建并运行 10 个线程,您将需要 10 个此类的对象。我们来写一个最简单的例子:
public class MyFirstThread extends Thread {

   @Override
   public void run() {
       System.out.println("I'm Thread! My name is " + getName());
   }
}
要创建和启动线程,我们需要创建一个类并从java.lang. Thread并重写其中的方法run()。最后一项非常重要。我们在方法中run()规定了线程必须执行的逻辑。现在,如果我们创建一个实例MyFirstThread并运行它,该方法run()将在控制台上打印一行及其名称:该方法会getName()打印线程的“系统”名称,该名称是自动分配的。但事实上,为什么要用“如果”呢?让我们来创建并测试吧!
public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {

           MyFirstThread thread = new MyFirstThread();
           thread.start();
       }
   }
}
控制台输出: 我是线程!我的名字是 Thread-2 我是 Thread!我的名字是 Thread-1 我是 Thread!我的名字是 Thread-0 我是 Thread!我的名字是 Thread-3 我是 Thread!我的名字是 Thread-6 我是 Thread!我的名字是 Thread-7 我是 Thread!我的名字是 Thread-4 我是 Thread!我的名字是 Thread-5 我是 Thread!我的名字是 Thread-9 我是 Thread!我的名字是 Thread-8 我们创建 10 个线程(对象) ,它们MyFirstThread继承Thread并通过调用对象的方法来启动它们start()。调用一个方法后,start()该方法开始工作run(),并执行其中编写的逻辑。请注意:线程名称不按顺序排列。很奇怪,为什么不依次执行:Thread-0、、、Thread-1等等Thread-2?这正是标准的“顺序”思维不起作用的例子。事实上,在本例中我们只发出命令来创建和启动 10 个线程。它们的启动顺序由线程调度程序决定:操作系统内部的一种特殊机制。它到底是如何构造的以及它根据什么原则做出决策是一个非常复杂的话题,我们现在不会深入探讨。主要要记住的是程序员无法控制线程执行的顺序。要认识到情况的严重性,请尝试main()多运行上面示例中的方法几次。第二个控制台输出: 我是线程!我的名字是 Thread-0 我是 Thread!我的名字是 Thread-4 我是 Thread!我的名字是 Thread-3 我是 Thread!我的名字是 Thread-2 我是 Thread!我的名字是 Thread-1 我是 Thread!我的名字是 Thread-5 我是 Thread!我的名字是 Thread-6 我是 Thread!我的名字是 Thread-8 我是 Thread!我的名字是 Thread-9 我是 Thread!我的名字是 Thread-7 第三个控制台输出: 我是 Thread!我的名字是 Thread-0 我是 Thread!我的名字是 Thread-3 我是 Thread!我的名字是 Thread-1 我是 Thread!我的名字是 Thread-2 我是 Thread!我的名字是 Thread-6 我是 Thread!我的名字是 Thread-4 我是 Thread!我的名字是 Thread-9 我是 Thread!我的名字是 Thread-5 我是 Thread!我的名字是 Thread-7 我是 Thread!我的名字是Thread-8

多线程产生的问题

在书籍的示例中,您看到多线程解决了非常重要的问题,并且它的使用加快了我们程序的工作速度。在很多情况下——很多次。但多线程被认为是一个复杂的话题并非没有道理。毕竟,如果使用不当,只会产生问题而不是解决问题。当我说“制造问题”时,我并不是指抽象的东西。多线程可能导致两个具体问题:死锁和竞争条件。死锁是多个线程正在等待彼此占用的资源,而没有一个线程可以继续执行的情况。我们将在以后的讲座中详细讨论它,但现在这个例子就足够了: Java 中的多线程:本质、优点和常见陷阱 - 4 想象线程 1 正在处理某个对象 1,而线程 2 正在处理对象 2。程序是这样写的:
  1. 一旦线程 2 停止使用对象 2 并切换到对象 1,线程 1 将停止使用对象 1 并切换到对象 2。
  2. 一旦线程 1 停止使用对象 1 并切换到对象 2,线程 2 将停止使用对象 2 并切换到对象 1。
即使没有深入了解多线程,您也可以轻松理解它不会产生任何结果。线程永远不会改变位置并且将永远等待彼此。这个错误看起来很明显,但实际上并非如此。您可以轻松地允许它进入程序。我们将在接下来的讲座中查看导致死锁的代码示例。顺便说一下,Quora 有一个很好的现实例子来解释什么是死锁。“在印度的一些邦,除非你注册为农民,否则他们不会向你出售农业用地。但是,如果您没有农业用地,则不会注册为农民。” 太好了,我还能说什么!:) 现在关于竞争条件 - 竞争的状态。 Java 中的多线程:本质、优点和常见陷阱 - 5竞争条件是多线程系统或应用程序中的设计缺陷,其中系统或应用程序的操作取决于代码部分的执行顺序。请记住运行线程的示例:
public class MyFirstThread extends Thread {

   @Override
   public void run() {
       System.out.println("Выполнен поток " + getName());
   }
}

public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {

           MyFirstThread thread = new MyFirstThread();
           thread.start();
       }
   }
}
现在想象一下该程序负责操作准备食物的机器人! Thread-0 从冰箱里取出鸡蛋。流 1 打开炉子。Stream-2拿出煎锅放在炉子上。流 3 点燃炉子上的火。流4将油倒入锅中。流5将鸡蛋打碎,倒入煎锅中。流 6 将贝壳扔进垃圾桶。Stream-7 将完成的炒鸡蛋从火上移开。Potok-8 将炒鸡蛋放在盘子上。小溪9洗碗。 看看我们程序的结果: Thread-0执行 Thread-2线程执行 Thread-1线程执行 Thread-4线程执行 Thread-9线程执行 Thread-5线程执行 Thread-8线程执行 Thread-7线程执行 Thread-7线程执行 -3 线程-6 线程执行。 脚本有趣吗?:) 这都是因为我们程序的操作取决于线程的执行顺序。只要稍微违反这个顺序,我们的厨房就会变成地狱,一个疯狂的机器人会摧毁周围的一切。这也是多线程编程中的常见问题,您会不止一次听到这个问题。在讲座的最后,我想向您推荐一本关于多线程的书。
Java 中的多线程:本质、优点和常见陷阱 - 6
《Java 并发实践》写于 2006 年,但并没有失去其相关性。它涵盖了 Java 中的多线程编程,从基础知识开始,到最常见错误和反模式的列表结束。如果您决定成为多线程编程大师,那么这本书是必读的。下期讲座再见!:)
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION