-
1、多线程,多任务,并发的概念
Java并发这一术语涵盖了java平台的多线程、并发和并行,它包括java并发工具,问题和解决办法。本教程涉及到java多线程相关的核心概念、并发结构、并发问题、多线程带来的好处和代价。
什么是多线程?
多线程是指在同一个程序里执行多个线程。线程就像是执行程序的一个单独的CPU,所以多线程程序就像是有多个CPU同时在执行不同部分的代码一样。

不过一个线程并不等同于一个CPU。通常情况下,多个线程共享一个CPU的执行时间。CPU在每个线程之间以一定间隔切换着执行。当然,程序的各个线程也可以在不同的CPU上运行。

为什么需要多线程?
为什么要在程序中使用多线程呢?原因有很多,常见的原因是:
**更好的使用单个CPU;
更好的使用多个CPU或者多核CPU;
从程序响应的角度给用户更好的体验;
从公平的角度给用户更好的体验。**我们会在下面的章节里更详细的解释这些原因。
更好的使用单个CPU
使用多线程最常见的一个原因是为了能够更好的使用计算机资源。例如,如果某个线程通过网络发送了请求,在它等待网络返回结果的这段时间,另一个线程可以使用CPU来处理别的事情。此外,如果计算机配置了多个CPU或者多核CPU,那程序通过多线程就可以使用这些额外的CPU核心。
更好的使用多个CPU或者多核CPU
如果计算机配置了多个CPU或者多核CPU,那就需要在你的程序中使用多线程,因为这样才能用到所有的CPU或者CPU核心。一个单独的线程最多只能使用一个CPU。但是正如前文提到的那种情况,一个线程有时甚至没办法完全的利用好一个CPU。
从程序响应的角度给用户更好的体验
使用多线程的另一个原因是为了提供更好的用户体验。例如,假如你在某个用户界面点击了一个按钮,结果是它要发送一个网络请求。问题在于,该让哪个线程执行该请求呢?如果让更新用户界面的线程来处理,那在等待请求响应的这段时间,用户可能会察觉到界面“卡住”了。更好的做法是用一个后台线程来处理该请求,这样的话用户界面线程才有空闲来响应其他的用户请求。
从公平的角度给用户更好的体验
第四个原因是为了让用户更公平的分享计算机资源。例如,我们设想有一个服务器用于接收用户请求,但是这个服务器只有一个线程来处理这些请求。如果某个用户的请求需要花很长的时间来处理,那么所有其他用户的请求都要等这个请求处理完成才行。我们也可以让每个用户的请求都在自己的线程里执行,这样就避免了某个任务独占CPU的情况。
多线程VS多任务
在以前,计算机只有一个CPU,而且同一时间只能运行一个程序。由于多数较小的计算机不具备同时运行多个程序的能力,也就不存在多任务。当然了,相对于个人计算机,很多大型计算机很早就已经具备同时运行多个程序的能力了。
多任务
后来出现了多任务,也就是计算机可以同时运行多个程序(也就是任务或进程)。但这实际上并不是“同一时刻”,而是程序之间共享一个CPU。操作系统需要在程序之间切换,每个程序运行一小段时间,然后再切换到另一个。
多任务的出现给软件开发者带来了新的挑战。程序不能再像以前那样,想当然的以为可以占用所有的CPU时间,内存以及其他计算机资源。一个良好的程序应该把不用的资源释放掉,这样其他程序才能使用。
多线程
后来又出现了多线程,也就是同一个程序内部可以执行多个线程。我们可以把执行的线程当作执行该程序的一个CPU,所以同一个程序由多个线程执行就像是该程序由多个CPU执行一样。
多线程,有点难
对于某些类型的程序来说,用多线程来提升性能是非常好的方法。但是多线程比多任务的难度更大。由于多个线程在同一个程序里执行,因而它们会同时读取和写入同一块内存。这样的错误在单线程程序中是无法发现的。有些错误在只有单个CPU的机器上可能也发现不了,因为两个线程不可能真的“同时”执行。但是现代计算机出现了多核甚至多个CPU的配置。这就意味着这些单独的线程可以在各自的CPU或核心中同时执行。

