您的位置:首页 > 编程语言 > Java开发

JAVA 多线程 及线程的 一些理解

2011-08-09 15:46 288 查看
对于 多线程的并发 及并发处理:

线程场景



这幅图中节点代表一个 single Thread,边代表执行的步骤。

回页首

模型的描述

如何来描述图 1 中所示的场景呢?可以采用 XML 的格式来描述我们的模型。我定义一个“Thread” element 来表示线程。
<ThreadList>
<Thread ID = "thread-id" PRETHREAD = "prethread1, prethread2…"></Thread>
<Thread ID = "thread-id" PRETHREAD = "prethread3, prethread4…"></Thread>
</ThreadList>

其中 ID 是线程的唯一标识符,PRETHREAD 便是该线程的直接先决线程的ID,每个线程 ID 之间用逗号隔开。

在 Thread 这个 element 里面可以加入你想要该线程执行任务的具体信息。

实际上模型的描述是解决问题非常重要的一个环节,整个线程场景可以用一种一致的形式来描述,作为 Java 多线程并发控制框架引擎的输入。也就是将线程运行的模式用 XML 来描述出来,这样只用改动 XML 配置文件就可以更改整个线程运行的模式,不用改动任何的源代码。

回页首

两种实现机制

对于 Java 多线程的运行框架来说,我们将采用“外”和“内”的两种模式来实现。

回页首

“外” - 主线程轮询

图 2. 静态类图





Thread 是工作线程。ThreadEntry 是 Thread 的包装类,prerequisite 是一个 HashMap,它含有 Thread 的先决线程的状态。如图1中显示的那样,T4 的先决线程是 T2 和 T3,那么 prerequisite 中就包含 T2 和 T3 的状态。TestScenario 中的 threadEntryList 中包含所有的 ThreadEntry。

图 3. 线程执行场景



TestScenario 作为主线程,作为一个“外”在的监控者,不断地轮询 threadEntryList 中所有 ThreadEntry 的状态,当 ThreadEntry 接受到 isReady 的查询后查询自己的 prerequisite,当其中所有的先决线程的状态为“正常结束时”,它便返回 ready,那么 TestScenario 便会调用 ThreadEntry 的 startThread() 方法授权该 ThreadEntry 运行线程,Thread 便通过 run() 方法来真正执行线程。并在正常执行完毕后调用
setPreRequisteState() 方法来更新整个 Scenario,threadEntryList 中所有 ThreadEntry 中 prerequisite 里面含有该 Thread 的状态信息为“正常结束”。

图 4. 状态更改的过程



如图 1 中所示的 T4 的先决线程为 T2 和 T3,T2 和 T3 并行执行。如图 4 所示,假设 T2 先执行完毕,它会调用 setPreRequisteState() 方法来更新整个 Scenario, threadEntryList 中所有 ThreadEntry 中 prerequisite 里面含有该 T2 的状态信息为“正常结束”。此时,T4 的 prerequisite 中 T2 的状态为“正常结束”,但是 T3 还没有执行完毕,所以其状态为“未完毕”。所以 T4 的 isReady 查询返回为
false,T4 不会执行。只有当 T3 执行完毕后更新状态为“正常结束”后,T4 的状态才为 ready,T4 才会开始运行。

其余的节点也以此类推,它们正常执行完毕的时候会在整个的 scenario 中广播该线程正常结束的信息,由主线程不断地轮询各个 ThreadEntry 的状态来开启各个线程。

这便是采用主控线程轮询状态表的方式来控制 Java 多线程运行框架的实现方式之一。

优点:概念结构清晰明了,实现简单。避免采用 Java 的锁机制,减少产生死锁的几率。当发生异常导致其中某些线程不能正常执行完毕的时候,不会产生挂起的线程。

缺点:采用主线程轮询机制,耗费 CPU 时间。当图中的节点太多的(n>??? 而线程单个线程执行时间比较短的时候 t<??? 需要进一步研究)时候会产生线程启动的些微延迟,也就是说实时性能在极端情况下不好,当然这可以另外写一篇文章来专门探讨。

回页首

“内” - wait¬ify

相对于“外”-主线程轮询机制来说,“内”采用的是自我控制连锁触发机制。

图 5. 锁机制的静态类图



Thread 中的 lock 为当前 Thread 的 lock,lockList 是一个 HashMap,持有其后继线程的 lock 的引用,getLock 和 setLock 可以对 lockList 中的 Lock 进行操作。其中很重要的一个成员是 waitForCount,这是一个引用计数。表明当前线程正在等待的先决线程的个数,例如图 1 中所示的 T4,在初始的情况下,他等待的先决线程是 T2 和 T3,那么它的 waitForCount 等于 2。

