1. 线程概述
1.1 线程和进程
进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
线程也被称为轻量级进程,线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其它线程共享该进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
1.2 并发和并行
并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
1.3 多线程的优势
(1)进程之间不能共享内存,但线程之间共享内存却非常容易。
(2)系统创建进程时需要为该进程重新分配系统资源,但创建线程代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
(3)java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了java的多线程编程。
1.4 程序运行原理
分时调度:
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度:
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
1.5 主线程
jvm启动后,必然有一个执行路径(线程)从main方法开始的,一直执行到main方法结束,这个线程在java中称之为主线程。当程序的主线程执行时,如果遇到了循环而导致程序在指定位置停留时间过长,则无法马上执行下面的程序,需要等待循环结束后能够执行。
2. 线程的创建和启动
2.1 Thread类
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流。Java使用县城执行体来表示这段流。
构造方法:
常用方法:
2.2 继承Thread类创建线程
通过继承Thread类来创建并启动线程的步骤:
(1)定义一个类继承Thread
(2)重写run方法
(3)创建Thread子类的实例,即创建了线程对象
(4)调用线程对象的star()方法来启动线程
示例:
package com.xupt.test;
public class Test01 {
public static void main(String[] args){
Thread01 t1=new Thread01();
Thread01 t2=new Thread01();
t1.start();
t2.start();
for (int i = 0; i <100; i++) {
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}
class Thread01 extends Thread{
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println( Thread.currentThread().getName()+"--"+i);
}
}
}
Thread.currentThread()获取当前线程对象
Thread.currentThread().getName();获取当前线程对象的名称
注意:
使用继承Thread类的方法来创建线程时,多个线程之间无法共享线程类的实例变量。
线程对象调用 run方法和调用start方法区别
线程对象调用run方法不开启线程。仅是对象调用方法。线程对象调用start开启线程,并让jvm调用run方法在开启的线程中执行。
2.3 继承Thread类原理
我们为什么要继承Thread类,并调用其的start方法才能开启线程呢?
继承Thread类:因为Thread类用来描述线程,具备线程应该有功能。那为什么不直接创建Thread类的对象呢?如下代码:
Thread t1 = new Thread();
t1.start();//这样做没有错,但是该start调用的是Thread类中的run方法,而这个run方法没有做什么事情,更重要的是这个run方法中并没有定义我们需要让线程执行的代码。
创建线程的目的是什么?
是为了建立程序单独的执行路径,让多部分代码实现同时执行。也就是说线程创建并执行需要给定线程要执行的任务。
对于之前所述的主线程,它的任务定义在main函数中。自定义线程需要执行的任务都定义在run方法中。
Thread类run方法中的任务并不是我们所需要的,只有重写这个run方法。既然Thread类已经定义了线程任务的编写位置(run方法),那么只要在编写位置(run方法)中定义任务代码即可。所以进行了重写run方法动作。
2.4 实现Runnable接口创建线程类
创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。
接口中的方法:
Thread类构造方法:
创建线程的步骤:
(1)定义类实现Runnable接口
(2)重写接口中的run()方法
(3)创建Thread类的对象
(4)将Runnable接口的子类对象作为参数传递给Thread类的构造函数
(5)调用Thread类的star()方法开启线程
示例:
package com.xupt.test;
public class Test02 {
public static void main(String[] args) {
Runnable01 r=new Runnable01();
Thread t1=new Thread(r);
Thread t2=new Thread(r);
t1.start();
t2.start();
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}
class Runnable01 implements Runnable{
public void run() {
for (int i = 0; i <100; i++) {
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}
2.5 实现Runnable原理&&实现Runnable好处
为什么需要定一个类去实现Runnable接口呢?继承Thread类和实现Runnable接口有啥区别呢?
实现Runnable接口,避免了继承Thread类的单继承局限性。覆盖Runnable接口中的run方法,将线程任务代码定义到run方法中。
创建Thread类的对象,只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。
实现Runnable好处:
第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。
2.6 使用匿名内部类创建线程
方式一:创建线程对象时,直接重写Thread类中的run方法
package com.xupt.test;
public class Test03 {
public static void main(String[] args) {
new Thread(){
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}.start();
for (int i = 0; i <100; i++) {
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}
方式二:使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法
package com.xupt.test;
public class Test03 {
public static void main(String[] args) {
new Thread(new Runnable(){
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}).start();
for (int i = 0; i <100; i++) {
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}
2.7 线程的生命周期
(1)新建和就绪状态
当使用new关键字创建一个线程后,该线程处于新建状态,此时它和其它java对象一样,仅仅由java虚拟机为其分配内存,并初始化其成员变量的值。此时线程对象没有表现出任何线程的动态特征,程序也不会执行线程线程执行体。
当线程对象调用了start()方法之后,该线程处于就绪状态(就绪状态相当于等待执行,但该线程并未真正进入运行状态),java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM线程调度器的调度
(2)运行和阻塞状态
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态。
阻塞状态是指线程因为某种原因放弃了cpu 使用权——当一个线程调用了它的sleep()或yield()方法后会放弃所占用的资源。当发生如下情况时,线程将会进入阻塞状态:
a.线程调用sleep()方法主动放弃所占用的处理器资源
b.线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
c.线程试图获得一个同步监视器,但该同步监视器正被其它线程所持有
d.线程在等待某个通知(notify)
e.程序调用了线程的suspend()方法将该线程挂起
当前正在执行的线程被阻塞后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态。即被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度它。针对上面几种情况,当发生如下特定情况时可以解除上面的阻塞,让线程重新进入就绪状态:
a.调用了sleep()方法的线程经过了指定的时间
b.线程调用的阻塞式IO方法已经返回
c.线程成功的获得了试图取得的同步监视器
d.线程正在等待某个通知时。其他线程发出了一个通知
e.处于挂起状态的线程调用了resume()恢复方法
(3)线程死亡
线程会以如下三种方式结束,结束后处于死亡状态。
a.run()或call()方法执行完成,线程正常结束
b.线程抛出一个未捕获的Exception或Error
c.直接调用该线程的stop()方法来结束该线程
为了测试某个线程是否已经死亡,可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞三种状态时,该方法返回true;当线程处于新建、死亡两种状态时,该方法将返回false。不能对处于死亡状态的线程调用start()方法,程序只能对新建状态的线程调用start()方法,对新建状态的线程两次调用start()方法也是错误的,都会引发IllegaThreadStateException异常。
2.8 线程的控制
(1)join线程
Thread提供了让一个线程等待另一个线程完成的方法——join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join()线程执行完为止。
(2)后台线程
有一种线程,他是在后台运行的,它的任务是为其他线程提供服务,这种线程被称为后台线程,也被称为守护线程或精灵线程。JVM的垃圾回收线程就是典型的后台线程,后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程。而且要将某个线程设置为后台线程,必须在该线程启动之前设置,即setDaemon(true) 必须在start()方法之前调用,否则会引发IllegaThreadStateException异常。
(3)线程睡眠
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现。当线程调用sleep()方法进入阻塞状态后,在其睡眠时间段内,该线程不会获得执行机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序的执行。
(4)线程让步
yield()与sleep()方法相似,它可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。实际上,当某个方法调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程优先级更高的处于就绪状态的线程才会获得执行机会。
sleep()方法和yield()方法的区别:
a.sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其它线程的优先级;但yield()方法只会给优先级相同,或者优先级更高的线程执行机会
b.sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;而yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用 yield()方法暂停之后,立即再次获得处理器资源被执行
c.sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么显式申明抛出该异常;而yield()方法没有声明抛出任何异常
d.sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行
2.9 改变线程的优先级
每个执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。每个线程默认的优先级都与创建它的父线程的优先级相同,在默认情况下,main线程具有普通优先级,由main线程创建的子线程也具有普通优先级。
Thread类提供了setPriority(intnewPriority )、getPriority()方法来设置和返回指定线程的优先级,其中setPriority()方法的参数可以是一个整数,范围是1~10之间,也可以使用Thread类的如下三个静态常量。
MAX_PRIORITY:其值是10
MIN_PRIORITY:其值是1
NORM_PRIORITY:其值是5
3.线程同步
3.1线程安全
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
示例:
package com.xupt.test02;
public class MyThread implements Runnable{
//定义100张火车票
private int i=100;
public void run() {
while(true){
if(i>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"**"+i--);
}
}
}
}
package com.xupt.test02;
/*
* 模拟火车站卖票
*/
public class Test01 {
public static void main(String[] args) {
MyThread mt=new MyThread();
//多个线程修改同一个数据时将会引发线程安全问题
Thread t0=new Thread(mt);
Thread t1=new Thread(mt);
Thread t2=new Thread(mt);
t0.start();
t1.start();
t2.start();
}
}
运行结果:
java中提供了线程同步机制,它能够解决上述的线程安全问题。线程同步的方式有两种:
方式1:同步代码块;
方式2:同步方法
3.2同步代码块
在代码块声明上加synchronized,同步代码块语法格式如下:
synchronized (obj){
可能会产生线程安全问题的代码
}
上面语法格式中synchronized后面括号里的obj就是同步监视器,上面代码块的含义:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
示例:
package com.xupt.test02;
public class MyThread implements Runnable{
//定义100张火车票
private int i=100;
Object obj=new Object();
public void run() {
synchronized(obj){
while(true){
if(i>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"**"+i--);
}
}
}
}
}
同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。
3.3 同步方法
与同步代码块对应,java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于synchronized修饰的实例方法(非static方法)而言,无需显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。
静态同步方法: 在方法声明上加上staticsynchronized
public static synchronizedvoid method(){
可能会产生线程安全问题的代码
}
静态同步方法中的锁对象是类名.class
通过使用同步方法可以非常方便地实现线程安全的类,线程安全的类具有如下特征:
(1)该类的对象可以被多个线程安全的访问
(2)每个线程调用该对象的任意方法之后都将得到正确的结果
(3)每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态
示例:
package com.xupt.test02;
public class MyThread implements Runnable{
//定义100张火车票
private int i=100;
@Override
public void run() {
//同步方法
payThread();
}
public synchronized void payThread(){
//模拟卖票
while(true){
if(i>0){
System.out.println(Thread.currentThread().getName()+"**"+i--);
}
}
}
}
synchronized关键字可以修饰方法和代码块,不能修饰构造器和成员变量
3.4 释放同步监视器的锁定
任何线程进入同步代码块之、同步方法之前,必须先获得同步监视器的锁定。程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定:
(1)当前线程的同步方法、同步代码块结束,当前线程即释放同步监视器
(2)当线程在同步代码块、同步方法中遇到break、return终止该代码块、该方法的继续执行,当前线程将会释放同步监视器
(3)当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器
(4)当前线程执行同步代码块或者同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器
以下情况,线程不会释放同步监视器:
(1)当前线程执行同步代码块或者同步方法时,程序调用了Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器
(2)线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。程序应尽量避免使用suspend()和resume()方法来控制线程
3.5 同步锁(Lock)
从java5开始,java提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当。
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。某些锁可能允许对共享资源的并发访问,如ReadWriteLock(读写锁),Lock、ReadWriterLock是java5提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。
Lock接口中的常用方法:
Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。
示例:
package com.xupt.test02;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyThread implements Runnable {
// 定义100张火车票
private int i = 100;
// 创建锁对象
Lock l = new ReentrantLock();
@Override
public void run() {
// 模拟卖票
while (true) {
l.lock();
if (i > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "**"
+ i--);
}
l.unlock();
}
}
}
3.6 死锁
同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。
synchronzied(A锁){
synchronized(B锁){
}
}
4. 线程间的通信——等待唤醒机制
多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源,这种手段即——等待唤醒机制。
等待唤醒机制所涉及到的方法:
wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。
其实,所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。
仔细查看JavaAPI之后,发现这些方法 并不定义在 Thread中,也没定义在Runnable接口中,却被定义在了Object类中,为什么这些操作线程的方法定义在Object类中?因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。
示例:
资源类:
package com.xupt.Thread;
public class Resource {
private String name;
private int age;
private boolean flag=true;
public static final Resource rs=new Resource();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public boolean getFlag() {
return flag;
}
}
输入类:
package com.xupt.Thread;
public class Input implements Runnable{
private Resource r=Resource.rs;
public void run() {
int i=0;
while(true){
synchronized(r){
if(r.getFlag()){
if(i%2==0){
r.setName("薛之谦");
r.setAge(30);
}else if(i%2==1){
r.setName("毛不易");
r.setAge(25);
}
i++;
}
//改变标记
r.setFlag(false);
//唤醒Output
r.notify();
try {
r.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
输出类:
package com.xupt.Thread;
public class Output implements Runnable{
private Resource r=Resource.rs;
public void run() {
while(true){
synchronized(r){
if(!r.getFlag()){
System.out.println(r.getName()+"--"+r.getAge());
}
//改变标记
r.setFlag(true);
//唤醒Input
r.notify();
try {
r.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
测试类:
package com.xupt.Thread;
public class Test {
public static void main(String[] args) {
Input in=new Input();
Output ou=new Output();
//创建线程对象
Thread t0=new Thread(in);
Thread t1=new Thread(ou);
//开启线程
t0.start();
t1.start();
}
}
5. 线程池
5.1线程池的概念
线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。
线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。
5.2使用线程池方式--Runnable接口
通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。
Executors:线程池创建工厂类
public static ExecutorServicenewFixedThreadPool(int nThreads):返回线程池对象
ExecutorService:线程池类
Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用
使用线程池中线程对象的步骤:
(1) 创建线程池对象
(2)创建Runnable接口子类对象
(3)提交Runnable接口子类对象
(4)关闭线程池
示例:public class ThreadPoolDemo {
public static void main(String[] args) {
//创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
//创建Runnable实例对象
MyRunnable r = new MyRunnable();
//自己创建线程对象的方式
//Thread t = new Thread(r);
//t.start(); ---> 调用MyRunnable中的run()
//从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(r);
//再获取个线程对象,调用MyRunnable中的run()
service.submit(r);
service.submit(r);
//注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中
//关闭线程池
//service.shutdown();
}
}
Runnable接口实现类:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一个教练");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了: " +Thread.currentThread().getName());
System.out.println("教我游泳,交完后,教练回到了游泳池");
}
}
5.3 使用线程池的方式——Callable接口
Callable接口:与Runnable接口功能相似,用来指定线程的任务。其中的call()方法,用来返回线程任务执行完毕后的结果,
call方法可抛出异常。
ExecutorService:线程池类
<T> Future<T> submit(Callable<T> task):获取线程池中的某一个线程对象,并执行线程中的call()方法
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用
使用线程池中线程对象的步骤:
(1)创建线程池对象
(2)创建Callable接口子类对象
(3)提交Callable接口子类对象
(4)关闭线程池
示例:
public class ThreadPoolDemo {
public static void main(String[] args) {
//创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
//创建Callable对象
MyCallable c = new MyCallable();
//从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(c);
//再获取个教练
service.submit(c);
service.submit(c);
//注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中
//关闭线程池
//service.shutdown();
}
}
Callable接口实现类,call方法可抛出异常、返回线程任务执行完毕后的结果
public class MyCallable implements Callable {
@Override
public Object call() throws Exception {
System.out.println("我要一个教练:call");
Thread.sleep(2000);
System.out.println("教练来了: " +Thread.currentThread().getName());
System.out.println("教我游泳,交完后,教练回到了游泳池");
return null;
}
}