如果一个线程读某一块内存的同时另一个线程在写这块内存,那第一个线程最后读到的值会是什么呢?是原来的值,还是第二个线程写入的值,还是两者混合之后的值呢?还有,要是两个线程同时写同一块内存,那在写完之后,这块内存的值会是什么,是头一个线程写的值,还是第二个线程写的值,还是这两者混合的值呢?
这种冲突如果不能很好的预防,任何结果都有可能。这甚至会导致程序的行为无法预测,因为每次的执行结果会不一样。因此,对于开发者而言,知道怎样采取正确的预防手段十分重要。开发者需要学习控制线程访问共享资源的方法,比如如何访问内存,文件以及数据库等。
这是本java并发教程要讲解的主题之一。
java的多线程和并发
java是最早的让开发者的多线程编程变得更简单的编程语言之一。java从一开始就已经具备了多线程的能力。因此,java开发者经常会遇到我们在前面描述过的问题。这是我写这篇java并发教程的原因。它既可以作为自己的笔记,也能让做java开发的同伴们参考受益。
本教程将主要侧重于java多线程,但是多线程中出现的一些问题,和多任务以及分布式系统中出现的问题很相似。因而本教程中也会参考多任务以及分布式系统。所以我没有用“多线程”,而是使用了“并发”这个词。
并发模型
最初的java并发模型假设同一个程序内执行的多个线程会共享对象。这种类型的并发模型通常被称为“共享状态并发模型”。有许多并发编程语言的结构和工具包都是根据这种并发模型来设计的。
然而,自第一本java并发的书问世以来,甚至自java5并发工具包公布以来,并发的架构和设计都已经发生了翻天覆地的变化。共享状态并发模型带来了大量的并发问题,并且这些问题很难巧妙的处理。因此,另一种并发模型流行了起来,它被称为“无共享”或者叫“分离状态并发模型”。在这一模型中,线程不会共享任何对象和数据。这样就避免了很多在“共享状态并发模型”中存在的并发访问问题。
而今已出现新的异步“分离状态”平台和工具包,比如Netty,Vert.x,Play / Akka和Qbit 。新的非阻塞式并发算法和非阻塞式工具也发布了,比如我们的工具包中新增了 LMax Disrupter。java7的Fork/Join框架还推出了新的并行函数式编程,java8又推出了collection streams 调用接口。
随着这些新的发展,又到了我更新java并发教程的时机了。因此这本教程又一次开始不断扩充。我会在空闲时间撰写并发布新的教程。
-
2、CPU使用,程序设计,程序响应,资源分配
多线程最明显的好处有:
更好的使用CPU;
某些情况下让程序设计更简单;
让程序更好的响应;
让CPU资源在不同任务间分配的更均衡。更好的使用CPU
设想有这样一个程序,它从本地文件系统中读文件并处理。我们假定从硬盘中读一个文件需要5s,然后处理这个文件需要2s。所以处理两个文件就需要:
读文件A,5s
处理文件A,2s
读文件B,5s
处理文件B,2s
总共14s。在从硬盘读文件的时候,CPU大部分时间都在等待硬盘读取数据,所以CPU多数时间处于空闲状态。我们可以改变操作顺序从而更好的使用CPU,比如看下面的顺序:
读文件A,5s
读文件B,5s + 处理文件A,2s
处理文件B
总共12sCPU先等待读取第一个文件,接着开始读第二个文件。当计算机IO设备在读第二个文件时,CPU处理第一个文件。别忘了,CPU等待从硬盘读文件的时候,几乎处于空闲状态。
总之,CPU在等待IO时还可以处理其他事情。不一定是磁盘IO,也可以是网络IO或者本机用户的输入。网络和磁盘IO通常比CPU和内存IO要慢的多。
让程序设计更简单
假如你要手工编写一个单线程的程序来实现上面的读取和处理文件的次序,你就需要跟踪每个文件的读取和处理状态。然而你也可以开启两个线程,每个线程仅负责读取和处理一个单独的文件。在等待磁盘读取文件的时候,这个线程会阻塞,而其他线程就可以让CPU处理已经读到的部分文件。于是,磁盘一直保持在工作状态,读取大量的文件到内存中。这样一来便更好的使用了磁盘和CPU,也更易于编程,因为每个线程只需要跟踪一个单独的文件。
让程序更好的响应
把单线程程序改为多线程程序的另一个常见目的,是为了让程序更好的响应。设想一个监听端口请求的服务端程序。它接收到服务并处理该请求,然后继续监听。服务端大概是这样的循环:

