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上同时(不是真正的同时,而是看来是同时,因为cpu要在多个程序间切换)运行多个程序。

并行,是每个cpu运行一个程序。

打个比方。并发,就像一个人(cpu)喂2个孩子(程序),轮换着每人喂一口,表面上两个孩子都在吃饭。并行,就是2个人喂2个孩子,两个孩子也同时在吃饭。

多线程的状态

初始状态(new)

new出来一个实例,就进入了初始状态

可运行/就绪状态(Runnable)

  • 调用线程的start()方法,此线程进入可运行状态。
  • 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入可运行状态。
  • 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入可运行状态。
  • 锁池里的线程拿到对象锁后,进入可运行状态。

当前线程具备了运行条件,但调度程序还没有调度到。

运行状态(Running)

就绪状态的线程获取了CPU,执行程序代码。

阻塞状态(Blocked)

线程因为某种原因释放了运行资源,暂停运行。只有线程再次变为可运行状态,才可能再次运行。

  • 执行wait()方法,释放锁资源
  • 在获取锁资源时,被其他线程占用,处于等待状态
  • 运行sleep()或者join()方法

死亡状态(dead)

线程执行完了run()方法,或者执行run()方法过程中异常退出,或者主线程执行完毕退出。

 

 

posted @ 2017-09-24 20:53  bug-zhang  阅读(360)  评论(0编辑  收藏  举报