Java并发/多线程系列——初识篇
回到过去,电脑有一个CPU,一次只能执行一个程序。后来多任务处理意味着计算机可以同时执行多个程序(AKA任务或进程)。这不是真的“同时”。单个CPU在程序之间共享。操作系统将在运行的程序之间切换,在切换之前执行每个程序一段时间。
随着多任务处理,软件开发人员面临新的挑战。程序不能再假定所有的CPU时间都可用,也不能使用所有内存或任何其他计算机资源。“good citizen”程序应该释放所有不再使用的资源,所以其他程序可以使用它们。
后来又来了多线程,这意味着你可以在同一个程序中有多个执行线程。执行线程可以被认为是执行程序的CPU。当您有多个线程执行相同的程序时,就像在同一程序中执行多个CPU。
多线程是可以提高某些类型程序性能的好方法。然而,多线程比多任务更具挑战性。线程在相同的程序中执行,就会同时读取和写入同一个存储器,这可能导致在单线程程序中看不到错误。在单CPU机器上可能看不到这些错误,因为两个线程从未真正“同时”的执行。现代电脑虽然配有多核CPU,甚至还有多个CPU。这意味着单独的线程可以由多个的内核或多个CPU同时执行。
如果一个线程在另一个线程写入内存时读取该位置的值,那么第一个线程会读取什么值?旧值?第二个线程写的值?还是两者之间的混合的一个值?或者,如果两个线程同时写入内存的同一位置,完成后会留下什么值?第一个线程写的值?第二个线程写的值?还是两个值的混合?
如果没有适当的预防措施,任何这些结果都是可能的,甚至是不可预测。结果可能会不时变化,因此,开发人员知道如何采取正确的预防措施,这就意味着学习控制线程如何访问内存,文件,数据库等共享资源是非常重要的。
多线程的优缺点
优点
- 更高的资源利用率
- 响应速度快
- 占用大量时间的任务可定期将处理器时间让给其它任务
- 可设置各个任务的优先级
缺点
- 对线程进行管理要求额外的 CPU开销。线程的使用会给系统带来上下文切换的额外负担。当这种负担超过一定程度时,多线程的特点主要表现在其缺点上,比如用独立的线程来更新数组内每个元素。
- 线程的死锁问题
- 多个线程同时操作同一资源,造成无法预知的错误
并发模型
Parallel Workers(并行工作)
第一个并发模型就是我所说的parallel worker(并行工作)模型。进入作业被分配给不同的工作人员。以下是并行工作并发模型的图示:
在并行工作并发模型中,委托者将传入的作业分配给不同的工作人员。每个工作人员完成了全部工作。工作并行工作,运行在不同的线程,可能在不同的CPU上。
并行工作并发模型是Java应用程序中最常用的并发模型(尽管正在发生变化)。java.util.concurrent 包中的许多并发实用程序都设计用于此模型。
并行工作的优点
要增加应用程序的并行化,只需添加更多的工作人员。实现简单,易上手。
并行工作的缺点
一旦共享状态进入并行工作并发模式,它就会变得复杂了。线程需要访问共享数据,以确保一个线程的更改对其他线程是可见的(推送到主内存,而不仅仅停留在执行线程的CPU的CPU缓存中)。 线程需要避免竞争条件, 死锁和许多其他共享状态并发问题。
共享状态可以由系统中的其他线程修改。因此,工作人员每次需要重新读取状态,以确保它正在处理最新的副本。无论共享状态是保存在内存还是外部数据库中,都是如此。内部不保持状态的工作者(但每次需要重新读取数据)被称为无状态。每次需要重新读取数据可能会变慢。特别是如果状态存储在外部数据库中。
并行工作者模型的另一个缺点是作业(job)执行顺序是非确定性的。没有办法保证首先或最后执行哪些工作。因此不能保证一个作业发生在另一个作业之前。
Assembly Line(流水线)
这些工人组织得像工厂里的工人一样。每个工作人员只执行完整工作的一部分。当该部分完成时,作业将作业转发到下一个工作人员。每个工人都在自己的线程中运行,与其他工作人员共享状态。 这也有时被称为共享的并发模式。
使用装配线并发模型的系统通常设计为使用非阻塞IO。非阻塞IO意味着当工作人员启动IO操作(例如从网络连接读取文件或数据)时,工作人员不会等待IO调用完成,IO操作速度很慢,所以等待IO操作完成是浪费CPU时间。CPU可能在做其他事情。当IO操作完成时,IO操作的结果(例如数据读取或写入的数据状态)传递给另一个工作。
实际上,这些工作可能不会沿着一条流水线流动。由于大多数系统可以执行多个作业,因此根据需要完成的任务,作业将从工作流程转移到工作。实际上,可能会有多个不同的虚拟装配线同时进行。
作业甚至可以转发给多个工作者进行并发处理。例如,作业可以转发给作业执行器和作业记录器。该图说明如何通过将其作业转发给同一个工作人员(中间装配线中的最后一个工作人员)来完成所有三条装配线的完成:
Actors vs. Channels(演员和通道)
actors和channels是两个类似的流水线模型的例子。
在演员模型中,每个工人都被称为演员。演员可以直接发送消息给对方。消息被异步发送和处理。演员可以用于实现一个或多个作业处理装配线,如前所述。以下是演示模型的图示:
在通道模式中,工作者之间不直接沟通。相反,他们在不同的通道上发布他们的消息(事件)。其他工作者可以在这些通道上收听消息,而不需要发送方知道谁正在收听。这是一个说明通道模型的图:
通道模式对我们来说似乎更加灵活。工作者不需要知道工作者稍后将在装配线中处理什么工作。它只需要知道什么通道转发作业(或发送消息等)。通道上的听众可以订阅并取消订阅,而不影响写入通道的工作者。这允许工人之间稍微松动的耦合。
流水线模型的优点
没有共享状态。工作者之间不用分享任何状态,因此无需考虑并发访问共享状态可能产生的所有并发问题。
有状态的工作者。由于工人知道没有其他线程修改其数据,所以工作人员可以是有状态的。他们可以保留他们需要在内存中操作的数据,只需将最后的外部存储系统进行更改。因此,有状态的工作者通常可能比无状态的工作者更快。
可实现工作订阅。
流水线模型的缺点
代码实现上会更加的复杂,代码的阅读性会降低。因为工作者的代码有时被写成回调处理程序员,有许多嵌套回调处理程序的代码可能会导致一些开发人员调用callback hell(回调地狱)。 callback hell只是意味着很难跟踪代码是在实际程序中是怎么调用的,以及确保每个回调都可以访问所需的数据。
并发性(concurrency)与并行性(parallellism)
术语并发性和并行性通常用于多线程程序。但并发和并行究竟是什么意思,而且是相同的术语还是什么?
简短的答案是“否”。它们不是相同的术语,尽管它们在表面上看起来非常相似。也花了我一些时间来终于找到并了解并发和并行性之间的区别。因此,我决定在这个Java并发教程中添加一个关于并发性与并行性的文本。
并发,是在同一个cpu上同时(不是真正的同时,而是看来是同时,因为cpu要在多个程序间切换)运行多个程序。
并行,是每个cpu运行一个程序。
打个比方。并发,就像一个人(cpu)喂2个孩子(程序),轮换着每人喂一口,表面上两个孩子都在吃饭。并行,就是2个人喂2个孩子,两个孩子也同时在吃饭。
多线程的状态
初始状态(new)
new出来一个实例,就进入了初始状态
可运行/就绪状态(Runnable)
- 调用线程的start()方法,此线程进入可运行状态。
- 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入可运行状态。
- 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入可运行状态。
- 锁池里的线程拿到对象锁后,进入可运行状态。
当前线程具备了运行条件,但调度程序还没有调度到。
运行状态(Running)
就绪状态的线程获取了CPU,执行程序代码。
阻塞状态(Blocked)
线程因为某种原因释放了运行资源,暂停运行。只有线程再次变为可运行状态,才可能再次运行。
- 执行wait()方法,释放锁资源
- 在获取锁资源时,被其他线程占用,处于等待状态
- 运行sleep()或者join()方法
死亡状态(dead)
线程执行完了run()方法,或者执行run()方法过程中异常退出,或者主线程执行完毕退出。