第16章 多线程
本章将会详细介绍Java多线程编程的相关方面,包括创建、启动线程、控制线程,以及多线程的同步操作,并会介绍如何利用Java内建支持的线程池来提高多线程性能。
16.1 线程概述
16.1.1 线程和进程
- 进程(Process):所有运行中的任务通常对应一个进程(Process)。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
- 进程包含如下三个特征:
(1)独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
(2)动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
(3) 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。 - 并发性(concurrency);并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
- 并行性(parallel):并行指在同一时刻,有多条指令在多个处理器上同时执行。
- 线程(Thread)也被称作轻量级进程(Lightweight Process),线程是进程的执行单元。
- 归纳起来:操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。
16.1.2 多线程的优势
- 使用多线程编程具有如下几个优点:
(1)进程之间不能共享内存,但线程之间共享内存非常容易。
(2)系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
(3)Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。
16.2 线程的创建和启动
16.2.1 继承Thread类创建线程类
- 通过继承Thread类来创建并启动多线程的步骤如下:
(1)定义Thread类的子类,并重写该类的run()
方法,该run()方法的方法体就代表了线程需要完成的任务。因此把run()方法称为线程执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3) 调用线程对象的start()
方法来启动该线程。 - 线程的几个方法:
Thread.currentThread()
:currentThread()是Thread类的静态方法,该方法总是返回当前正在执行的线程对象。
getName()
:该方法是Thread类的实例方法,该方法返回调用该方法的线程名字。
setName(String name)
:为线程设置名字。
- 使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。
16.2.2 实现Runnable接口创建线程类
- 实现Runnable接口来创建并启动多线程的步骤如下:
(1)定义Runnable接口的实现类,并重写该接口的run()
方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()
方法来启动该线程。 - Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的
run()
方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。 - Runnable接口中只包含一个抽象方法,从Java8开始,Runnable接口使用了
@Functionallnterface修饰。也就是说,Runnable接口是函数式接口,可使用Lambda表达式创建Runnable对象。接下来介绍的Callable接口也是函数式接口。
16.2.3 使用Callable和Future创建线程
- 从Java 5开始,Java提供了Callable接口,该接口怎么看都像是Runnable接口的增强版,Callable接口提供了一个
call()
方法可以作为线程执行体,但call()方法比run()方法功能更强大。
call()方法可以有返回值。
call()方法可以声明抛出异常。
-
Java 5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口——可以作为Thread类的target。
-
在Future接口里定义了如下几个公共方法来控制它关联的Callable任务:
boolean cancel(boolean mayInterruptIfRunning):试图取消该Future里关联的Callable任务。
V get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值。
V get(long timeout, TimeUnit unit):返回Callable任务里call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException异常。
boolean isCancelled():如果在Callable任务正常完成前被取消,则返回true。
boolean isDone():如果Callable任务已完成,则返回true。
- Callable接口有泛型限制,Callable接口里的泛型形参类型与call()方法返回值类型相同。而且Callable接口是函数式接口,因此可使用Lambda表达式创建Callable对象。
- 创建并启动有返回值的线程的步骤如下:
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例。
(2)使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
16.2.4 创建线程的三种方式对比
- 实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此可以将实现Runnable接口和实现Callable接口归为一种方式。这种方式与继承Thread方式之间的主要差别如下。
- 采用实现Runnable、Callable接口的方式创建多线程的优缺点:
(1)线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
(2)在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
(3)劣势是,编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。采用继承Thread类的方式创建多线程的优缺点:
(4)劣势是,因为线程类已经继承了Thread类,所以不能再继承其他父类。
(5)优势是,编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。 - 因此一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。
16.3 线程的生命周期
在线程的生命周期中,要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。
16.3.1 新建和就绪状态
- 当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。
- 当线程对象调用了
start()
方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了,至于该线程何时开始运行,取决于JVM里线程调度器的调度。 - 如果希望调用子线程的start()方法后子线程立即开始执行,程序可以使用
Thread.sleep(1)
来让当前运行的线程(主线程)睡眠1毫秒——1毫秒就够了,因为在这1毫秒内CPU不会空闲,它会去执行另一个处于就绪状态的线程,这样就可以让子线程立即开始执行。
16.3.2 运行和阻塞状态
当发生如下情况时,线程将会进入阻塞状态。
- 线程调用sleep()方法主动放弃所占用的处理器资源。
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
- 线程在等待某个通知(notify)。
- 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。
当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态。也就是说,被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度它。
针对上面几种情况,当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态。
- 调用sleep()方法的线程经过了指定时间。
- 线程调用的阻塞式IO方法已经返回。
- 线程成功地获得了试图取得的同步监视器。
- 线程正在等待某个通知时,其他线程发出了一个通知。
- 处于挂起状态的线程被调用了resume()恢复方法。
16.3.3 线程死亡
- 线程会以如下三种方式结束,结束后就处于死亡状态。
(1) run()或call()方法执行完成,线程正常结束。
(2)线程抛出一个未捕获的Exception或Error。
(3)直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。 - 当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会受主线程的影响。
- 为了测试某个线程是否已经死亡,可以调用线程对象的
isAlive()
方法,当线程处于就绪、运行、阻塞三种状态时,该方法将返回true;当线程处于新建、死亡两种状态时,该方法将返回false。
16.4 控制线程
16.4.1 join线程
- join()方法:Thread提供了让一个线程等待另一个线程完成的方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。
- join()方法有如下三种重载形式。
join():等待被join的线程执行完成。
join(long millis):等待被join的线程的时间最长为millis毫秒。如果在millis毫秒内被join的线程还没有执行结束,则不再等待。
join(long millis,int nanos):等待被join的线程的时间最长为millis 毫秒加nanos 毫微秒。
16.4.2 后台线程(守护线程)
- “后台线程(Daemon Thread)”,又称为“守护线程”或“精灵线程”有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务。JVM的垃圾回收线程就是典型的后台线程。
- 后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。
- 调用Thread对象的
setDaemon(true)
方法可将指定线程设置成后台线程。 - Thread类还提供了一个
isDaemon()
方法,用于判断指定线程是否为后台线程。 - 前台线程死亡后,JVM会通知后台线程死亡,但从它接收指令到做出响应,需要一定时间。而且要将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说,
setDaemon(true)
必须在start()
方法之前调用,否则会引发IllegalThreadStateException异常。
16.4.3 线程睡眠:sleep
- 如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态
sleep()
方法来实现。方法有两种重载形式。
static void sleep(long millis)
:让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度与准确度的影响。
static void sleep(long millis,int nanos)
:让当前正在执行的线程暂停millis毫秒加nanos毫微秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度与准确度的影响。
- sleep() 方法常用来暂停程序的执行。
- 此外,Thread还提供了一个与
sleep()
方法有点相似的yield()
静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。 - 实际上,当某个线程调用了
yield()
方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。 - 关于
sleep()
方法和yield()
方法的区别如下:
(1)sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级;但yield()方法只会给优先级相同,或优先级更高的线程执行机会。
(2)sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;而yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程被yield()方法暂停之后,立即再次获得处理器资源被执行。
(3)sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么显式声明抛出该异常;而yield()方法则没有声明抛出任何异常。
(4)sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。
16.4.4 改变线程优先级
- 每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。
- 每个线程默认的优先级都与创建它的父线程的优先级相同,在默认情况下,main线程具有普通优先级,由main线程创建的子线程也具有普通优先级。
- Thread类提供了
setPriority(int newPriority)
、getPriority()
方法来设置和返回指定线程的优先级,其中setPriority()
方法的参数可以是一个整数,范围是1~10之间,也可以使用Thread类的如下三个静态常量:
MAX_PRIORITY:其值是10。
MIN_PRIORITY:其值是1。
NORM_PRIORITY:其值是5。
16.5 线程同步
16.5.1 线程安全问题
关于线程安全问题,银行取钱的问题(不能多取);卖票问题(不能超卖)等。
16.5.2 同步代码块
- java 引入同步监视器来解决这个问题,同步代码块的语法格式如下:
synchronized(obj){
...
//此处的代码就是同步代码块
}
- 任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
16.5.3 同步方法
- 同步方法:就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于synchronized修饰的实例方法(非static方法)而言,无须显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。
- 不可变类总是线程安全的,因为它的对象状态不可改变;可变对象需要额外的方法来保证其线程安全。
- synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、成员变量等。
- JDK所提供的StringBuilder、StringBuffer就是为了照顾单线程环境和多线程环境所提供的类,在单线程环境下应该使用StringBuilder来保证较好的性能;当需要保证多线程安全时,就应该使用StringBuffer。
16.5.4 释放同步监视器的锁定
- 任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定。
- 程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定。
(1)当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器。
(2)当前线程在同步代码块、同步方法中遇到break、return终止该代码块、该方法的继续执行,当前线程会释放同步监视器。
(3)当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器。
(4)当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。 - 在如下所示的情况下,线程不会释放同步监视器。
(1)线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
(2)线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。
16.5.5 同步锁(Lock)
- 从Java5开始,Java提供了一种功能更强大的线程同步机制—通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当。
- Lock 提供了比synchronized 方法和synchronized代码块更广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。
- Lock 是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- 某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock、ReadWriteLock是Java5提供的两个根接口,并为Lock提供了**ReentrantLock(可重入锁)**实现类,为ReadWriteLock 提供了ReentrantReadWriteLock实现类。
Java8新增了新型的StampedLock类,在大多数场景中它可以替代传统的ReentrantReadWriteLock。 - ReentrantReadWriteLock 为读写操作提供了三种锁模式:Writing、ReadingOptimistic、Reading。
- 在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。使用该Lock对象可以显式地加锁、释放锁,通常使用ReentrantLock的代码格式如下:
class x {
//定义锁对象
private final ReenrantLock lock = new ReentrantLock();
...
//定义需要保证线程安全的方法
public void m(){
//加锁
lock.lock();
try{
//需要保证线程安全的代码
...
}
//使用finally块来保证释放锁
finally{
lock.unlock();
}
}
}
- ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock 对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
16.5.6 死锁
- 当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。
- 一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
- 可以通过下面几种常见方式来解决死锁问题。
(1)避免多次锁定:尽量避免同一个线程对多个同步监视器进行锁定。
(2)具有相同的加锁顺序:如果多个线程需要对多个同步监视器进行锁定,则应该保证它们以相同的顺序请求加锁。
(3)使用定时锁:程序调用Lock对象的tryLock()方法加锁时可指定time和unit参数,当超过指定时间后会自动释放对Lock的锁定,这样就可以解开死锁了。
(4)死锁检测:这是一种依靠算法来实现的死锁预防机制,它主要针对那些不可能实现按序加锁,也不能使用定时锁的场景。 - 由于Thread类的suspend()方法也很容易导致死锁,所以Java不再推荐使用该方法来暂停线程的运行。
16.6 线程通信
16.6.1 传统的线程通信
- 要求:系统要求存款者和取钱者不断地重复存款、取钱的动作,而且要求每当存款者将钱存入指定账户后,取钱者就立即取出该笔钱。不允许存款者连续两次存钱,也不允许取钱者连续两次取钱。
- 为了实现这种功能,可以借助于Object类提供的
wait()
、notify()
和notifyAll()
三个方法,这三个方法并不属于Thread类,而是属于Object类。但这三个方法必须由同步监视器对象来调用,这可分成以下两种情况:
(1)对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。
(2)对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法。 - 关于这三个方法的解释如下:
wait()
:导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。该wait()方法有三种形式一—无时间参数的wait(一直等待,直到其他线程通知)、带毫秒参数的和带毫秒、毫微秒参数的(这两种方法都是等待指定时间后自动苏醒)。调用wait()方法的当前线程会释放对该同步监视器的锁定。
notify()
:唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。
notifyAll()
:唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
16.6.2 使用Condition控制线程通信
- 如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()、notify()、notifyAll()方法进行线程通信了。
- 当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。
- Lock替代了同步方法或代码块,Condition替代了同步监视器的功能。
- Condition实例被绑定在一个Lock对象上。要获得特定Lock实例的Condition实例,调用Lock对象的
newCondition()
方法即可。 - Condition类提供了如下三个方法。
await()
:类似于隐式同步监视器上的wait()方法,导致当前线程等待,直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程。
signal()
:唤醒在此Lock对象上等待的单个线程。如果所有线程都在该Lock对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该Lock对象的锁定后(使用await()方法),才可以执行被唤醒的线程。
signalAll()
:唤醒在此Lock对象上等待的所有线程。只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。
16.6.3 使用阻塞队列(BlockingQueue)控制线程通信
- Java 5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程同步的工具。
- BlockingQueue具有一个特征:
(1)当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;
(2)当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。 - BlockingQueue提供如下两个支持阻塞的方法。
put(Ee)
:尝试把E元素放入BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
take()
:尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。
- BlockingQueue继承了Queue接口,当然也可使用Queue接口中的方法。这些方法归纳起来可分为如下三组。
(1)在队列尾部插入元素。包括add(Ee)
、offer(Ee)
和put(Ee)
方法,当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。
(2)在队列头部删除并返回删除的元素。包括remove()
、poll()
和take()
方法。当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。
(3)在队列头部取出但不删除元素。包括element()
和peek()
方法,当队列已空时,这两个方法分别抛出异常、返回false。
ArrayBlockingQueue:基于数组实现的BlockingQueue队列。
LinkedBlockingQueue:基于链表实现的BlockingQueue队列。
PriorityBlockingQueue:它并不是标准的阻塞队列。与前面介绍的PriorityQueue类似。
SynchronousQueue:同步队列。对该队列的存、取操作必须交替进行。
DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现。
16.7 线程组和未处理的异常
- Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。对线程组的控制相当于同时控制这批线程。用户创建的所有线程都属于指定线程组,如果程序没有显式指定线程属于哪个线程组,则该线程属于默认线程组。在默认情况下,子线程和创建它的父线程处于同一个线程组内。
- 一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组。
- Thread类提供了如下几个构造器来设置新创建的线程属于哪个线程组。
Thread(ThreadGroup group, Runnable target)
:以target的run()方法作为线程执行体创建新线程,属于group线程组。
Thread(ThreadGroup group, Runnable target, String name)
:以target的run()方法作为线程执行体创建新线程,该线程属于group线程组,且线程名为name。
Thread(ThreadGroup group, String name)
:创建新线程,新线程名为name,属于group线程组。
- 因为中途不可改变线程所属的线程组,所以Thread类没有提供setThreadGroup()方法来改变线程所属的线程组,但提供了一个
getThreadGroup()
方法来返回该线程所属的线程组,getThreadGroup()方法的返回值是ThreadGroup对象,表示一个线程组。 - ThreadGroup类提供了如下两个简单的构造器来创建实例。
ThreadGroup(String name)
:以指定的线程组名字来创建新的线程组。
ThreadGroup(ThreadGroup parent, String name)
:以指定的名字、指定的父线程组创建一个新线程组。
- 上面两个构造器在创建线程组实例时都必须为其指定一个名字,也就是说,线程组总会具有一个字符串类型的名字,该名字可通过调用ThreadGroup的
getName()
方法来获取,但不允许改变线程组的名字。 - ThreadGroup类提供了如下几个常用的方法来操作整个线程组里的所有线程。
int activeCount()
:返回此线程组中活动线程的数目。
interrupt()
:中断此线程组中的所有线程。
isDaemon()
:判断该线程组是否是后台线程组。
setDaemon(boolean daemon)
:把该线程组设置成后台线程组。后台线程组具有一个特征——当后台线程组的最后一个线程执行结束或最后一个线程被销毁后,后台线程组将自动销毁。
setMaxPriority(int pri)
:设置线程组的最高优先级。
- ThreadGroup内还定义了一个很有用的方法:
void uncaughtException(Thread t,Throwable e)
,该方法可以处理该线程组内的任意线程所抛出的未处理异常。 - 从Java 5开始,Java加强了线程的异常处理,如果线程执行过程中抛出了一个未处理异常,JVM在结束该线程之前会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到该处理器对象,则会调用该对象的uncaughtException(Thread t,Throwable e)方法来处理该异常。
- Thread.UncaughtExceptionHandler是Thread类的一个静态内部接口,该接口内只有一个方法:void uncaughtException(Thread t,Throwable e),该方法中的t代表出现异常的线程,而e代表该线程抛出的异常。
- Thread类提供了如下两个方法来设置异常处理器。
static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandlereh)
:为该线程类的所有线程实例设置默认的异常处理器。
setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
:为指定的线程实例设置异常处理器。
- ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,所以每个线程所属的线程组将会作为默认的异常处理器。当一个线程抛出未处理异常时,JVM会首先查找该异常对应的异常处理器(setUncaughtExceptionHandler()方法设置的异常处理器),如果找到该异常处理器,则将调用该异常处理器处理该异常;否则,JVM将会调用该线程所属的线程组对象的uncaughtException()方法来处理该异常。
- 线程组处理异常的默认流程如下。
(1) 如果该线程组有父线程组,则调用父线程组的uncaughtException()方法来处理该异常。
(2)如果该线程实例所属的线程类有默认的异常处理器(由setDefaultUncaughtExceptionHandler()方法设置的异常处理器),那么就调用该异常处理器来处理该异常。
(3)如果该异常对象是ThreadDeath的对象,则不做任何处理;否则,将异常跟踪栈的信息打印到System.err错误输出流,并结束该线程。
16.8 线程池
16.8.1 使用线程池管理线程
- 从Java5开始,Java内建支持线程池。Java5新增了一个Executors工厂类来产生线程池,该工厂类包含如下几个静态工厂方法来创建线程池。
newCachedThreadPool()
:创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。
newFixedThreadPool(int nThreads)
:创建一个可重用的、具有固定线程数的线程池。
newSingleThreadExecutor()
:创建一个只有单线程的线程池,相当于调用newFixedThread Pool()方法时传入参数为1。
newScheduledThreadPool(int corePoolSize)
:创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。
newSingleThreadScheduledExecutor()
:创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。
ExecutorService newWorkStealingPool(int parallelism)
:创建持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争。
ExecutorService newWorkStealingPool()
:该方法是前一个方法的简化版本。如果当前机器有4个CPU,则目标并行级别被设置为4,也就是相当于为前一个方法传入4作为参数。
- 前三个方法返回一个ExecutorService对象,该对象代表一个线程池,它可以执行Runnable对象或Callable对象所代表的线程;
- 而中间两个方法返回一个ScheduledExecutorService线程池,它是ExecutorService的子类,它可以在指定延迟后执行线程任务;
- 最后两个方法则是Java 8新增的,这两个方法可充分利用多CPU并行的能力。
- ExecutorService代表尽快执行线程的线程池,提供了如下三个方法:
Future<?> submit(Runnable task)
:将一个Runnable对象提交给指定的线程池,线程池将在有空闲线程时执行Runnable对象代表的任务。其中Future对象代表Runnable任务的返回值——但run()方法没有返回值,所以Future对象将在run()方法执行结束后返回null。但可以调用Future的isDone()、isCancelled()方法来获得Runnable对象的执行状态。
<T>Future<T>submit(Runnable task, T result)
:将一个Runnable对象提交给指定的线程池,线程池将在有空闲线程时执行Runnable对象代表的任务。其中result显式指定线程执行结束后的返回值,所以Future对象将在run()方法执行结束后返回result。
<T>Future<T>submit(Callable<T>task)
:将一个Callable对象提交给指定的线程池,线程池将在有空闲线程时执行Callable对象代表的任务。其中Future代表Callable对象里call()方法的返回值。
- ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池,它提供了如下4个方法。
ScheduledFuture<V>schedule(Callable<V>callable, long delay, TimeUnit unit)
:指定callable任务将在delay延迟后执行。
ScheduledFuture<?>schedule(Runnable command, long delay, TimeUnit unit)
:指定command任务将在delay延迟后执行。
ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay,long period, TimeUnit unit)
:指定command任务将在delay延迟后执行,而且以设定频率重复执行。也就是说,在initialDelay后开始执行,依次在initialDelay+period、initialDelay+2*period…处重复执行,依此类推。
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, longinitialDelay, long delay, TimeUnit unit)
:创建并执行一个在给定初始延迟后首次启用的定期操作,随后在每一次执行终止和下一次执行开始之间都存在给定的延迟。
- 用完一个线程池后,应该调用该线程池的
shutdown()
方法,该方法将启动线程池的关闭序列,调用shutdown()方法后的线程池不再接收新任务,但会将以前所有已提交任务执行完成。 - 使用线程池来执行线程任务的步骤如下:
(1)调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。
(2)创建Runnable实现类或Callable实现类的实例,作为线程执行任务。
(3)调用ExecutorService对象的submit()
方法来提交Runnable实例或Callable实例。
(4)当不想提交任何任务时,调用ExecutorService对象的shutdown()
方法来关闭线程池。
public class TreadPoolTest{
public static void main(String[] args){
//创建一个具有固定线程数(6)的线程池
ExecutorService pool = Executors.newFixedThreadPool(6);
//使用Lambda表达式创建Runnable对象
Runnable target = () -> {
for (int i = 0; i < 100 ;i++){
System.out.println(Thread.currentThread().getName() + "的i值为:" + i);
}
};
//向线程池中提交两个线程
pool.submit(target);
pool.submit(target);
//关闭线程池
pool.shutdown();
}
}
16.8.2 java 8 增强的ForkJoinPool
- Java 7提供了ForkJoinPool来支持将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果。ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池。
- ForkJoinPool提供了如下两个常用的构造器。
ForkJoinPool(int parallelism)
:创建一个包含parallelism个并行线程的ForkJoinPool。
ForkJoinPool()
:以Runtime.availableProcessors()方法的返回值作为parallelism参数来创建Fork JoinPool。
- Java 8进一步扩展了ForkJoinPool的功能,Java 8为ForkJoinPool增加了通用池功能。ForkJoinPool类通过如下两个静态方法提供通用池功能。
ForkJoinPool commonPool()
:该方法返回一个通用池,通用池的运行状态不会受shutdown()或shutdownNow()方法的影响。当然,如果程序直接执行System.exit(0);来终止虚拟机,通用池以及通用池中正在执行的任务都会被自动终止。
int getCommonPoolParallelism()
:该方法返回通用池的并行级别。
- 创建了ForkJoinPool实例之后,就可调用ForkJoinPool的
submit(ForkJoinTask task)
或invoke(ForkJoinTask task)
方法来执行指定任务了。其中ForkJoinTask代表一个可以并行、合并的任务。 - ForkJoinTask是一个抽象类,它还有两个抽象子类:RecursiveAction和RecursiveTask。其中RecursiveTask代表有返回值的任务,而RecursiveAction代表没有返回值的任务。
//当end与start之间的差大于THRESHOLD,即要累加的数超过20个时
//将大任务分解成两个“小任务”
int middle = (start + end) / 2;
CalTask left = new CalTask(arr, start, middle);
CalTask right = new CalTask(arr, middle, end);
//并行执行两个“小任务”
left.fork();
right.fork();
//把两个“小任务”累加的结果合并起来
return 1eft.join() + right.join();
16.9 线程相关类
16.9.1 ThreadLocal类
- ThreadLocal,是Thread Local Variable(线程局部变量)的意思。线程局部变量(ThreadLocal)的功用其实非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量一样。
- ThreadLocal类的用法非常简单,它只提供了如下三个public方法。
T get()
:返回此线程局部变量中当前线程副本中的值。
void remove()
:删除此线程局部变量中当前线程的值。
void set(T value)
:设置此线程局部变量中当前线程副本中的值。
- ThreadLocal从另一个角度来解决多线程的并发访问,ThreadLocal将需要并发访问的资源复制多份,每个线程拥有一份资源,每个线程都拥有自己的资源副本,从而也就没有必要对该变量进行同步了。
- ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源(变量)的竞争,也就不需要对多个线程进行同步了。
- 通常建议:如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制;如果仅仅需要隔离多个线程之间的共享冲突,则可以使用ThreadLocal。
16.9.2 包装线程不安全的集合
- 如果程序中有多个线程可能访问以上这些集合,就可以使用Collections提供的类方法把这些集合包装成线程安全的集合。
- Collections提供了如下几个静态方法。
<T> Collection<T> synchronizedCollection(Collection<T> c)
:返回指定collection对应的线程安全的collection。
static <T> List<T> synchronizedList(List<T> list)
:返回指定List对象对应的线程安全的List对象。
static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
:返回指定Map对象对应的线程安全的Map对象。
static <T> Set<T> synchronizedSet(Set<T> s)
:返回指定Set对象对应的线程安全的Set对象。
static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)
:返回指定SortedMap对象对应的线程安全的SortedMap对象。
static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s)
:返回指定SortedSet对象对应的线程安全的SortedSet对象。
16.9.3 线程安全的集合类
- 实际上从Java5开始,在java.util.concurrent包下提供了大量支持高效并发访问的集合接口和实现类。
- 以Concurrent开头的集合类,如ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkip ListSet、ConcurrentLinkedQueue和ConcurrentLinkedDeque。代表了支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。
- 以CopyOnWrite开头的集合类,如CopyOnWriteArrayList、CopyOn WriteArraySet。这些集合类采用了更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。
- Java8扩展了ConcurrentHashMap的功能,这些方法可借助于Stream和Lambda表达式支持执行聚集操作。ConcurrentHashMap新增的方法大致可分为如下三类。
forEach系列(forEach,forEachKey,forEachValue,forEachEntry)
search系列(search,searchKeys,search Values,searchEntries)
reduce系列(reduce,reduce ToDouble,reduce ToLong,reduceKeys,reduce Values)
16.9.4 Java 9新增的发布-订阅框架
- Java 9新增了一个发布-订阅框架,该框架是基于异步响应流的。这个发布-订阅框架可以非常方便地处理异步线程之间的流数据交换(比如两个线程之间需要交换数据)。而且这个发布-订阅框架不需要使用数据中心来缓冲数据,同时具有非常高效的性能。
- 这个发布-订阅框架使用Flow类的4个静态内部接口作为核心API。
Flow.Publisher:代表数据发布者、生产者。
Flow.Subscriber:代表数据订阅者、消费者。
Flow.Subscription:代表发布者和订阅者之间的链接纽带。订阅者既可通过调用该对象的request()方法来获取数据项,也可通过调用对象的cancel()方法来取消订阅。
Flow.Processor:数据处理器,它可同时作为发布者和订阅者使用。
- Flow.Publisher发布者作为生产者,负责发布数据项,并注册订阅者。Flow.Publisher接口定义了如下方法来注册订阅者。
void subscribe(Flow.Subscriber<?super T>subscriber)
:程序调用此方法注册订阅者时,会触发订阅者的onSubscribe()方法,而Flow.Subscription对象作为参数传给该方法;如果注册失败,将会触发订阅者的onError() 方法。
- Flow.Subscriber接口定义了如下方法:
void onSubscribe(Flow.Subscription subscription)
:订阅者注册时自动触发该方法。
void onComplete()
:当订阅结束时触发该方法。
void onError(Throwable throwable)
:当订阅失败时触发该方法。
void onNext(T item)
:订阅者从发布者处获取数据项时触发该方法,订阅者可通过该方法获取数据项。
- 为了处理一些通用发布者的场景,Java 9为Flow.Publisher提供了一个SubmissionPublisher实现类,它可向当前订阅者异步提交非空的数据项,直到它被关闭。每个订阅者都能以相同的顺序接收到新提交的数据项。
- 程序创建SubmissionPublisher对象时,需要传入一个线程池作为底层支撑;该类也提供了一个无参数的构造器,该构造器使用
ForkJoinPool.commonPool()
方法来提交发布者,以此实现发布者向订阅者提供数据项的异步特性。