如果请求要花很长时间处理,那在这段时间里,其他用户则无法再发送请求。因为请求只有在服务端监听时才能被收到。
另一种设计是由监听线程把请求传递给工作线程,之后立即返回到监听状态。工作线程处理完请求后向用户发送返回消息。这种设计大概是这样:

这使得服务端线程可以很快的返回监听状态,于是更多的用户可以发请求给服务端,服务端的响应性更好了。
对于桌面应用也是一样的。如果你点击一个按钮开启一个长时间任务,并由更新界面的线程执行该任务。那么在该任务执行时,程序看上去是不响应的。也可以把任务交给工作线程,这样当工作线程忙于处理任务时,界面线程就有空闲来响应其他的用户请求。工作线程完成任务后通知界面线程,于是界面线程将任务结果更新到程序界面。采用工作线程设计的程序会显得有更好的响应性。
让CPU资源在不同任务间分配的更均衡
设想有一个接受客户端请求的服务端。假设其中一个客户端发送了一个请求,处理该请求耗时很长——比如要10秒。如果服务端只用一个线程来处理所有任务,那么直到该耗时请求被完全处理完,所有后面的请求都必须等待。
通过将CPU时间分配到多个线程,并且让CPU在线程之间切换的方式,多个线程便可以更公平的共享CPU执行时间。这样即使其中某个请求很耗时,其他的快速请求可以和这个耗时请求并行执行。当然,这会让耗时请求处理的更慢,因为该请求无法再独占CPU来处理自己的任务了。即便如此,其他请求等待的时间会更短,因为它们不用再等耗时任务完成就可以处理了。而假如只有一个耗时请求待处理,那该任务仍然可以独占CPU。
-
3、程序设计,上下文切换,资源消耗
把程序从单线程转变到多线程并非仅仅带来好处,也需付出代价。我们不应该一味的让程序支持多线程,而只应在权衡了利大于弊的情况使用多线程。当我们不能确定时,可以先试着衡量下程序的性能和响应性,而不是凭空猜想。
多线程的设计更复杂
虽然多线程程序的某些部分比单线程更简单,但是其他部分更复杂。多线程代码在执行时需要特别注意共享数据的访问。线程之间的交互远不是一直这么简单的。因没有正确线程同步而引起的错误会很难发现,复现和解决。
多线程上下文切换带来开销
CPU从一个线程切换到另一线程执行时,需要保存当前线程的局部数据和程序指针等,还要加载下一个线程待执行的局部数据和程序指针等。这个切换称为“上下文切换”。CPU从某个线程的执行上下文切换到另一个线程的执行上下文。
上下文切换的开销可不低,在没必要时不应在线程之间切换。
在维基百科上可以读到更多关于上下文切换的内容:
http://en.wikipedia.org/wiki/Context_switch多线程增加了资源消耗
线程需要一定的计算机资源才能运行,除了CPU时间,还需一定的内存来保存局部栈,可能还需要占用操作系统的一些资源来管理线程。我们可以试试在一个程序中创建100个线程,这些线程不做任何事只是等待,然后看看该程序在运行时占用了多少内存。
关闭