Java语言程序设计—多线程
已经介绍了GUI(图形用户界面)( Java语言程序设计—GUI(图形用户界面) ),前面章节讲到的都是单线程编程,单线程的程序如同现在生活中只雇一名员工的工厂,这名员工必须做完一件事情后才可以做下一件事,多线程的程序则如同雇佣多名员工的工厂,他们可以同时分别做多件事情,Java语言提供了非常优秀的多线程支持,程序可以通过非常简单的方式来启动多线程。将对多线程的相关知识进行详细讲解。
线程概述
Ø 进程
进程是程序的一次动态执行过程,它需要经历从代码加载、代码执行到执行完毕的一个完整过程,这个过程也是进程本身从产生、发展到最终消亡的过程。每个运行中的程序就是一个进程,一般而言,进程在系统中独立存在,拥有自己独立的资源,多个进程可以在单个处理器上并发执行且互不影响。例如,打开计算机中的杀毒软件,可以在Windows任务管理器中查看该进程,如图所示。
进程是程序的一次动态执行过程,它需要经历从代码加载、代码执行到执行完毕的一个完整过程,这个过程也是进程本身从产生、发展到最终消亡的过程。每个运行中的程序就是一个进程,一般而言,进程在系统中独立存在,拥有自己独立的资源,多个进程可以在单个处理器上并发执行且互不影响。例如,打开计算机中的杀毒软件,可以在Windows任务管理器中查看该进程,如图所示。
图中,Windows任务管理器的“进程”选项卡中,可以查看到刚打开的杀毒软件,将软件正常关闭或者右键结束进程,都可以使这个进程消亡。
Ø 线程
操作系统可以同时执行多个任务,任务就是线程,进程可以同时执行多个任务,其中每个任务就是线程。例如,前面讲解的杀毒软件程序是一个进程,那么它在为计算机体检的同时可以清理垃圾文件,这就是两个线程同时运行。在Windows任务管理器中也可以查看当前系统的线程数,如图所示。
在图中,Windows任务管理器的“性能”选项卡中,可以查看到当前系统的总进程数和总线程数,可以看出线程数远远多于进程数。另外,多个线程并发执行时相互独立、互不影响。
线程的创建
在Java中,类仅支持单继承,也就是说,当定义一个新的类时,它只能扩展一个外部类。如果创建自定义线程类的时候是通过扩展 Thread类的方法来实现的,那么这个自定义类就不能再去扩展其他的类,也就无法实现更加复杂的功能。因此,如果自定义类必须扩展其他的类,那么就可以使用实现Runnable接口的方法来定义该类为线程类,这样就可以避免Java单继承所带来的局限性。Java提供了三种创建线程的方式,下面分别进行详细的讲解。
Ø 继承Thread类创建线程
Java提供了Thread类代表线程,它位于java.lang包中,下面介绍Thread类创建并启动多线程的步骤,具体步骤如下。
(1)定义Thread类的子类,并重写run()方法,run()方法称为线程执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法启动线程。
启动一个新线程时,需要创建一个Thread类实例,接下来了解一下Thread类的常用构造方法。如表所示。
表中列出了Thread类的常用构造方法,这些构造方法可以创建线程实例,线程真正的功能代码在类的run()方法中。当一个类继承Thread类后,可以在类中覆盖父类的run()方法,在方法内写入功能代码。另外,Thread类还有一些常用方法。
如表所示。
表中列出了Thread类的常用方法,接下来用一个案例演示如何用继承Thread类的方式创建线程,如例所示。
程序的运行结果如图所示。
在例中,声明SubThread1类继承Thread类,在类中重写了run()方法,方法内循环打印小于4的奇数,其中,currentThread()方法是Thread类的静态方法,可以返回当前正在执行的线程对象的引用,最后在main()方法中创建两个SubThread1类实例,分别调用start()方法启动两个线程,两个线程都运行成功。这是继承Thread类创建多线程的方式。
如果start()方法调用一个已经启动的线程,程序会报IllegalThreadStateException异常。
Ø 实现Runnable接口创建线程
11.2.1节讲解了继承Thread类的方式创建线程,但Java只支持单继承,一个类只能有一个父类,继承Thread类后,就不能再继承其他类,为了解决这个问题,可以用实现Runnable接口的方式创建线程,下面介绍实现Runnable接口创建并启动多线程,具体步骤如下。
(1)定义Runnable接口实现类,并重写run()方法。
(2)创建Runnable实现类的示例,并将实例对象传给Thread类的target来创建线程对象。
(3)调用线程对象的start()方法启动线程。
接下来通过一个案例演示如何用实现Runnable接口的方式创建线程,如例所示。
程序的运行结果如图所示。
在例中,声明SubThread2类实现Runnable接口,在类中实现了run()方法,方法内循环打印小于4的奇数,最后在main()方法中创建SubThread2类实例,分别创建并开启两个线程对象,这里调用public Thread(Runnable target, String name)构造方法,指定了线程名称,两个线程都运行成功。这是实现Runnable接口的方式创建线程。
Ø 使用Callable接口和Future接口创建线程
讲解了实现Runnable接口的方式创建多线程,但重写run()方法实现功能代码有一定局限性,这样做方法没有返回值且不能抛出异常,JDK5.0后,Java提供了Callable接口来解决此问题,接口内有一个call()方法可以作为线程执行体,call()方法有返回值且可以抛出异常。下面介绍实现Callable接口创建并启动线程,具体步骤如下。
(1)定义Callable接口实现类,指定返回值类型,并重写call()方法。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
Callable接口不是Runnable接口的子接口,所以不能直接作为Thread的target,而且call()方法有返回值,是被调用者,JDK5.0还提供了一个Future接口代表call()方法的返回值,Future接口有一个FutureTask实现类,它实现了Runnable接口,可以作为Thread类的target,接下来先了解一下Future接口的方法,如表所示。
使用Callable接口和Future接口创建线程
表中列出了Future接口的方法,接下来通过一个案例演示如何用Callable接口和Future接口创建多线程,示例代码参考教材11.2.3节。
程序的运行结果如图所示。
在例中,声明SubThread3类实现Callable接口,重写call()方法,在方法内实现功能代码,main()方法中执行for循环,循环中启动子线程,最后调用get()方法获得子线程call()方法的返回值。
另外,多次执行例的程序,sum=3永远都是最后打印,而“主线程for循环执行完毕..”可能在子线程循环前、后或中间输出,sum=3永远都是最后输出,是因为通过get()方法获取子线程call()方法的返回值时,当子线程此方法还未执行完毕,get()方法会一直阻塞,直到call()方法执行完毕才能取到返回值。这是用Callable接口和Future接口创建线程的方式。
Ø 三种实现多线程方式的对比分析
前面讲解了可以通过三种方式创建线程,包括继承Thread类、实现Runnable接口和实现Callable接口的方式,接下来介绍一下这三种创建线程方式的优点和弊端。
1.继承Thread类创建线程
优点:编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。
弊端:线程类已经继承了Thread类,所以不能再继承其他父类。
2.实现Runnable接口创建线程
优点:避免由于Java单继承带来的局限性。
弊端:编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
3.使用Callable接口和Future接口创建线程
优点:避免由于Java单继承带来的局限性,有返回值,可以抛出异常。
弊端:编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
如上列出了三种创建多线程方式的优点和弊端,一般情况下推荐使用后两种实现接口的方式创建多线程,实际开发中要根据实际需求确定使用哪种方式。
Ø 线程的生命周期及状态转换
前面讲解了线程的创建,接下来了解一下线程的生命周期。线程有新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Terminated)五种状态,线程从新建到死亡称为线程的生命周期,接下来了解一下线程的生命周期及状态转换,如图所示。
图描述了线程的生命周期及状态转换,下面详细讲解线程这五种状态。
1.新建状态
当程序使用new关键字创建一个线程后,该线程处于新建状态,此时它和其他Java对象一样,在堆空间内分配了一块内存,但还不能运行。
2.就绪状态
当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态,Java虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待获得CPU的使用权。
3.运行状态
处于这个状态的线程占用CPU,执行程序代码。在并发执行时,如果计算机只有一个CPU,那么只会有一个线程处于运行状态。如果计算机有多个CPU,那么同一时刻可以有多个线程占用不同CPU处于运行状态,只有处于就绪状态的线程才可以转换到运行状态。
4.阻塞状态
阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态,它才有机会转换到运行状态。
5.死亡状态
(1)线程的run()方法正常执行完成,线程正常结束。
(2)线程抛出异常(Exception)或错误(Error)。
(3)调用线程对象的stop()方法结束该线程。
线程一旦转换为死亡状态,就不能运行且不能转换为其他状态。
线程的调度
如果计算机只有一个CPU,那么在任意时刻只能执行一条指令,每个线程只有获得CPU使用权才能执行指令。多线程的并发运行,从宏观上看,是各个线程轮流获得CPU的使用权,分别执行各自的任务。但在运行池中,会有多个处于就绪状态的线程在等待CPU,Java虚拟机的一项任务就是负责线程的调度,即按照特定的机制为多个线程分配CPU使用权。调度模型分为分时调度模型和抢占式调度模型两种。
分时调度模型是让所有线程轮流获得CPU使用权,平均分配每个线程占用CPU的时间片。抢占式调度模型是优先让可运行池中优先级高的线程占用CPU,若运行池中线程优先级相同,则随机选择一个线程使用CPU,当它失去CPU使用权,再随机选取一个线程获取CPU使用权。Java默认使用抢占式调度模型,接下来详细讲解线程调度的相关知识。
Ø 线程的优先级
所有处于就绪状态的线程根据优先级存放在可运行池中,优先级低的线程运行机会较少,优先级高的线程运行机会更多。Thread类的setPriority(int newPriority)方法和getPriority()方法分别用于设置优先级和读取优先级。优先级用整数表示,取值范围1~10,除了直接用数字表示线程的优先级,还可以用Thread类中提供的三个静态常量来表示线程的优先级,如表所示。
表中列出了Thread类的三个静态常量,可以用这些常量设置线程的优先级,接下来用一个案例演示线程优先级的使用,如例所示。
程序的运行结果如图所示。
在例中,声明SubThread1类继承Thread类,在类中重写了run()方法,方法内循环打印小于6的奇数,在main()方法中创建三个SubThread1类实例并指定线程名称,调用setPriority(int newPriority)方法分别设置三个线程的优先级,最后调用start()方法启动三个线程,从执行结果看,优先级高的线程优先执行。这里要注意,优先级低的不一定永远后执行,有可能优先级低的线程先执行,只不过几率较小。
Thread类的setPriority(int newPriority)方法可以设置10种优先级,但这些优先级级别需要操作系统的支持,但是不同的操作系统上支持的优先级不同,不能很好地支持Java的10个优先级别,例如Windows2000只支持7个优先级别,所以尽量避免直接用数字指定线程优先级,应该使用Thread类的三个常量指定线程优先级别,这样可以保证程序有很好的可移植性。
Ø 线程休眠
前面讲解了线程的优先级,可以发现将需要后执行的线程设置为低优先级,也有一定几率先执行该线程,可以用Thread类的静态方法sleep()来解决这一问题,sleep()方法有两种重载形式,具体示例如下。
如上所示是sleep()方法的两种重载形式,前者参数是指定线程休眠的毫秒数,后者是指定线程休眠的毫秒数和毫微秒数。正在执行的线程调用sleep()方法可以进入阻塞状态,也叫线程休眠,在休眠时间内,即使系统中没有其他可执行的线程,该线程也不会获得执行的机会,当休眠时间结束才可以执行该线程。接下来用一个案例来演示线程休眠。如例所示。
程序的运行结果如图所示。
在例中,在循环中打印五次格式化后的当前时间,每次打印后都调用Thread类的sleep()方法,让程序休眠2秒,打印的五次运行结果,每次的间隔都是2秒。这是线程休眠的基本使用。
Ø 线程让步
前面讲解了使用sleep()方法使线程阻塞,Thread类还提供一个yield()方法,它与sleep()方法类似,它也可以让当前正在执行的线程暂停,但yield()方法不会使线程阻塞,只是将线程转换为就绪状态,也就是让当前线程暂停一下,线程调度器重新调度一次,有可能还会将暂停的程序调度出来继续执行,这也称为线程让步。接下来用一个案例演示线程让步。如例所示。
程序的运行结果如图所示。
在例中,声明SubThread3类实现Runnable接口,在类中实现了run()方法,方法内循环打印数字1~6,当变量i能被3整除时,调用yield()方法线程让步,在main()方法中创建SubThread3类实例,分别创建并开启两个线程,运行结果中,线程执行到3或6次时,变量i能被3整除,调用Thread类的yield()方法线程让步,切换到其他线程,这里注意,并不是线程执行到3或6次一定切换到其他线程,也有可能线程继续执行。这是线程让步的基本使用。
Ø 线程插队
Thread类提供了一个join()方法,当某个线程执行中调用其他线程的join()方法时,此线程将被阻塞,直到被join()方法加入的线程执行完为止,也称为线程插队。接下来用一个案例演示线程插队,如例所示。
程序的运行结果如图所示。
在例中,声明SubThread4类实现Runnable接口,在类中实现了run()方法,方法内循环打印数字1~5,在main()方法中创建SubThread4类实例并启动线程,main()方法中同样循环打印数字1~5,当变量i为2时,调用join()方法将子线程插入,子线程开始执行,直到子线程执行完,main()方法的主线程才能继续执行。这是线程插队的基本使用。
Ø 后台线程
线程中还有一种后台线程,它是为其他线程提供服务的,又称为“守护线程”或“精灵线程”,JVM的垃圾回收线程就是典型的后台线程。
如果所有的前台线程都死亡,后台线程会自动死亡。当整个虚拟机中只剩下后台线程,程序就没有继续运行的必要了,所以虚拟机也就退出了。
若将一个线程设置为后台线程,可以调用Thread类的setDaemon(boolean on)方法,将参数指定为true即可,Thread类还提供了一个isDaemon()方法,用于判断一个线程是否是后台线程,接下来用一个案例演示后台线程。
程序的运行结果如图所示。
在例中,声明SubThread5类继承Thread类,在类中实现了run()方法,方法内循环打印数字0~1000的奇数,在main()方法中创建SubThread5类实例,调用setDaemon(boolean on)方法,将参数指定为true,此线程被设置为后台线程,随后开启线程,最后循环打印0~2的数字,这里可以看到,新线程本应该执行到打印999,但是这里执行到3就结束了,因为前台线程执行完毕,线程死亡,后台线程随之死亡。这是后台线程的基本使用。
多线程同步
前面讲解了线程的基本使用,在并发执行的情况下,多线程可能会突然出现“错误”,这是因为系统的线程调度有一定随机性,多线程操作同一数据时,很容易出现这种“错误”,接下来会详细讲解如何解决这种“错误”。
Ø 线程安全
关于线程安全,有一个经典的问题——窗口卖票的问题。窗口卖票的基本流程大致为首先知道共有多少张票,每卖掉一张票,票的总数要减1,多个窗口同时卖票,当票数剩余0时说明没有余票,停止售票。流程很简单,但如果这个流程放在多线程并发的场景下,就存在问题,可能问题不会及时暴露出来,运行很多次才出一次问题。接下来用一个案例来演示这个卖票窗口的经典问题。
程序的运行结果如图所示。
在例中,声明Ticket1类实现Runnable接口,首先在类中定义一个int型变量,用于记录总票数,然后在for循环中卖票,每卖一张,票总数减1,为了让程序的问题暴露出来,这里调用sleep()方法让程序每次循环都休眠100ms,最后在main()方法中创建并启动三个线程,模拟三个窗口同时售票。运行结果可以看出,第5张票重复卖了2次,剩余的票数还出现了-1张。
例中出现这种情况是因为run()方法的循环中判断票数是否大于0,大于0则继续出售,但这里调用sleep()方法让程序每次循环都休眠100ms,这就会出现第一个线程执行到此处休眠的同时,第二和第三个线程也进入执行,所以总票数减的次数增多,这就是线程安全的问题。
Ø 同步代码块
前面提出了线程安全的问题,为了解决这个问题,Java的多线程引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块,具体示例如下。
如上所示,synchronized关键字后括号里的obj就是同步监视器,当线程执行同步代码块时,首先会检查同步监视器的标志位,默认情况下标志位为1,线程会执行同步代码块,同时将标志位改为0,当第二个线程执行同步代码块前,检查到标志位为0,第二个线程会进入阻塞状态,直到前一个线程执行完同步代码块内的操作,标志位重新改为1,第二个线程才有可能进入同步代码块。接下来通过修改例11-9的代码演示用同步代码块解决线程安全问题。
程序的运行结果如图所示。
例与前边的例几乎完全一样,区别就是例在run()方法的循环中执行售票操作时,将操作变量ticket的操作都放到同步代码块中,在使用同步代码块时必须指定一个需要同步的对象,一般用当前对象(this)即可。将例修改为例后,多次运行该程序不会出现重票或负票的情况。
同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。“任意”说的是共享锁对象的类型。所以,锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁,每个锁都有自己的标志位,线程之间便不能产生同步的效果。
Ø 同步方法
前面讲解了用同步代码块解决线程安全问题,Java还提供了同步方法,即使用synchronized关键字修饰方法,该方法就是同步方法,同步方法的监视器是this,也就是调用该方法的对象,同步方法也可以解决线程安全的问题,接下来通过修改例的代码来演示用同步方法解决线程安全问题。
程序的运行结果如图所示。
例与前边的例几乎完全一样,区别就是例的run()方法是用synchronized关键字修饰的,将例修改为例后,多次运行该程序不会出现重票或负票的情况。
同步方法的锁就是当前调用该方法的对象,也就是this指向的对象,但是静态方法不需要创建对象就可以用“类名.方法名()”的方式调用,这时的锁不再是this,静态同步方法的锁是该方法所在类的class对象,该对象可以直接用“类名.class”的方式获取。
Ø 死锁问题
在多线程应用中还存在死锁的问题,不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。接下来通过一个案例演示死锁的情况。
程序的运行结果如图所示。
在例中,当TestDeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒,而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,再锁定o2,睡眠500毫秒,td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁,程序出现阻塞状态。
在编写代码时要尽量避免死锁,采用专门的算法、原则,尽量减少同步资源的定义。此外,Thread类的suspend()方法也容易导致死锁,已被标记为过时的方法。
Ø 多线程通信
不同的线程执行不同的任务,如果这些任务有某种联系,线程之间必须能够通信,协调完成工作,例如,生产者和消费者互相操作仓库,当仓库为空时,消费者无法从仓库取出产品,应该先通知生产者向仓库中加入产品。当仓库已满时,生产者无法继续加入产品,应该先通知消费者从仓库取出产品。java.lang包中Object类提供了三个用于线程通信的方法,如表所示。
表中列出了Object类提供的三个用于线程通信的方法,这里要注意的是,这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报IllegalMonitorStateException异常。
多线程通信
线程通信中有一个经典例子就是生产者和消费者问题,生产者(Productor)将产品交给售货员(Clerk),而消费者(Customer)从售货员处取走产品,售货员一次最多只能持有固定数量的产品(比如10),如果生产者试图生产更多的产品,售货员会让生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,售货员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。接下来通过一个案例演示生产者和消费者的问题,首先需要创建一个代表售货员的类,示例代码参考教材11.6节。
例的Clerk类代表售货员,它有两个方法和一个变量,两个方法都是同步方法,其中,addProduct()方法用来添加商品,getProduct()方法用来取走商品,接下来继续编写代表生产者和消费者的类,示例代码参考教材11.6节。
例的Productor类代表生产者,调用Clerk类的addProduct()方法不停地生产产品,例的Consumer类代表消费者,调用Clerk类的getProduct()方法不停地消费产品,最后来编写程序的入口main()方法,如例所示。
程序的运行结果如图所示。
在例中,先创建售货员实例,然后创建并开启生产者和消费者两个线程,生产者和消费者不停地生产和消费产品,且售货员一次持有的产品数量不超过10个,这就是线程通信中生产者和消费者的经典问题。
Ø 线程组和未处理的异常
Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。用户创建的所有线程都属于指定的线程组,若未指定线程属于哪个线程组,则该线程属于默认线程组。在默认情况下,子线程和创建它的父线程处于同一个线程组内,另外,线程运行中不能改变它所属的线程组。Thread类提供了一些构造方法来设置新创建的线程属于哪个线程组,如表所示。
表列出了Thread类的构造方法,这些构造方法可以为线程指定所属的线程组,指定线程组的参数为ThreadGroup类型,接下来了解一下ThreadGroup类的构造方法,如表所示。
表列出了ThreadGroup类的构造方法,构造方法中都有一个String类型的名称,这就是线程组的名字,可以通过ThreadGroup类的getName()方法来获取,但不允许修改线程组的名字。另外,还需要了解一下ThreadGroup类的常用方法。
如表所示。
表列出了ThreadGroup类的常用方法,接下来通过一个案例演示线程组的使用,示例代码参考教材11.7节。
程序的运行结果如图所示。
在例中,声明SubThread类继承Thread类,该类有两个构造方法,一个是指定名称,一个是指定线程组和名称,在run()方法的循环中打印执行了第几次,在main()方法中先得到主线程名称并判断是否是后台线程,然后创建一个新线程组并设为后台线程,判断新线程是否是后台线程,最后同时运行这些线程,从运行结果可看出主线程、tg2组的线程1、tg2组的线程2分别执行了3次。这就是线程组的基本使用。
ThreadGroup类还定义了一个可以处理线程组内任意线程抛出的未处理异常,具体示例如下。
当此线程组中的线程因为一个未捕获的异常而停止,并且线程在JVM结束该线程之前没有查找到对应的Thread.UncaughtExceptionHandler时,由JVM调用如上方法。
Thread.UncaughtExceptionHandler是Thread类的一个静态内部接口,接口内只有一个方法void uncaughtException(Thread t,Throwable e),方法中的t代表出现异常的线程,e代表该线程抛出的异常。接下来通过一个案例演示主线程运行抛出未处理异常如何处理,如例所示。
程序的运行结果如图所示。
在例中,声明MyHandler类实现UncaughtExceptionHandler类,在uncaughtException(Thread t,Throwable e)方法中打印某个线程出现某个异常,在main()方法中用0做除数,运行程序报错,可以看到异常处理器对未捕获的异常进行处理了,但程序仍然不能正常结束,说明异常处理器与通过catch捕获异常是不同的,异常处理器对异常进行处理后,异常依然会传播给上一级调用者。
Ø 线程池
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好地提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
在JDK5.0之前,大家必须手动实现自己的线程池,从JDK5.0开始,Java内置支持线程池。提供一个Executors工厂类来产生线程池,该类中都是静态工厂方法,先来了解一下Executors类的常用方法。如表所示。
表列出了Executors类的常用方法,接下来通过一个案例演示线程池的使用,如例所示。
程序的运行结果如图所示。
在例中,调用Executors类的newFixedThreadPool(int nThreads)方法,创建了一个大小为10的线程池,向线程池中添加了2个线程,两个线程分别循环打印执行了第几次,最后关闭线程池。
小结:Java语言程序设计—多线程
通过学习,能够掌握Java多线程的相关知识。重点要了解的是Java的多线程机制可以同时运行多个程序块,从而使程序有更好的用户体验,也解决了传统程序设计语言所无法解决的问题。