什么 是 多线程呢 ?
- 多线程:指的是这个程序(一个进程)运行时产生了不止一个线程。
举个 例子:
如果要计算很大的 Fibonacci 数,是不能使用该算法的,因为其中有大量的重复计算。图 27.1 展示了在计算 F6 时所创建的递归过程实例树。其中,对于对于 FIB(6) 的调用会递归地调用 FIB(5) 和 FIB(4) 。而对 FIB(5)的调用又会去调用 FIB(4) 。这两个 FIB(4) 实例返回的结果完全相同( F4 =3 )。由于 FIB 并没有去记住这些结果,因此对于 FIB(4) 的第二次调用重复了第一次调用的工作。
把多线程计算(由一个代表多线程程序的处理器执行的运行时指令集)看做是一个有向无环图 G= ( V, E )是很有帮助的,我们称其为计算 dag ( directed acyclic graph ) 。图 27.2
中给出了一个示例,其中的计算 dag 来自计算 P-FIB(4) 。从概念层面来讲, V 中的顶点都是指令, E 中边则表示指令间的依赖关系, (u,v ) ∈ E 表示指令 u 必须在指令 v 之前执行。为了方便起见,如果一条指令链中不包含任何并行控制语句(没有 spawn 、 sync 以及被 spawn 例程中 return ——显式的 return 语句或者过程执行完后的隐式 return ),我们就把它们组成一组,形成一个 strand ,每个 strand 都表示一条或者多条指令。涉及并行控制的指令不包括在 strand 中,但是会出现在 dag 结构中。例如,如果一个 strand 有两个后继,那么其中之一必须得被spawn 出来,如果一个 strand 有多个前驱,就表示前驱因为一条 sync 语句被合并在一起。因此,一般来说, V 形成了 strand 集合,而有向边集合 E 则表示由并行控制产生的 strand 间的依赖关系。如果 G 中有一个从 strand u 到strand v 的有向路径,那么我们就说这两个 strands 是(逻辑上)串行的。否则就称其为(逻辑上)并行的。
扎好马步:线程的状态
先来两张图:
各种状态一目了然,值得一提的是"blocked"这个状态:
线程在Running的过程中可能会遇到阻塞(Blocked)情况
- 调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
- 调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)
- 对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。
此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。
内功心法:每个对象都有的方法(机制)
synchronized, wait, notify 是任何对象都具有的同步工具。让我们先来了解他们
他们是应用于同步问题的人工线程调度工具。讲其本质,首先就要明确monitor的概念,Java中的每个对象都有一个监视器,来监测并发代码的重入。在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内,监视器发挥作用。
wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。这意味着wait之后,其他线程可以进入同步块执行。
当某代码并不持有监视器的使用权时(如图中5的状态,即脱离同步块)去wait或notify,会抛出java.lang.IllegalMonitorStateException。也包括在synchronized块中去调用另一个对象的wait/notify,因为不同对象的监视器不同,同样会抛出此异常。
来自国际象棋程序的教训
我们以一个真实的故事来结束本小节,该故事发生在世界级多线程国际象棋对弈程序 Socrates[81] 的开发期间(时间做了简略)。该程序的原型是在一个具有 32 个处理器的计算机上进行的,但是最终运行在一个具有 512 个处理器的超级计算机上。某天,开发人员对程序进行了一项优化,针对一项重要的在 32 个处理器上基准测试( benchmark ),把其运行时间从 T32 = 65 秒减少到 T’32 =40 秒。然而,开发人员使用关于 work和 span 的性能度量得出结论:在 32 个处理器上更快的优化版本,在 512 个处理器上运行时,实际上比原始版本慢一些。结果,他们放弃了“优化”。
他们的分析方法是这样的。程序的原始版本的 work T1 = 2048 秒, span T∞ = 1 秒。如果把不等式 (27.4)看做是等式, TP = T1 /P+T∞ ,并把它当作在 P 个处理器上的近似运行时间,确实可以得出, T32=2048/32+1 = 65 。如果做了优化, work 变成 T’1 =1024 秒, span 变成 T’∞ = 8 秒。采用同样的近似方法,有 T’32 =1024/32 + 8=40 。
但是,当在 512 个处理器上进行计算运行时间时,这两个版本的相对速度会发生转换。此时, T512=2048/512+1 = 5 秒, T’512 =1024/512+8 = 10 秒。在 32 个处理器上能够提速程序的优化版本在 512 个处理器上会使得程序慢 2 倍!优化版本的 span 为 8 ,对于 32 个处理器来说,其不是运行时间中的支配项,但是在 512 个处理器时,就变成了支配项,把更多核的优势化为乌有。
这个故事的寓意在于, work 和 span 是一种好的推断性能的方法,而不是一种好的推断运行时间的方法。