内容来自于《Java语言程序设计》
线程是指一个任务从头至尾的执行流。线程提供了运行一个任务的机制。对于java而言,可以在一个程序中并发地启动多个线程。这些线程可以在多处理器系统上同时运行。
在单处理器系统中,多个线程共享CPU时间成为时间共享(time sharing),而操作系统负责调度及分配资源给它们。这种安排是切实可行的,因为CPU的大部分时间都是空闲的,例如,在等待用户输入数据时,CPU什么都不做。
多线程可以使程序反应更快,交互性更强、执行效率更高。例如一个好的文字处理程序允许在输入文字的同时,打印或保存文件。在一些情况下,即使在单处理器系统上,多线程程序的运行速度也比单线程程序更快。Java对多线程程序的创建和运行,以及锁定资源以避免冲突提供了非常好的支持。
当程序作为一个应用程序运行时,java解释器为main方法启动一个线程。当程序作为一个applet运行时,web浏览器启动一个线程来运行applet,还可以在程序中创建附加的线程以执行并发任务。在java中,每个任务都是runnable接口的一个实例,也成为可运行对象。线程本质上讲就是便于任务执行的对象。
任务就是对象,为了创建任务,必须首先为任务定义一个类。任务类必须实现runnable接口。Runnable接口非常简单,它只包含一个run方法。需要实现这个方法老告诉系统线程将如何运行。
将任务从线程中分离出来是比较好的设计。
线程的sleep方法可能抛出一个InterruptedException,这是一个必检异常。如果在一个循环中调用了sleep方法,那就应该将这个循环放在try-catch块中。
可以使用join()方法等待另一个线程的结束。
Java给每个线程指定了一个优先级。默认情况下,线程继承生成它的线程的优先级。可以使用setPriority方法提高或降低线程的优先级。默认情况下,线程继承生成它的线程的优先级。优先级是从1到10的数字。Thread类有int型常量MIN_PRIORITY、NORM_PRIORITY、MAX_ PRIORITY,分别代表了1,5和10。主线程的优先级是Thread.NOR_PRIORITY。
Java虚拟机总是选择当前优先级最高的可运行线程。较低优先级的线程只有在没有比它更高级的优先级的线程运行时才能运行。如果所有可运行线程具有相同的优先级,那将会使用循环队列给它们分配相同的CPU份额。这被成为循环调度。
如果总有一个优先级较高的线程在运行,或者有一个相同优先级的线程不退出,那么这个线程可能永远没有运行的机会。这种情况成为资源竞争或者缺乏状态。为避免竞争现象,高优先级的线程必须定时调用sleep或者yield方法,来给低优先级或相同优先级的线程一个运行的机会。
可以使用一个定时器或者一个线程来控制动画,那个更好些呢?定时器是一个源组件,以“固定的速率”触发一个ActionEvent事件。当发生动作事件时,定时器会调用监听器的actionPerformed方法来处理这个事件。定时器和事件处理运行在同一事件分发线程上。如果处理这个事件需要花费很长时间,那么两个时间之间的真正的延时时间比请求的延时时间还长。在这种情况下,应该在一个独立的线程上运行事件处理。下一节将会举例演示这个问题,并通过在独立的线程上运行时间处理机制来解决它。通常线程比定时器更加可靠,响应速度更快。如果需要精确的延时时间或者快速响应,最好使用线程。否则,使用定时器会比线程更加简单,也更加有效。因为定时器运行在GUI的事件分发线程上。所以,定时器比线程占用的系统资源少,故而不需要为定时器创建新线程。
GUI事件处理和绘图代码在一个被称为事件分发线程的特殊线程中执行。由于大多数的Swing方法都不是线程安全的,因此这是很有必要的,从多线程中调用它们可能导致冲突。
在某些情况下,需要在事件分发线程中运行代码,以避免可能的冲突。可以使用javax.swing.SwingUtilities类中的静态方法invokeLater和invokeAndWait来运行事件分发线程的代码。必须将这些代码放到Runnabel对象的run方法中,并将Runnable对象指定为invokeLater和invokeAndWait的参数。invokeLater方法立即返回,无需等待时间分发线程执行代码。invokeAndWait方法和invokeLater方法很类似,不同的是,invokeAndWait在事件分发线程执行特定代码前是不会返回的。
到目前为止,创建一个框架并且使之可见就能从主方法启动GUI应用程序。这对大多数应用程序都是适用的。然而,如果启动一个GUI应用程序消耗了很长的时间,那就可能会发生问题。为了编码这种情况中可能的问题,应该从事件分发线程中开始GUI的创建。
之前的创先方法对单一任务的执行时很方便的,但是由于必须为每个任务创建一个线程,因此对大量的任务而言是不够高效的。为每个人物开始一个新线程可能会限制流量并且造成性能降低。线程池是管理并发执行任务个数的理想方法。Java提供Executor接口来执行线程池中的任务,提供ExectorService接口来管理和控制任务。ExecutorService是Executor的子接口。
为了创建一个Executor对象,可以使用Executors类中的静态方法。使用newFixedThreadPool(int)方法在池中创建固定数目的线程。如果线程完成了任务的执行,它可以被重新使用以执行另外一个任务。如果线程池中所有的线程都不是出于空闲状态,而且有任务在等待执行,那么在关机之前,如果由于一个错误终止了一个线程,就会创建一个新的线程来替代它,如果线程池中所有的线程都不是出于空闲状态,而且有任务在执行,那么newCachedThreadPool()方法就会创建一个新线程。如果缓冲池中的线程在60秒内都没有被使用,就该终止它。对多个小任务而言,一个缓冲池已经足够。方法shutdown()通知执行器关闭,不能接受新的任务,但是现有的任务将继续执行直至完成。
任务1和任务2以一种会引起冲突的方式访问一个公共资源。这是多线程程序中的一个普遍问题,成为竞争状态。如果一个类的对象在多线程程序中没有导致竞争状态,则称这样的类为线程安全的。
Synchronized关键字,为了避免竞争状态,应该防止多个线程同时进入程序的某一特定部分,程序中的这部分成为临界区。一种方法通过给对应方法加上synchronized关键字,以便一次只有一个线程可以访问这个方法。另一种方法是在执行之前加锁。对于实例方法,要给调用该方法的对象加锁。对于静态方法,要给这个类加锁。如果一个线程调用一个对象上的同步实例方法(静态方法),首先给该对象加锁,然后执行该方法,最后解锁。在解锁之前,另一个调用那个对象(类)的方法中的线程将被阻塞,直到解锁。
调用一个对象的同步实例方法要求给该对象加锁。调用一个类的同步静态方法要求对该类加锁。当执行方法中的某一个代码块时,同步语句不仅可用于this对象加锁,而且可用于对任何对象加锁。这个代码块称为同步块。
Synchronized(expr){
Statements;
}
表达式expr必须求出对象的引用。如果对象已经被另一个线程锁定,则在解锁之前,该线程将被阻塞。当获准对一个对象加锁时,该线程执行同步块中的语句,然后解除给对象加的锁。
同步语句允许设置同步方法中的部分代码,而不必是整个方法。这大大增强了程序的并发能力。
任何同步的实例方法都可以转换为同步于巨。
利用加锁同步:
同步的实例方法在执行方法之前都隐式地需要一个锁。
Java可以显式地加锁,这给协调线程带来了更多的控制功能。一个锁是一个Lock接口的实例,它定义了加锁和释放锁的方法。锁也可以使用newCondition()方法来创建任意个数的condition对象,用来进行线程通信。
ReentrantLock是为创建相互排斥的锁的Lock的具体实现,可以创建具有特定的公平策略的所。真正的公平策略确保等待时间最长的线程首先获得锁。假的公平策略将锁给任意一个在等待的线程。被多个线程访问的使用公正锁的程序,其整体性能可能比那些使用默认设置的程序差。但是在获取锁且避免资源缺乏时变化很小。
通常使用synchronized方法或语句比使用相互排斥的显示锁简单些。然而,使用显式锁对同步具有状态的线程更加直观和灵活。
线程间协作:
通过保证在临界区上多个线程的相互排斥,线程同步完全可以避免竞争状态的发生,但是有时候,还需要通过线程之间的相互协作。使用条件便于线程间通信。一个线程可以指定在某种条件下该做什么。条件是通过调用Lock对象的newCondition()方法而创建的对象。一旦创建了条件,就可以使用await()、signal()和signalAll()方法来实现线程之间的相互通信。Await()方法可以让当前线程都出于等待状态,直到条件发生。Signal()方法唤醒一个等待的线程,而signalAll()唤醒所有等待的线程。
锁和条件是java 5 中的新内容。在java5 之前,线程通信是使用对象的内置监视器编程实现的。锁和条件与内置监视器相比是非常强大且灵活的。
监视器是一个相互排斥且具备同步能力的对象。监视器中的一个时间点上,只能有一个线程执行一个方法。线程通过获取监视器上的锁进入监视器,并且通过释放锁退出监视器。任意对象都可能是一个监视器。一旦线程锁住对象,该对象就成为监视器。加锁时通过在方法或块上使用synchronized关键字来实现的。在执行同步方法或块之前,线程必须获取锁。如果条件不适合线程继续在监视器内执行,线程可能在监视器中等待。可以对监视器对象调用wait()方法来释放锁。这样其他的一些监视器中的线程就可以获取它,也就有可能改变监视器的状态。当条件合适时,另一线程可以调用notify()或notifyAll()方法来通知一个或所有的等待线程重新获取锁并且恢复执行。
考虑典型的消费者/生产者的例子,假设使用缓冲区存储整数。缓冲区的大小是受限的。缓冲区提供write(int)方法将一个int值添加到缓冲区中。还提供方法read()从缓冲区中读取和删除一个int值。为了同步这个操作,使用具有两个条件的锁:notEmpty和notFull。
阻塞队列:
阻塞队列在试图向一个满队列添加元素或者从空队列中删除元素时会导致线程阻塞。BlockingQueue接口扩展java.util.Queue,并且提供同步的put和take方法向队列头添加元素,以及从队列尾删除元素。Java支持的三个具体的阻塞队列ArrayBlockingQueue、LinkedBlockingQueue和PriorityBlockingQueue。他们都在java.util.concurrent包中。ArrayBlockingQueue使用数组实现阻塞队列。必须指定一个容量或者可选的公平性来构造ArrayBlockingQueue。LinkedBlockingQueue使用链表实现阻塞队列。可以创建不受限的或受限的LinkedBlockingQueue。PriorityBlockingQueue是优先队列,可以创建不受限的或受限的优先队列。
可以创建不受限的LinkedBlockingQueue或PriorityBlockingQueue。对于不受限队列而言,put方法永远不会阻塞。
信号量:
信号量可以用来限制访问共享资源的线程数。在访问资源之前,线程必须从信号量获取许可。在访问完资源之后,这个线程必须将许可返回给信号量。
为了创建信号量,必须使用可选择的公平策略来确定许可的数量。任务通过调用信号量的acquire()方法来获得许可,通过调用信号量的release()方法来释放许可。一旦获得许可,信号量中可用许可的总数减1。一旦许可被释放,信号量中可用许可的总数加1。只有一个许可的信号量可以用来模拟一个相互排斥的锁。
避免死锁。有时两个或多个线程需要在几个共享对象上获取锁。这可能会导致死锁,也就是说,每个线程已经锁定一个对象,而且正在等待锁定另一个对象。使用一种名为资源排序的简单技术可以轻易避免死锁的发生。该技术是给每一个需要锁的对象指定一个顺序,确保每个线程都按照这个顺序来获取锁。
线程的状态,任务在线程中执行,线程可以是一下五种状态之一:新建、就绪、运行、阻塞或结束。
新创建一个线程时,她就进入新建状态,调用线程的start()方法启动线程后,它进入就绪状态。就绪线程是可运行的,但可能还没有开始运行。操作系统必须为他分配CPU时间。
就绪线程开始运行时,它就会进入运行状态,如果给定的CPU时间用完或者调用线程的yield()方法,出于运行状态的线程可能就会进去就绪状态。
有几种原因可能使线程进入阻塞状态(即非活动状态)。可能是它资质调用了join()、sleep()或wait()方法,也可能是其他线程调用了这些方法,它可能是在等到I/O操作的完成,当阻塞行为不起阻塞作用时,阻塞线程可能被激活。例如,如果线程处于休眠状态并且休眠时间已满,线程就会被重新激活并进入就绪状态。
最后,如果一个线程执行完它的run()方法,这个线程就被结束。
isAlive()方法是用来判断线程状态的方法,如果线程处于就绪、阻塞或运行状态,则返回true。如果线程处于新建并且没有启动状态,或者已经结束,则返回false。
方法interrupt()按下列方式中断一个线程:当线程处于就绪状态或运行状态时,给它设置一个中断标志,当线程处于阻塞状态时,它将被唤醒并进入就绪状态,同时抛出异常java.lang.InterruptedException。
同步集合:
java集合框架中的类不是线程安全的,也就是说,如果它们同时被多个线程访问和更新,他们的内容可能被破坏。可以通过锁定集合或者同步集合保护集合中的数据。
Collections类提供六个静态方法将集合转成同步版本。始终这些方法创建的集合称为同步包装类。
调用synchronizedCollection会返回一个新的Collection对象,在它里面所有访问和更新原来的结合的方法都被同步。这些方法使用synchronized关键字来实现。同步集合可以很安全地被多个线程并发的访问和修改。
这些同步包装类都是线程安全的,但是迭代器具有快速失败的特性。这就意味着当下层集合被另一个线程修改时,如果整个集合使用一个迭代器,那么迭代器会通过抛出异常而结束。
所有的Swing GUI事件在一个事件分发线程中被处理。如果一个事件需要很长时间的处理,线程就不能估计队列中的其他任务。为了解决这个问题,可以运行费时间的任务来处理单线程中的事件。Java 6中引入了SwingWorker,这是一个实现Runnable的抽象类。可以定义一个任务类来扩展SwingWorker,使用任务产生的结果来运行费时的任务并且更新GUI。