java多线程

来源:互联网 发布:java的框架 编辑:程序博客网 时间:2024/05/01 14:44

java多线程

何时使用多线程技术,以及何时避免用它,这是我们需要掌握的重要课题。骼它的主要目的是对大量任务进行有序的管理。通过多个任务的混合使用,可以更有效地利用计算机资源,或者对用户来说显得更方便。资源均衡的经典问题是在 IO 等候期间如何利用 CPU。至于用户方面的方便性,最经典的问题就是如何在一个长时间的下载过程中监视并灵敏地反应一个“停止”( stop )按钮的下。
多线程的主要缺点包括:
(1) 等候使用共享资源时造成程序的运行速度变慢。
(2) 对线程进行管理要求的额外 CPU 开销。
(3) 复杂程度无意义的加大,比如用独立的线程来更新数组内每个元素的愚蠢主意。
(4) 漫长的等待、浪费精力的资源竞争以及死锁等多线程症状。
线程另一个优点是它们用“轻度”执行切换( 100 条指令的顺序)取代了“重度”进程场景切换( 1000 条指令)。由于一个进程内的所有线程共享相同的内存空间,所以“轻度”场景切换只改变程序的执行和本地变量。而在“重度”场景切换时,一个进程的改变要求必须完整地交换内存空间。
不管在什么情况下,涉及线程的程序设计:
(1) 刚开始会让人摸不着头脑,要求改换我们传统的编程思路;
(2) 其他语言对线程的支持看来是类似的。所以一旦掌握了线程的概念,在其他环境也不会有太大的困难。
尽管对线程的支持使 Java 语言的复杂程度多少有些增加,但请不要责怪 Java。毕竟,利用线程可以做许多有益的事情。
多个线程可能共享同一个资源(比如一个对象里的内存),这是运用线程时面临的最大的一个麻烦。必须保证多个线程不会同时试图读取和修改那个资源。这要求技巧性地运用 synchronized(同步)关键字。它是一个有用的工具,但必须真正掌握它,因为假若操作不当,极易出现死锁。

线程

利用对象,可将一个程序分割成相互独立的区域。我们通常也需要将一个程序转换成多个独立运行的子任务。象这样的每个子任务都叫作一个“线程”( Thread)。
“进程”是指一种“自包容”的运行程序,有自己的地址空间。“多任务”操作系统能同时运行多个进程(程序) —— 但实际是由于 CPU 分时机制的作用,使每个进程都能循环获得自己的 CPU 时间片。但由于轮换速度非常快,使得所有程序好象是在“同时”运行一样。“线程”是进程内部单一的一个顺序控制流。因此,一个进程可能容纳了多个同时执行的线程。
线程模型”(以及Java 中的编程支持)是一种程序编写规范,可在单独一个程序里实现几个操作的同时进行。根据这一机制, CPU 可为每个线程都分配自己的一部分时间。每个线程都“感觉”自己好象拥有整个 CPU,但 CPU 的计算时间实际却是在所有线程间分摊的。
线程机制多少降低了一些计算效率,但无论程序的设计,资源的均衡,还是用户操作的方便性,都从中获得了巨大的利益。综合考虑,这一机制是非常有价值的。当然,如果本来就安装了多块 CPU,那么操作系统能够自行决定为不同的 CPU 分配哪些线程,程序的总体运行速度也会变得更快(所有这些都要求操作系统以及应用程序的支持)。多线程和多任务是充分发挥多处理机系统能力的一种最有效的方式。

从线程继承

为创建一个线程,最简单的方法就是从 Thread 类继承。这个类包含了创建和运行线程所需的一切东西。Thread 最重要的方法是 run()。但为了使用 run(),必须对其进行过载或者覆盖,使其能充分按自己的吩咐行事。因此, run()属于那些会与程序中的其他线程“并发”或“同时”执行的代码。run()方法几乎肯定含有某种形式的循环—— 它们会一直持续到线程不再需要为止。因此,我们必须规定特定的条件,以便中断并退出这个循环(或者在上述的例子中,简单地从 run()返回即可)。 run()通常采用一种无限循环的形式。也就是说,通过阻止外部发出对线程的 stop()或者 destroy()调用,它会永远运行下去(直到程序完成)。
在 main()中,可看到创建并运行了大量线程。 Thread 包含了一个特殊的方法,叫作 start(),它的作用是对线程进行特殊的初始化,然后调用 run()。所以整个步骤包括:调用构建器来构建对象,然后用 start()配置线程,再调用 run()。如果不调用 start()—— 如果适当的话,可在构建器那样做—— 线程便永远不会启动。
线程并不是按它们创建时的顺序运行的。事实上, CPU 处理一个现有线程集的顺序是不确定的——除非我们亲自介入,并用 Thread 的 setPriority()方法调整它们的优先级。
main()创建 Thread 对象时,它并未捕获任何一个对象的句柄。普通对象对于垃圾收集来说是一种“公平竞赛”,但线程却并非如此。若某样东西有一个 Runnable 接口,实际只是意味着它有一个 run()方法,但不存在与之相关的任何特殊东西—— 它不具有任何天生的线程处理能力,这与那些从Thread 继承的类是不同的。所以为了从一个Runnable 对象产生线程,必须单独创建一个线程,并为其传递Runnable 对象;可为其使用一个特殊的构建器,并令其采用一个 Runnable 作为自己的参数使用。随后便可为那个线程调用 start(),如下所示:
selfThread.start();
它的作用是执行常规初始化操作,然后调用 run()。
Runnable 接口最大的一个优点是所有东西都从属于相同的类。若需访问什么东西,只需简单地访问它即可,不需要涉及一个独立的对象。但为这种便利也是要付出代价的—— 只可为那个特定的对象运行单独一个线程(尽管可创建那种类型的多个对象,或者在不同的类里创建其他对象)。注意 Runnable 接口本身并不是造成这一限制的罪魁祸首。它是由于 Runnable 与我们的主类合并造成的,因为每个应用只能主类的一个对象。

