1、线程的创建和启动方式
1.1 继承Thread类创建线程类
让一个类继承Thread,并重写其中的run()方法,该run()方法中定义的就是线程需要做的事。所以run()方法是线程执行体。
以上是定义一个线程类,要让线程类执行还需要创建这个类的实例,并执行实例的start()方法启动该线程。定义和启动的示例代码如下。
public class FirstThread extends Thread{
private int i;
public void run(){
for(; i < 20; i++){
System.out.println(this.getName() + " " + i);
}
}
public static void main(String[] args) {
for(int j = 0; j < 100; j++){
System.out.println(Thread.currentThread().getName() + " " + j);
if(j == 20){
new FirstThread().start();
new FirstThread().start();
}
}
}
}
这段代码的输出如下
这段代码的入口是main函数。main函数开始运行时就启动了一个主线程,从上图中可以看出主线程的名字是main。主线程启动后就开始循环输出j。当j=20时,主线程就会开启两个线程。当然这两个线程开启后不一定会马上开始执行,于是主线程和另外两个线程开始随机执行。
上面的代码中涉及到了两个方法,可能比较常用。
- Thread.currentThread():这是Thread的静态方法,用来返回当前正在执行的线程对象。
- getName():该方法是Thread的实例方法,返回该线程的名字。默认情况下,主线程名字是main,其它线程创建顺序不同名字依次是Thread-0,Thread-1…等。
虽然同一个进程中的子线程是可以共享变量的,但是继承于Thread类的线程类A,我们多次调用A的start()方法,即启动了多个线程,这些线程的成员变量不是共享的,每个线程各自拥有属于自己的成员变量。就像上面例子中Thread-1和Thread-0的i不是共享的。
1.2 实现Runnable接口创建线程类
实现Runnable接口,并重写其中的run()方法,该run()方法和继承Thread类中的一样。
创建该类的实例,然后将实例作为参数放到Thread类的构造函数中构造出新的线程,然后调用线程的start()方法启动线程。示例代码如下。
public class FirstThread implements Runnable{
private int i;
public void run(){
for(; i < 20; i++){
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
for(int j = 0; j < 100; j++){
System.out.println(Thread.currentThread().getName() + " " + j);
if(j == 20){
FirstThread runnable = new FirstThread();
new Thread(runnable, "新线程1").start();
new Thread(runnable, "新线程2").start();
}
}
}
}
代码的执行结果如下
1.2.1 Runnable和Thread创建线程间的区别
这里有一点是需要注意到的,在通过继承Thread创建线程的例子中,我们通过继承Thread类定义一个线程类A。类A中有一个私有变量i
。之后我多次调用A的start()方法,即创建多个线程,并对私有变量i
累加并输出。可以发现结果如左图所示,通过类A创建的线程各自拥有自己的私有变量i
。
而通过实现Runnable接口的线程类创建的线程之间是互相共享实例变量的,所以右图中的输出两个线程的i是连续的。
1.3使用Callable和Furture创建线程
1.3.1、FutureTask创建线程
Future接口是用于增强Runnable接口的,比如Future中提供了获取run
方法返回值的方法(在Future中,run方法就是call方法)。
但Future接口本身无法创建线程,通常我们使用Future
的子类FutureTask
来创建线程。FutureTask
同时实现了Runnable
和Future
接口,也就是说,我们可以使用FutureTask
创建线程(来自Runnable),同时也可以获取线程执行方法的返回值(来自Future)。
FutureTask的构造函数如下
其中的Callable是个函数式接口,可以看成是Runnable的增强版。Callable提供了一个call()方法作为线程执行体,并且call()方法可以有返回值并且可以抛出异常。call()方法对于Callable而言就是Runnable中的run()方法,call()方法里定义线程的逻辑。
刚刚说call()方法可以有返回值。Callable是个泛型接口,Callable,T就是call()方法的返回值的类型。
下面来看看FutureTask如何构造一个线程。FutureTask实现接口Future。
创建线程之前先定义一个FutureTask实例。
FutureTask<Integer> futureTask = new FutureTask((Callable<Integer>) () -> {
int i = 0;
for(; i < 100; i++){
System.out.println(Thread.currentThread().getName() + "的i的值是" + i);
}
return i;
});
有了FutureTask实例后,就可以像Runnable那样启动一个线程,所以整个线程的demo如下所示
FutureTask<Integer> task = new FutureTask((Callable<Integer>) () -> {
int i = 0;
for(; i < 100; i++){
System.out.println(Thread.currentThread().getName() + "的i的值是" + i);
}
return i;
});
for(int i = 0; i < 100; i++){
System.out.println(Thread.currentThread().getName() + "的i的值是" + i);
if(20 == i){
//新建线程并启动
new Thread(task, "有返回值的线程").start();
}
}
执行结果如下图所示
1.3.2、Future中的方法
Future接口提供了好几个方法来控制它关联的Callable任务,其中就包括获取返回值。
- V get():返回Callable任务里的call()方法的返回值。调用该方法会导致当前线程阻塞,必须等到该
FutureTask
对象代表的线程执行结束后才会得到返回值。 - V get(long time, TimeUnit unit):返回Callable任务里的call()方法的返回值。调用该方法会导致当前线程阻塞,必须等到该
FutureTask
对象代表的线程执行结束后才会得到返回值。或者当阻塞时间超过了参数指定的时间之后,会直接抛出TimeoutException异常。 - boolean cancel(boolean mayInterrupt):取消线程的执行。如果参数是true,那即使线程已经开始执行(执行了
start()
方法),那线程也会被强行中断;如果参数是false,且线程刚新建,还未执行start()
方法,线程可以被取消,并且取消后线程再执行start()
方法也是无效的。但是如果线程已经执行了start()
方法,那就会中断失败。如果线程成功被取消,返回true,否则返回false。 - boolean isCancelled():如果线程被
cancel()
方法成功取消掉,该方法返回true。其他方法导致线程终止,该方法返回false。 - boolean isDone():无论是什么方法,线程被
cancel()
方法取消也好,正常流程结束也好,发生异常终止线程也好,只要线程任务结束了,该方法就返回true,否则返回false。
2、线程的生命周期
2.1 线程的生命周期
线程的生命周期是:新建、就绪、运行、阻塞、死亡。
2.2 新建和就绪
当new一个线程时,该线程就处于新建状态,和其它普通Java对象一样,Java虚拟机只是给它分配内存并初始化成员变量的值。这时的线程并没有表现出线程的动态特征。
线程对象调用start()方法后,线程就处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器。处于这个状态的线程还没运行,但随时可能进入运行状态,具体情况要看Java虚拟机的调度。
注意是使用start()方法启动线程,而不是run()。而且一旦直接调用了 run()方法,那这个线程对象就只会被看成是普通对象,即使之后再调用start()方法也没用了。
2.3 运行和阻塞
一个CPU在同一时间只能有一个线程处于运行状态。
发生以下情况时,线程会发生阻塞。
- 线程调用sleep()方法,不会放弃锁。
- 线程调用了阻塞式的I/O方法,在方法返回之前,线程被阻塞。
- 线程试图获取一个同步监听器,而这同步监听器正被其它线程锁持有。
- 线程在等待某个通知(notify)执行了wait()方法,该方法会放弃锁。
- 程序调用了线程的suspend()方法将线程挂起。但这个方法容易引起死锁,所以应该尽量避免使用该方法。
正在执行的程序被阻塞之后会在何时的时间进入就绪状态。针对上面的几种情况,发生以下的事件会让线程进入就绪状态。
- 调用sleep()的线程经过了指定时间。
- 线程调用的阻塞式I/0方法已经返回。
- 线程成功地获取了试图取得的同步监视器。
- 线程正在等待某个通知时,其它线程发出了一个通知。
- 处于挂起状态的线程调用了resume()回复方法。
2.4 线程死亡
当发生下面三种情况时,线程会死亡。
- run()或call()方法执行完成,线程正常结束。
- 线程抛出一个为捕获的Exception或Error。
- 直接调用该线程的stop()方法来结束该线程,但这方法容易引起死锁。
为了测试某个线程是否已经死亡,可以调用线程对象的isAlive()方法,当线程处于就绪,运行,阻塞三种状态时,该方法返回true;当线程处于新建,死亡两种状态时,方法返回false。
对于已死亡的线程和已经启动的线程,若再次调用start()方法,会抛出IllegalThreadStateException异常。
3、控制线程
这一节讲的是如何控制控制线程的状态,比如让某个线程获得更多的执行机会(设置优先级),线程中断以后处理方式等等。
3.1 线程的 join()方法:暂停,让我先行
Thread提供了让一个线程等待另一个线程的方法,join()方法。当一个线程A的执行体中新建了另一个线程B,并启动线程B的start()方法,然后执行线程B的join()方法,那线程A会一直等待线程B执行完毕后,才会开始重新执行未完成的代码。
注意,以上的过程必须是按顺序的,线程B要先执行start()方法后才能执行join()方法。
另外,关于执行体的解释需要详细说一下。这里的执行体在代码中是指两个地方:一是主函数的psvm代码块,二是线程的run()方法里。
第一种是主程序中定义了一个线程A,执行这个线程A的join()方法时,主程序的执行会暂停,一直等到线程A结束后才开始执行。网上的很多例子介绍线程的join()方法时,举的例子都是第一种情况。
但第二种情况很少见,我担心线程A的run中执行线程B的join()方法时,出现的结果不符合预期,于是我做了以下测试。
public class Test {
public static void main(String[] args) {
new Thread(() -> {
for(int i = 0; i < 20; i++){
System.out.println("子线程A " + i);
if(i == 10){
Thread thread = new Thread(() -> {
for(int j = 0; j < 20; j++){
System.out.println("子线程B " + j);
}
}, "子线程B");
thread.start();
try{
thread.join();
} catch(Exception e){}
}
}
}, "子线程A").start();
}
}
输出结果如下,线程B在线程A中执行join()方法后,线程A就立刻暂停。直到线程B执行结束以后,线程A才开始执行。
3.2 线程中断
3.2.1 检测线程中断状态,向线程发起中断
我们可以使用interrupt()
方法来向一个线程发出中断请求,这是一个实例方法。该方法执行后,线程中的中断标志位会变为true,表示线程收到了中断请求。
thread1.interrupt(); //thread1线程被中断
我们使用isInterrupted()
方法来看看线程是否被中断,这也是一个实例方法true表示被中断,false表示没有被中断。
还有一个静态方法interrupted()
可以检测线程是否被中断,返回值和isInterrupted()
一样。不同点是,该方法检测的是当前线程的中断状态,并且执行以后会将线程的中断位设为false。
3.2.2 中断的概念
Thread.sleep()
的官方文档中可以看到该方法会抛出InterruptedException
异常,这里需要介绍线程中断的概念。
线程的中断和操作系统中的中断不是同一个概念,前者收到中断请求后可以不理会中断,而后者必须处理中断。
我们可以使用interrupt()
方法来向一个线程发出中断请求,该方法执行后,线程中的中断标志位会变为true。我们使用isInterrupted()
方法来看看线程是否被中断。
线程的中断机制可以看成是一个消息提示,中断可以不被理会。比如下面的代码,主线程执行线程1的thread1.interrupt();
,thread1的isInterrupted()
也返回true,表示收到中断请求了。但是线程1仍然继续执行原有逻辑。
public class InterruptTest {
private static Integer i = 0;
public static void main(String[] args) {
Runnable runnable = new InterruptedRunnable();
Thread thread1 = new Thread(() -> {
while(true) {
if(!Thread.currentThread().isInterrupted()) {
System.out.println("线程1正在执行");
} else {
System.out.println("线程1收到中断请求,但是不想理这个中断");
}
}
}, "线程1");
thread1.start();
while(true) {
i++;
System.out.println("主线程:" + i);
if(i == 20) {
thread1.interrupt();
}
if(i == 30) {
return;
}
}
}
}
所以说,线程的中断只是一个消息机制,如果线程逻辑不处理中断请求,那中断不会对线程的状态产生任何影响。
3.3 线程睡眠
sleep()方法可让当前正在执行的线程进入阻塞状态。下面是sleep()方法的文档
要注意到sleep()是静态方法不是实例方法,所以它的功能是让当前正在执行的方法进入阻塞状态
3.3.1 InterruptedException抛出的时机
当线程的中断位是true
时(线程处于中断状态),此时对这个线程sleep()
的话,sleep()
方法会抛出InterruptedException
异常,睡眠失败,并且使线程的中断位变为false
(线程不处于中断状态)。
比如下面的代码,线程1一开始在中断状态下执行sleep()
就抛出异常,同时清除了中断位,使之后的sleep()
都没有抛出异常。
public static void main(String[] args) {
Runnable runnable = new InterruptedRunnable();
Thread thread1 = new Thread(() -> {
while(true) {
try{
System.out.println(Thread.currentThread().getName() + "的中断状态:"+ Thread.currentThread().isInterrupted());
Thread.sleep(99);
}
catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "发生了中断异常");
System.out.println(Thread.currentThread().getName() + "睡眠失败");
System.out.println(Thread.currentThread().getName() + "的中断状态:"+ Thread.currentThread().isInterrupted());
}
}
}, "线程1");
thread1.start();
thread1.interrupt();
}
执行结果如下
同样的场景还有线层阻塞,await()
和wait()
方法。当线程中断时,调用这两个方法也会抛出该异常。
3.4 线程让步
yield()和sleep()方法类似,两者都是静态方法,并且都是让现正在运行的线程暂停。不过不同的是,sleep()是让线程进入阻塞状态,而yield()是让线程进入就绪状态。
两者都不会放弃锁的占用
所以执行yield()后,即使当前执行的线程转入就绪状态,但下一个瞬间它还是有可能被选中继续执行。
3.5 改变线程优先级
每个线程具有一定的优先级,优先级高的线程会获得更高的执行机会。
Thread类提供了setPriority(int newPriority)和getPriority()两个实例方法来设置线程的优先级和获取线程的优先级。优先级是int类型,从1到10,数字越大优先级越高。Thread有三个静态变量表示优先级,
- MAX_PRIORITY:值为10。
- MIN_PRIORITY:值为1。
- NORM_PRIORITY:值为5。
当然,不同的操作系统的优先级不一定都是1到10,比如Windows 2000就只提供了7个优先级,所以编程时要避免直接为线程指定数字的优先级,而要用上面三个静态变量确保高移植性。
主线程的优先级默认是5,子线程的优先级默认与父线程的优先级相等,所以平时我们创建的子线程默认优先级为5。
3.6 后台线程
后台线程是用来服务其它线程的,比如JVM就是典型的后台线程。后台线程有个特征:所有的前台线程死亡后,后台线程才会自动死亡。
前台线程创建的线程是前台线程,后台线程创建的线程是后台线程。
主线程默认是前台线程,所以我们平时创建的线程都是前台线程。
将一个线程设置为后台线程的方法是通过线程实例的setDaemon(true)方法。判断一个线程是不是后台线程可用isDaemon()方法,返回true则为后台线程,反之为前台线程。
线程一旦进入就绪状态,即执行start()之后,就不能再更改线程属性,即不能条用setDaemon()方法,否则会引起IllegalThreadStateException异常。所以线程的setDaemon()方法必须在start()方法之前执行。
4、线程的同步
多线程环境下,一个线程的执行随时会被另一个线程打断。一般情况下这没什么,等会再继续执行呗。但是如果是多个线程同时访问操作一个公共资源,那就很可能出现问题。学过操作系统的都知道,多个线程同时读一个资源完全不会有事,但只要涉及到写,那就会出现隐患,无论是同时写还是同时读写。
同步解决的就是多线程访问操作同一公共资源时可能出现的问题。解决方式是加锁,一个线程想要访问操作一个资源时,就必须先取得关于这个资源的锁才能继续。当线程取得锁,完成相关操作后,才会将锁还回去,其他线程才有机会获得锁。
Java中加锁是通过synchronized和lock,下面一一讲解。
4.1 synchronized
synchronized有两种使用方法,一种是同步代码块,另一钟是同步方法,两者的主要区别是加锁的对象不同。
4.1.1 同步代码块
先说说同步代码块,同步代码块的语法是
synchronized(obj){
···
}
其中的obj是线程之间的共享变量,如果这个变量是线程独有的,那加这个锁毫无意义。
这个语法的含义是,在线程执行括号包含的代码之前,必须先获得关于obj这个公共资源的锁后才能执行。而且obj同一时间内只能被一个线程锁住,而且只有这个线程执行完之后才会将锁释放,然后别的线程才能进入这个代码块执行。
4.1.2 同步方法
同步方法的语法是在方法(必须是实例方法),代码如下
public synchronized void test(){
...
}
只要将synchronized 放在限定词之后,返回值之前就行了。要记住,同步方法是对this加锁,也就是这个方法的调用者,而不是对这个方法加锁。
一个类A有两个方法,一个加锁一个不加,代码如下
class A{
public synchronized void a(){...}
public void b(){...}
}
生成两个线程和A的实例对象,线程1执行实例的a方法,线程2执行实例的b方法。因为a方法加上了锁,所以线程1在执行a方法前,要先取得关于实例对象的锁;而方法b没加锁,所以线程2随时可以执行b方法。
于是写了下面的代码
class A{
public synchronized void a(){
int i = 0;
for (; i < 20; i++){
System.out.println("方法a:" + i);
}
}
public void b(){
int i = 0;
for (; i < 100; i++){
System.out.println("方法b:" + i);
}
}
}
public class Test {
public static void main(String[] args) throws Exception{
A a = new A();
FutureTask<Integer> task1 = new FutureTask<>(() -> {
//对象a的a方法没加锁,即访问a方法需要获得关于a对象的锁
a.a();
return;
});
FutureTask<Integer> task2 = new FutureTask<>(() -> {
//对象a的a方法没加锁,即访问a方法需要获得关于a对象的锁
a.a();
return;
});
FutureTask<Integer> task3 = new FutureTask<>(() ->{
//对象a的b方法没加锁,即访问b方法不需要获得关于a对象的锁
a.b();
return;
});
//线程1
new Thread(task1).start();
//线程2
new Thread(task2).start();
//线程3
new Thread(task3).start();
}
}
根据上面的分析,线程1和线程2会争夺锁。因为线程1先得到锁,所以线程2必须等到线程1释放锁以后(上面的例子中线程1释放锁就相当于线程1执行完成),才能有机会获得锁。所以预测线程2要等到线程1执行完成之后才会开始执行a方法。
而线程3调用的b方法不需要获得锁,所以线程3会随机执行。
运行上面的代码,执行结果验证了以上的分析。
可以看到,线程1和线程2是交替执行的。
但是如果把方法b也加上锁
//其余代码不变
public synchronized void b(){
int i = 0;
for (; i < 100; i++){
System.out.println("方法b:" + i);
}
}
//其余代码不变
那么执行结果就变成了严格的顺序执行,而两个线程访问的是不同方法,所以同步方法是对this加锁,而不是对方法加锁。
4.1.3 锁释放的时间
- 当线程的同步区的代码执行完自动释放锁。
- 当同步区的代码出现return或break使程序跳出同步区,则锁自动释放。
- 当同步区的代码出现为处理的Error或Exception导致同步区代码结束时,锁自动释放。
- 当同步区代码执行时,出现线程的wait()操作,则线程被暂停并释放锁
要注意,以下的操作并不会释放锁。
- 程序调用了Thread.sleep()和Thread.yield()使线程暂停时,线程不会释放锁
- 程序调用suspend()方法时,线程也不会释放锁。
4.2 Lock
Lock是一个更强大的同步机制,它的锁样式较多,甚至可允许一定程度的并发操作。synchronized就比较暴力,原本多线程对同一资源只读不写是完全没问题的,但是synchronized的锁不允许这种情况出现,而Lock则可以。
Lock是一个接口,他有很多实现类,这些实现类分别对应着不同的锁策略,比如ReentrantLock。首先,要先知道Lock锁的对象是谁。
4.2.1 Lock锁锁住的对象
Lock的使用方法是创建一个Lock对象,然后执行lock()
或者tryLock()
方法加锁,然后使用unLock()
方法解除锁。示例如下。
Lock lock = new ReentrantLock();
lock.lock();
try{
···
}
finally {
lock.unLock();
}
Lock锁的对象是自己,当lock()
执行时,当前线程会获得这个关于这个lock对象的锁。比如上面的示例中,线程1执行上面的代码(假设成功获得锁),那线程1就拥有关于lock对象的锁。在线程1未释放关于lock对象的锁之前,如果其它线程尝试执行lock.lock()
(该lock和线程1持有的lock是同一个),那这些线程会进入阻塞,直到重新获得lock锁。
加锁后,在保护区的代码往往会用try包住,最后再finally中释放锁。这是为了防止保护区出现异常,直接使线程终止,从而没有执行到后面的lock.unLock()
方法,导致线程一直占用锁让系统发生死锁。
5、线程通信
线程的执行虽然是由系统来决定怎么调度,但我们还是有一些权利来影响系统的决策。如何影响就是线程通信。
5.1 传统的线程通信
传统的线程通信是通过Object类提供的wait()、notify()、notifyAll()三个方法。上面提到synchronized有两种使用方法,一种是同步代码块,另一种是同步方法。这两者加锁的对象分别是synchronized括号里的对象和this。
我们就调用被锁住的对象的wait()、notify()、notifyAll()三个方法即可实现线程通信。
这三个方法的效果如下
5.1.1 wait方法
使占用该锁的线程等待,等待会让该线程放弃对锁的占用,并且等待(也就是阻塞)不是就绪状态,线程在唤醒之前都不会再次占用锁。
这里线程进入的是锁对象的等待空间中。
其它线程调用该锁的notify()方法或者notifyAll()通知唤醒该线程。该方法有两种重载形式:不带参数,线程无线等待;带参数的,有毫秒和毫微秒级别的参数。
- notify():唤醒在这个锁上处于等待状态的一个线程。如果有多个线程,那是选择任意一个线程唤醒。
- nofityAll():唤醒在这个锁上等待的所有线程。
5.1.2 代码调用的位置
上面的三个方法只能在sychronized的同步区代码调用。
5.2 使用Condition控制线程通信
如果系统是通过Lock来控制线程同步的,那才可以使用Condition。在某些时候,线程需要满足一定的条件才可以把任务完成。同时我们希望如果尚不满足条件,那就让线程一直等待。直到条件满足线程再去完成任务。并且在线程等待的过程中,我们不想让线程一直占用锁。这时候就可以使用Condition来控制线程通信,告诉线程什么时候等待(同时释放锁),什么时候重新执行代码。
Condition必须依附于某个Lock,可以通过Lock的实例方法newCondition()
获得Condition实例。如上所述,Condition就是用来控制线程通信,能做到的事就两个,让线程阻塞,让线程重新回到就绪状态。
5.2.1 Condition的用法
Condition有三个方法来控制线程。
5.2.1.1 await()方法
使当前线程阻塞,阻塞的线程会暂时放弃锁的权限。这个放弃锁的权限与unLock()
不一样。unLock()
是对Lock对象加层锁,ReentrantLock是可以连续多次lock()
的。只是unLock()
,那线程还有可能继续占有这个锁,但是await()
会让线层直接放弃锁的占用。
尽管await()
会让线程放弃对锁的占用,但是很多关于锁的信息并没有被删除。比如在线程1阻塞前,线程1对lock的lock.getHoldCount()
等于3。等线程1被唤醒后,lock.getHoldCount()
还是等于3。
某个锁的await()
使线程阻塞以后,线程是不会主动苏醒的。直到其它线程执行这个锁的signal()
或者signalAll()
之后,才能将锁唤醒。
await()
的设计就是为了让线程满足一定条件后才执行后面的代码,所以使用时往往会使用while来一直检验条件,直到通过。
while(如果条件不满足) {
lock.await();
}
5.2.1.2 signal()方法
随机唤醒在某个锁上被阻塞的线程。
使用时要注意,唤醒的只是一个随机的线程。如果这个线程不执行唤醒操作,那在这个锁上被阻塞的其他线程就有可能永远被阻塞,造成死锁。
5.2.1.3 signalAll()方法
随机唤醒在某个锁上被阻塞的所有线程。
5.3 阻塞队列控制线程通信
Java5提供了一个BlockingQueue接口,虽然该接口是Queue的子接口,但它的主要作用并不是作为一个容器,而是一个线程通信的工具。
阻塞队列有这样一个特征:当生产者试着想阻塞队列中放入元素时,如果队列已满,则生产者线程阻塞;当消费者试图从阻塞队列中获取元素时,如果队列为空,则消费者线程阻塞。
放入元素和获取元素分别对应着以下几个方法
- 在队列尾部插入元素:add(E e),offer(E e)和put(E e)。正常情况下,这三个方法效果相同。但队列已满时,这三个方法分别会抛出异常,返回false,阻塞队列。
- 在队列头部取出元素:remove(),poll()和take(),正常情况下,这三个方法效果相同。但队列为空时,这三个方法分别会抛出异常,返回false,阻塞队列。
BlockingQueue以及其实现类如下图所示
- ArrayBlockingQueue:基于数组实现的BlockingQueue队列。
- LinkedBlockingQueue:基于链表实现的BlockingQueue队列。
- PriorityBlockingQueue:这并不是个标准的阻塞队列。和PriorityQueue类似,该队列调用remove()、poll()和take()方法来取出元素时,并不是取出队列中存在时间最长的元素,而是队列中值最小的元素。对于大小的判定是根据元素实现的Conparable接口来判断。
- SynchronousQueue:同步队列。该队列的存取操作必须交替执行。
- DelayQueue:他是特殊的BlockingQueue,底层基于PriorityBlockingQueue实现。但是DelayQueue要求集合元素都实现Delay接口(接口里有一个long getDelay()方法),DelayQueue根据集合元素的getDelay()方法的返回值进行排序。
5.4 线程组和未处理的异常
Java可以以组为单位来管理线程,使用ThreadGroup。我们直接对ThreadGroup进行操作,就相当于对属于这个组的所有线程进行相同的操作。
我们创建的线程其实都属于某一个线程组,及时我们没有显式指定。默认情况下,子线程属于父线程的组,这个逻辑和设置后台线程的逻辑差不多。
显式设置线程所属的组只能通过线程的构造函数,所以,线程一旦被创建,那线程所属的组就永远不能被改变。下面是唯一的三个为线程指定线程组的方法。
- 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线程组。
我们可以通过getThreadGroup()来获取线程所属的线程组。
线程组有两个构造器,ThreadGroup(String name)和ThreadGroup(ThreadGroup parentGroup, String name)。这第一个构造器很容易理解,第二个构造器是创建一个输入parentGroup的子线程组,子线程组的名字为name。
线程组创建后不允许改名,只能通过getName()获取名字。
线程组有以下几个常用方法
- int activeGroup():返回此线程组中活动线程的数目。
- interrupt():中断该线程中的所有线程。
- isDaemo():判断该线程组是否是后台线程组。
- setDaemon(boolean daemon):把该线程组设置成后台线程组,当后台线程组的最后一个线程执行结束或被销毁后,后台线程自动销毁。
setMaxPriority(int pri):设置线程组的最高优先级。
5.4.1、使用UncaughtExceptionHandler捕获线程的异常
当创建一个线程以后,这个新创建的线程在执行过程中有可能发生异常。这种异常传统的try-catch无法捕捉到。比如下面的代码无法捕捉到线程1向上抛出的异常。
try{
Thread arrayException = new Thread(() -> {
int[] buffer = new int[3];
for (int i = 0; i < 5; i++) {
buffer[i] = 0;
}
}, "线程1");
arrayException.start();
} catch (Exception e) {
System.out.println("捕捉到执行过程中的异常");
}
try-catch只是捕捉执行的代码,上面lambda只是定义了线程1的执行,start()
也只是启动线程,他们都没有去执行for循环。那么只要定义执行没有语法错误,线程的启动没有发生异常,那上面的代码就不会捕捉到任何异常。
想要捕捉到线程1执行时抛出的异常就要利用UncaughtExceptionHandler
。
我们可以实现UncaughtExceptionHandler
中的uncaughtException(Thread t, Throwable e)
方法,在该方法中定义如何处理异常。其中,抛出异常的线程,e是线程抛出的异常。之后在线程启动前使用setUncaughtExceptionHandler(UncaughtExceptionHandler eh)
设置属于自己的异常处理器,当这个线程执行发生异常时就会异常处理器捕捉处理
下面看使用示例。
//定义线程
public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
if(e instanceof ArrayIndexOutOfBoundsException) {
System.out.println("捕获了" + t.getName() + "的数组越界异常");
}
if(e instanceof IllegalArgumentException) {
System.out.println("捕获了" + t.getName() + "的参数错误异常");
}
}
}
之后定义线程然后抛出异常,查看异常处理器是否捕捉到并处理。
ExceptionHandler exceptionHandler = new ExceptionHandler();
//数组越界异常捕获
Thread arrayException = new Thread(() -> {
int[] buffer = new int[3];
for (int i = 0; i < 5; i++) {
buffer[i] = 0;
}
}, "线程1");
//参数错误异常捕获
Thread argumentException = new Thread(() -> {
throw new IllegalArgumentException();
}, "线程2");
arrayException.setUncaughtExceptionHandler(exceptionHandler);
argumentException.setUncaughtExceptionHandler(exceptionHandler);
arrayException.start();
argumentException.start();
最后输出如下,成功捕获异常并处理。
不过,我们也只能捕捉到unchecked异常,checked异常要求方法写throws语句,但是线程的建立都要通过继承或者实现,无法自主在run()
方法上加上throws语句。