一. 理解多线程:
(1)多线程是这样一种机制,它允许在程序中并发执行多个指令流,每个指令流都称为一个线程,彼此间互相独立。
(2)线程又称为轻量级进程,它和进程一样拥有独立的执行控制权,由操作系统负责调度,区别在于线程没有独立的存储空间,而是和所属进程
中的其它线程共享一个存储空间,这使得线程间的通信较进程简单。
(3)多个线程的执行是并发的,也就是逻辑上“同时”,不管是否是物理上的“同时”。如果系统只有一个CPU,那么真正的“同时”是不可能
的,但是由于CPU的速度非常快,用户感觉不到其中的区别,因此我们也不用关心它,只需要设想各个线程是同时执行即可。
(4)多线程和传统的单线程在程序设计上最大的区别在于,由于各个线程的控制流彼此独立,使得各个线程之间的代码是乱序执行的,由此带来
的线程调度,同步等问题。
二. Java中实现多线程:
(1)为了创建一个新的线程,我们需要做些什么?很显然,我们必须指明这个线程所要执行的代码,而这就是在Java中实现多线程我们所需要做
的一切!
(2)真是神奇!Java是如何做到这一点的?通过类!作为一个完全面向对象的语言,Java提供了类 java.lang.Thread 来方便多线程编程,这
个类提供了大量的方法来方便我们控制自己的各个线程,我们以后的讨论都将围绕这个类进行。
(3)那么如何提供给 Java 我们要线程执行的代码呢?让我们来看一看 Thread 类。Thread 类最重要的方法是 run() ,它为Thread 类的方
法 start() 所调用,提供我们的线程所要执行的代码。为了指定我们自己的代码,只需要覆盖它!
方法一:继承 Thread 类,覆盖方法 run():
方法二:实现 Runnable 接口:
三. 线程的四种状态:
(1)新状态:线程已被创建但尚未执行(start() 尚未被调用)。
(2)可执行状态:线程可以执行,虽然不一定正在执行。CPU 时间随时可能被分配给该线程,从而使得它执行。
(3)死亡状态:正常情况下 run() 返回使得线程死亡。调用 stop()或 destroy() 亦有同样效果,但是不被推荐,前者会产生异常,后者是强
制终止,不会释放锁。
(4)阻塞状态:线程不会被分配 CPU 时间,无法执行。
四. 线程的优先级:
线程的优先级代表该线程的重要程度,当有多个线程同时处于可执行状态并等待获得 CPU 时间时,线程调度系统根据各个线程的优先级来决定
给谁分配 CPU 时间,优先级高的线程有更大的机会获得 CPU 时间,优先级低的线程也不是没有机会,只是机会要小一些罢了。你可以调用
Thread 类的方法 getPriority() 和 setPriority()来存取线程的优先级,线程的优先级界于1(MIN_PRIORITY)和10(MAX_PRIORITY)之间,
缺省是5(NORM_PRIORITY)。
五. 线程的同步:
由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门机制以解决这种冲
突,有效避免了同一个数据被多个线程同时访问。这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法和
synchronized 代码块。
(1)synchronized 方法:通过在方法声明中加入 synchronized关键字,如:public synchronized void accessVal(int newVal);
synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法所属类实例的锁
才能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后,被阻塞的线程方能获得该锁,重新进
入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态
(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明
为 synchronized)。
在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成
员变量的访问。
synchronized 方法的缺陷:若将一个方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为
synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然
我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java
为我们提供了更好的解决办法,那就是 synchronized块。
(2)synchronized代码块:通过 synchronized 关键字来声明synchronized代码块。
synchronized(syncObject) {
//允许访问控制的代码
}
synchronized 代码块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁才能执行,具体
机制同前所述。
由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。
六. 线程的阻塞:
为了解决对共享存储区的访问冲突,Java 引入了同步机制,现在让我们来考察多个线程对共享资源的访问,显然同步机制已经不够了,因为在任
意时刻所要求的资源不一定已经准备好了被访问,反过来,同一时刻准备好了的资源也可能不止一个。为了解决这种情况下的访问控制问题,
Java 引入了阻塞机制。 阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操作系统的同学对它一定已经很熟悉了。
Java 提供了大量方法来支持阻塞。
(1)sleep() 方法:sleep() 允许指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定
的时间一过,线程重新进入可执行状态。典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间
后重新测试,直到条件满足为止。
(2)suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被
调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还
没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
(3)yield() 方法:yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。
调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。
(4)wait() 和 notify() 方法:两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单位的一段时间作为
参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调
用。
sleep() 方法和suspend() 方法阻塞时都不会释放占用的锁,当睡眠结束或调用resume() 方法该线程继续执行。
wait() 方法导致线程阻塞,并且该对象上的锁被释放。当调用notify() 方法解除阻塞。
关于 wait() 和 notify() 方法的两点说明:
第一:调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选
择,所以编程时要特别小心,避免因这种不确定性而产生问题。
第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法
而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
七. 守护线程:
守护线程是一类特殊的线程,它和普通线程的区别在于它并不是应用程序的核心部分,当一个应用程序的所有非守护线程终止运行时,即使仍然有
守护线程在运行,应用程序也将终止,反之,只要有一个非守护线程在运行,应用程序就不会终止。守护线程一般被用于在后台为其它线程提供服
务。可以通过调用方法 isDaemon() 来判断一个线程是否是守护线程,也可以调用方法 setDaemon() 来将一个线程设为守护线程。
八. 线程组:
线程组是一个 Java 特有的概念,在 Java 中,线程组是类ThreadGroup 的对象,每个线程都隶属于唯一一个线程组,这个线程组在线程创建时
指定并在线程的整个生命期内都不能更改。你可以通过调用包含 ThreadGroup 类型参数的 Thread 类构造函数来指定线程属的线程组,若没有
指定,则线程缺省地隶属于名为 system 的系统线程组。
我们可以通过调用线程组的相应方法来设置其中所有线程的优先级,也可以启动或阻塞其中的所有线程。线程组机制也允许我们通过分组来区分有
不同安全特性的线程,对不同组的线程进行不同的处理,还可以通过线程组的分层结构来支持不对等安全措施的采用。Java 的 ThreadGroup 类
提供了大量的方法来方便我们对线程组树中的每一个线程组以及线程组中的每一个线程进行操作。
九. 总结:
我们一起学习了 Java 多线程编程的方方面面,包括创建线程,以及对多个线程进行调度、管理。我们深刻认识到了多线程编程的复杂性,以及线
程切换开销带来的多线程程序的低效性,这也促使我们认真地思考一个问题:我们是否需要多线程?何时需要多线程?
假如我们的程序根本不要求多个代码块并发执行,那自然不需要使用多线程;假如我们的程序虽然要求多个代码块并发执行,但是却不要求乱序,
则我们完全可以用一个循环来简单高效地实现,也不需要使用多线程;只有当它完全符合多线程的特点时,多线程机制对线程间通信和线程管理的
强大支持才能有用武之地,这时使用多线程才值得。