图 6. 锁机制执行顺序图



当整个过程开始运行的时候,我们将所有的线程 start,但是每个线程所持的 lock 都处于 wait 状态,线程都会处于 waiting 的状态。此时,我们将 root thread 所持有的自身的 lock notify,这样 root thread 就会运行起来。当 root 的 run 方法执行完毕以后。它会检查其后续线程的 waitForCount,并将其值减一。然后再次检查 waitForCount,如果 waitForCount 等于 0,表示该后续线程的所有先决线程都已经执行完毕,此时我们 notify
该线程的 lock,该后续线程便可以从 waiting 的状态转换成为 running 的状态。然后这个过程连锁递归的进行下去,整个过程便会执行完毕。

我们还是以 T2,T3,T4 为例,当进行 initThreadLock 过程的时候,我们可以知道 T4 有两个直接先决线程 T2 和 T3,所以 T4 的 waitForCount 等于 2。我们假设 T3 先执行完毕,T2 仍然在 running 的状态,此时他会首先遍历其所有的直接后继线程,并将他们的 waitForCount 减去 1,此时他只有一个直接后继线程 T4,于是 T4 的 waitForCount 减去 1 以后值变为 1,不等于 0,此时不会将 T4 的 lock notify,T4 继续
waiting。当 T2 执行完毕之后,他会执行与 T3 相同的步骤,此时 T4 的 waitForCount 等于 0,T2 便 notify T4 的 lock,于是 T4 从 waiting 状态转换成为 running 状态。其他的节点也是相似的情况。

当然,我们也可以将整个过程的信息放在另外的一个全局对象中,所有的线程都去查找该全局对象来获取各自所需的信息,而不是采取这种分布式存储的方式。

优点:采用 wait¬ify 机制而不采用轮询的机制,不会浪费CPU资源。执行效率较高。而且相对于“外”-主线程轮询的机制来说实时性更好。

缺点:采用 Java 线程 Object 的锁机制,实现起来较为复杂。而且采取一种连锁触发的方式,如果其中某些线程异常,会导致所有其后继线程的挂起而造成整个 scenario 的运行失败。为了防止这种情况的发生,我们还必须建立一套线程监控的机制来确保其正常运


线程的调度

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定应调度哪些线程来执行。高优先级的线程会中断低优先级线程的执行。

在抢先式调度下,又分为时间片方式和非时间片方式,又叫独占方式。
在时间片方式下,当前活动的线程执行完时间片后,如果有其它同优先级的线程,则当前活动线程转入等待执行队列,等待下一个时间片的调度。
在非时间片方式下:当且活动线程一旦获得执行权,将一直执行下去,直到执行完毕或由于某种原因主动放弃CPU,或者有高优先级的线程处于就绪状态。
在windows2000下的调度方式是抢先式和时间片方式相结合。
调度的核心问题就是,什么时候,哪一个线程占有CPU,什么时候放弃CPU。


一、放弃CPU的方式

在下面的情况下,当前线程会放弃CPU。
1、线程调用yield()或者sleep()方法主动放弃。
sleep()方法调用后,所有的其它线程都有执行的机会,包括低优先级的线程。
yield()方法,只给同优先级的线程执行的机会,如果没有同优先级的线程,则该方法不产生任何效果。
同学们要特别注意这两个方法的差别。
2、由于当前线程进行I/O访问、外存读写、等待用户输入等操作,导致线程阻塞。
3、抢先式系统下,有高优先级的线程参与调度;时间片方式下,当前时间片用完,有同优先级的线程参与调度。
Java中,线程的优先级用数字来表示,范围从1到10;1最低用常量Thread.MIN_PRIORITY表示,10最高,用Thread.MAX_PRIORITY表示。一个线程缺省优先级是5,用常量Thread.NORM_PRIORITY表示。
可以用Thread类中的getPriority()获取线程的优先级,用setPrioriy()设置线程的优先级。

产生这种问题的原因在于对共享数据(临界资源)访问的操作的不完整性、非原子性。

解决方法有:互斥锁,
在Java中,每个对象都对应于一个可以称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

Java中提供了关键字synchronized来与对象的互斥锁联系。

如果synchronized用在类声明中,则表明该类中的所有方法都是synchronized的。

 在多线程的同步中使用了wait()和notify()方法来同步线程的执行,对这些方法的说明如下:

1、Wait,notify ,notifyAll必须在已经持有锁的情况下执行,所以它们只能出现在synchronized作用的范围内。

2、wait的作用:释放已持有的锁,当前线程进入wait队列。

3、notify 的作用:唤醒wait队列中的第一个线程并把它移入锁申请队列。

4、notifyAll的作用:唤醒wait队列中的所有的线程并把它们移入锁申请队列。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: