Java 多线程基础


Brief History of Concurrency

最早的时候一个计算机只有一个 CPU,一次只能执行一个程序。后来慢慢的出现了多任务,可以同时执行多个程序(进程)了,但这时候还并不是真正的并行执行,是操作系统通过将 CPU 资源分割为时间片在多个程序之间进行调度,从而给人一种并行执行的错觉。多任务随之也带来一些新的问题,程序不能再假设自己拥有所有的 CPU、内存及其它计算机资源,并且能够在使用完资源之后及时释放以便其它程序使用。

再后来又出现了多线程,即一个程序(进程)里可以执行多个线程,感觉就好像是有多个 CPU 在同时执行同一个程序。多线程对一些问题可以极大的提升性能,但同时带来了比多进程并行更大的挑战。程序中的多个线程共享这个程序的资源,它们在同时读写同一块内存区域,这时就会出现一些我们在单线程程序中从来都不会遇到的新问题。另外随着多核 CPU 的出现,还会出现一些单 CPU 机器上不会发生的一些问题,因为单核下程序并不会真正的并行执行,但在多核中成了现实。

如果一个线程在读一个内存时,另一个线程正向该内存进行写操作,那进行读操作的那个线程将获得什么结果呢?同样的如果是两个线程同时写同一个内存,在操作完成后将会是什么结果呢?如没有合适的预防措施,任何结果都是可能的,这种行为的发生甚至不能预测。

Java 从一开始就支持多线程,在 Java 开发中经常会遇到多线程相关的问题,因此 Java 程序开发者掌握如何正确的控制线程去访问共享的资源就显得非常重要。

Why Multithreading

ref: Why we use Threads in Java?

既然多线程开发这么复杂麻烦,为什么还要用呢?

  1. 使程序可以同时处理多个任务,例如在 GUI 程序中,我点击了下载按钮(可能耗时很久),这时我想去修改个头像总不能让我一直等着下载完才能操作吧。

  2. 充分利用 CPU 的计算能力,多线程之所以可以做到这些是因为随着科技发展 CPU 计算能力越来越强并出现了多核计算机,单线程根本没有能力充分利用这些资源,但通过多线程我们可以尽可能的利用其这些昂贵的资源来使程序服务更多的用户并且更快速的响应。

  3. 减小程序响应时间

  4. 同时服务更多的用户

其实上面的几点都是相互关联的,总而言之可以归纳为一句话:我们希望利用多线程让程序变得更快。

下面举两个多线程的例子:

  1. 假设一种文件处理的场景,从磁盘读取一个文件需要5秒,处理一个文件需要2秒。单线程处理两个文件的时间是 5+2+5+2=14s,而如果我们启动两个线程 t1&t2,t1 只负责读文件A,t2 只负责处理文件B,这样的话 CPU 可以在 t2 读取第二个文件 IO 阻塞的时候分配给 t1 处理已经读取的第一个文件,总共需要的时间就减少为 5+5+2=12s 了。通过将 CPU 等待 IO 的时间分配去做一些其他的事情就可以提升 CPU 的利用率。

  2. 设想一个服务器应用,它在某一个端口监听进来的请求。当一个请求到来时,它去处理这个请求,然后再返回去监听。单线程程序中如果一个请求需要占用大量的时间来处理,在这段时间内新的客户端就无法发送请求给服务端。而在多线程程序中我们可以有更好的设计:监听线程每接受一个请求就把请求传递给工作线程(线程池),然后立刻返回去监听。工作线程处理这个请求并发送一个回复给客户端。这样同一时间内更多的客户端能够发送请求给服务端,服务也变得响应更快。

Multithreading Costs

首先很多情况下多线程让编码设计变得更加复杂,对线程访问共享数据非常小心以避免出现错误的结果,并且多线程产生的错误相对更难排查;其次

线程上下文切换会带来额外开销,CPU 从执行一个线程切换到另外一个线程时,需要先存储当前线程的本地数据、程序指针等,然后载入另一个线程的本地数据、程序指针等,最后才开始执行。而这样的切换并不廉价,如果没有必要,应该尽量减少上下文切换的发生。

最后多线程也会增加资源消耗,需要一定的内存来存放线程的本地数据,也需要占用操作系统中一些资源来管理线程。

总的来说,多线程确实能带来很多好处,但相应的也需要付出一定的代价,在编码中不要为了使用多线程而使用多线程,而应该在确定使用多线程所带来的好处比代价更大的时候才去使用它。

一些基本概念

线程 vs 进程

  • 进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含 1-n 个线程。进程是资源分配的最小单位。简单讲进程就是在某种程度上相互隔离的、独立运行的程序。

  • 线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。线程是 cpu 调度的最小单位。

线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。 线程状态转换图示

  1. 新建状态(New)
    新创建了一个线程对象。例如执行Thread1 th1 = new Thread1("A")

  2. 就绪状态(Runnable)
    线程对象创建后,其他线程调用了该对象的 start() 方法。该状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权。需要特别注意的是 start() 方法被调用后该线程并不是立即执行,而是处于就绪状态等待操作系统分配 CPU 时间。

  3. 运行状态(Running)
    就绪状态的线程获取了CPU,执行程序代码。

  4. 阻塞状态(Blocked)
    阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

    • 等待阻塞:运行的线程执行 wait() 方法,JVM 会把该线程放入等待池中。(wait会释放持有的锁)
    • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中
    • 其他阻塞:运行的线程执行 sleep() 或 join() 方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep() 状态超时、join() 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。(注意, sleep 是不会释放持有的锁)
  5. 死亡状态(Dead)
    线程执行完了或者因异常退出了 run() 方法,该线程结束生命周期

线程安全 vs 线程同步

线程安全:多个线程访问某个类,这个类始终都能表现出正确的行为。换句话说一个对象可以完全的被多个线程同时使用我们称之为线程安全的。

线程同步:多个线程并发访问共享数据时,保证共享数据在同一个时刻只能被一个线程使用。

如果线程同步则一定线程安全,但是线程安全却不一定通过线程同步来实现!实现线程安全通过是否需要同步分为两大类:

  • 互斥同步
    实现同步最常见的方法是互斥。如临界区、互斥量、信号量等。因此互斥是因,同步是果;互斥是方法、同步是目的。 Java 互斥手段包括 synchronized、JUC 下的 ReentrantLock。

  • 非阻塞同步
    随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。 非阻塞的实现 CAS(compare and swap):CAS 指令需要有3个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。CAS 指令执行时,当且仅当 V 处的值符合旧预期值 A 时,处理器用 B 更新 V 处的值,否则不执行更新,但是无论是否更新了 V 处的值,都会返回 V 的旧值,上述的处理过程是一个原子操作。

  • 无需同步的方案
    要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性,因此会有一些代码天生就是线程安全的。

临界区、互斥量、信号量、事件

  • 临界区(Critical Section)
    如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。 临界区有两个操作原语:EnterCriticalSection() 进入临界区,LeaveCriticalSection() 离开临界区。临界区同步速度很快,但只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。

  • 互斥量(Mutex)
    临界区和互斥量可是视为相同的类型,区别是临界区只能用于进程内,而互斥量可用于不同进程中不不同线程。

  • 信号量(Semaphores)
    它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。它最大的优点是避免了线程在达到信号量最大容量前的线程调度成本,最典型的例子就是数据库连接池。

这几个概念属于操作系统的范畴,是四种进程或线程同步互斥的控制方法,值得注意的是临界区是进程内的对象,只能用来同步本进程内的线程,所以使用它的成本最低。而其它的几个都是系统内核级别的对象,使用的开销要大于临界区。

References

Copyright © jverson.com 2019 all right reserved,powered by Gitbook 11:49

results matching ""

    No results matching ""