Daemon线程

“ Daemon”线程的作用是在程序的运行期间于后台提供一种“常规”服务,但它并不属于程序的一个基本部分。因此,一旦所有非 Daemon 线程完成,程序也会中止运行。相反,假若有任何非 Daemon 线程仍在运行(比如还有一个正在运行 main()的线程),则程序的运行不会中止。
通过调用 isDaemon(),可调查一个线程是不是一个 Daemon,而且能用 setDaemon()打开或者关闭一个线程的Daemon 状态。如果是一个 Daemon 线程,那么它创建的任何线程也会自动具备 Daemon 属性。有的时候,我们并不介意一个资源在尝试使用它的时候是否正被访问。但为了让多线程机制能够正常运转,需要采取一些措施来防止两个线程访问相同的资源—— 至少在关键的时期。为防止出现这样的冲突,只需在线程使用一个资源时为其加锁即可。访问资源的第一个线程会其加上锁以后,其他线程便不能再使用那个资源,除非被解锁。

堵塞

一个线程可以有四种状态:
(1) 新( New):线程对象已经创建,但尚未启动,所以不可运行。
(2) 可运行( Runnable ):意味着一旦时间分片机制有空闲的 CPU 周期提供给一个线程,那个线程便可立即开始运行。因此,线程可能在、也可能不在运行当中,但一旦条件许可,没有什么能阻止它的运行——它既没有“死”掉,也未被“堵塞”。
(3) 死(Dead):从自己的 run()方法中返回后,一个线程便已“死”掉。亦可调用 stop()令其死掉,但会产生一个违例——属于 Error 的一个子类(也就是说,我们通常不捕获它)。记住一个违例的“掷”出应当是一个特殊事件,而不是正常程序运行的一部分。所以不建议你使用 stop()(在 Java 1.2 则是坚决反对)。另外还有一个 destroy()方法(它永远不会实现),应该尽可能地避免调用它,因为它非常武断,根本不会解除对象的锁定。
(4) 堵塞( Blocked):线程可以运行,但有某种东西阻碍了它。若线程处于堵塞状态,调度机制可以简单地跳过它,不给它分配任何 CPU 时间。除非线程再次进入“可运行”状态,否则不会采取任何操作。

堵塞原因

线程被堵塞可能是由下述五方面的原因造成的:
(1) 调用 sleep(毫秒数),使线程进入“睡眠”状态。在规定的时间内,这个线程是不会运行的。
(2) 用 suspend()暂停了线程的执行。除非线程收到 resume()消息,否则不会返回“可运行”状态。
(3) 用 wait()暂停了线程的执行。除非线程收到 nofify()或者 notifyAll()消息,否则不会变成“可运行”(是的,这看起来同原因 2 非常相象,但有一个明显的区别是我们马上要揭示的)。
(4) 线程正在等候一些 IO(输入输出)操作完成。
(5) 线程试图调用另一个对象的“同步”方法,但那个对象处于锁定状态,暂时无法使用

死锁

由于线程可能进入堵塞状态,而且由于对象可能拥有“同步”方法—— 除非同步锁定被解除,否则线程不能访问那个对象—— 所以一个线程完全可能等候另一个对象,而另一个对象又在等候下一个对象,以此类推。这个“等候”链最可怕的情形就是进入封闭状态—— 最后那个对象等候的是第一个对象!此时,所有线程都会陷入无休止的相互等待状态,我们将这种情况称为“死锁”。

优先级

线程的优先级( Priority)告诉调试程序该线程的重要程度有多大。如果有大量线程都被堵塞,都在等候运行,调试程序会首先运行具有最高优先级的那个线程。然而,这并不表示优先级较低的线程不会运行(换言之,不会因为存在优先级而导致死锁)。若线程的优先级较低,只不过表示它被准许运行的机会小一些而已。

线程组

所有线程都隶属于一个线程组。那可以是一个默认线程组,亦可是一个创建线程时明确指定的组。在创建之初,线程被限制到一个组里,而且不能改变到一个不同的组。每个应用都至少有一个线程从属于系统线程组。若创建多个线程而不指定一个组,它们就会自动归属于系统线程组。
线程组也必须从属于其他线程组。必须在构建器里指定新线程组从属于哪个线程组。若在创建一个线程组的时候没有指定它的归属,则同样会自动成为系统线程组的一名属下。因此,一个应用程序中的所有线程组最终都会将系统线程组作为自己的“父”。
如果在一个多线程的程序中遇到了性能上的问题,那么现在有许多因素需要检查:
(1) 对 sleep, yield()以及/或者 wait()的调用足够多吗?
(2) sleep()的调用时间足够长吗?
(3) 运行的线程数是不是太多?
(4) 试过不同的平台和 JVM 吗?
1 0
原创粉丝点击