当前位置: 首页 > news >正文

多线程篇(基本认识 - 线程相关API)(持续更新迭代)

目录

前言

一、线程通知与等待

wait()

wait(long timeout)

wait(long timeout, int nanos)

二、线程唤醒

notify()

notifyAll()

生成汉堡包问题

吃货

桌子

演示

三、等待线程执行终止的join

1. 简介

2. 作用

3. join() & join(long timeout)

4. 方法源码

无参数方法

有参数方法

5. 注意点

注意点一:join阻塞的是当前线程,并不是join方法的线程对象对应的线程

注意点二:唤醒当前线程的操作是在JVM底层实现的,并没有显式调用notifyAll()方法

注意点三:当前线程A在进入到线程对象B的join方法中使获取了线程对象B的锁,在join内部调用wait()方法时又会释放掉线程对象B的锁,在线程B执行完后当前线程才会再次获取线程对象B的锁

注意点四:线程A调用线程对象B的join方法时,只有当此时线程对象已经被启动并且还没有执行完时才会起作用

6. 总结

四、让线程睡眠的sleep

五、让出CPU执行权的yield

六、设置线程优先级的priority

七、线程中断

1. 什么是中断线程

2. 判断线程是否被中断

3. 中断原理和中断线程用法的模板

4. 底层中断异常处理方式

5. 中断应用最佳实践

5.1. 使用中断信号量中断’非阻塞’状态的线程

5.2. 使用thread.interrupt()中断’非阻塞’状态线程

5.3. 使用thread.interrupt()中断阻塞状态线程

5.4. 死锁状态线程无法被中断

5.5. 中断I/O操作

6. 中断用法总结

6.1. 常见误区,interrupt一定能终止线程吗

6.2. jvm线程中断底层机制

八、守护线程与用户线程

1. 前言

2. 守护线程与用户线程的定义及区别

3. 守护线程的特点

4. 守护线程的创建

5. 守护线程与 JVM 的退出实验

6. 守护线程的作用及使用场

7. 总结

九、线程状态

1. 六种状态

2. 状态变迁

3. getState:获取线程状态

十、其它

1. currentThread():获取当前线程对象

2. getName():获取当前线程名称

3. setName:设置线程名称

4. start():启动线程

5. stop:停止线程

6. run:指定线程任务

十一、面试题

1. join和wait方法的区别是

2. 调用yield() 、sleep()、wait()、notify()等方法对锁有何影响?


前言

Java 中的 Object 类是所有类的父类,鉴于继承机制,Java 把所有类都需要的方法放到了Object

类里面,

一、线程通知与等待

wait()

当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事

情之一才返回:

  1. 其他线程调用了该共享对象的 notify()或者 notifyAll()方法;
  2. 其他线程调用了该线程的 interrupt() 方法,该线程抛出 InterruptedException 异常返回。

如果调用 wait() 方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛

出 IllegalMonitorStateException 异常。

那么一个线程如何才能获取一个共享变量的监视器锁呢?

1. 执行 synchronized 同步代码块时,使用该共享变量作为参数。

2. 调用该共享变量的方法,并且该方法使用了 synchronized 修饰。

另外需要注意的是,一个线程可以从挂起状态变为可以运行状态(也就是被唤醒),使该线程没有

被其他线程调用notify()、notifyAll() 方法进行通知,或者被中断,或者等待超时,这就是所谓的

虚假唤醒。

虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒的

条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait()方法进行防范。

退出循环的条件是满足了唤醒该线程的条件。

可以用一个简单的生产者和消费者作为例子。

例如,用 queue 为共享变量,生产者线程在调用 queue 的 wait() 方法前,使用 synchronized 关

键字拿到了该共享变量 queue 的监视器锁,所以调用 wait() 方法才不会抛出

IllegalMonitorStateException 异常。

如果当前队列没有空闲容量则会调用 queued 的 wait() 方法挂起当前线程,这里使用循环就是为

了避免上面说的虚假唤醒问题。

假如当前线程被虚假唤醒了,但是队列还是没有空余容量,那么当前线程还是会调用wait()方法

把自己挂起。

wait(long timeout)

该方法相比wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该

方法挂起后,没有在指定的timeout ms时间内被其他线程调用该共享变量的notify() 或者

notifyAll() 方法唤醒,那么该函数还是会因为超时而返回。

如果将 timeout 设置为 0 则和 wait 方法效果一样,因为在 wait 方法内部就是调用了wait(0)。

需要注意的是,如果在调用该函数时,传递了一个负的 timeout 则会抛出

IllegalArgumentException 异常。

wait(long timeout, int nanos)

Java Object 类

Object wait(long timeout, int nanos) 方法让当前线程处于等待(阻塞)状态,

直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过参数 timeout 与 nanos 设

置的超时时间。

该方法与 wait(long timeout) 方法类似,多了一个 nanos 参数,这个参数表示额外时间(以纳秒

为单位,范围是 0-999999)。

所以超时的时间还需要加上 nanos 纳秒。

如果 timeout 与 nanos 参数都为 0,则不会超时,会一直进行等待,类似于 wait() 方法。

当前线程必须是此对象的监视器所有者,否则还是会发生 IllegalMonitorStateException 异常。

如果当前线程在等待之前或在等待时被任何线程中断,则会抛出 InterruptedException 异常。

如果传递的参数不合法或 nanos 不在 0-999999 范围内,则会抛出 IllegalArgumentException 异

常。

语法

public final void wait(long timeout, int nanos)

参数

  • timeout - 等待时间(以毫秒为单位)。
  • nanos - 额外等待时间(以纳秒为单位)。

返回值

没有返回值。

实例

以下实例演示了 wait(long timeout, int nanos) 方法的使用:

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;public class RunoobTest extends Object {private List synchedList;public RunoobTest() {// 创建一个同步列表synchedList = Collections.synchronizedList(new LinkedList());}// 删除列表中的元素public String removeElement() throws InterruptedException {synchronized (synchedList) {// 列表为空就等待,等待  10 秒加上 500 纳秒while (synchedList.isEmpty()) {System.out.println("List is empty...");synchedList.wait(10000, 500);System.out.println("Waiting...");}String element = (String) synchedList.remove(0);return element;}}// 添加元素到列表public void addElement(String element) {System.out.println("Opening...");synchronized (synchedList) {// 添加一个元素,并通知元素已存在synchedList.add(element);System.out.println("New Element:'" + element + "'");synchedList.notifyAll();System.out.println("notifyAll called!");}System.out.println("Closing...");}public static void main(String[] args) {final RunoobTest demo = new RunoobTest();Runnable runA = new Runnable() {public void run() {try {String item = demo.removeElement();System.out.println("" + item);} catch (InterruptedException ix) {System.out.println("Interrupted Exception!");} catch (Exception x) {System.out.println("Exception thrown.");}}};Runnable runB = new Runnable() {// 执行添加元素操作,并开始循环public void run() {demo.addElement("Hello!");}};try {Thread threadA1 = new Thread(runA, "Google");threadA1.start();Thread.sleep(500);Thread threadA2 = new Thread(runA, "Runoob");threadA2.start();Thread.sleep(500);Thread threadB = new Thread(runB, "Taobao");threadB.start();Thread.sleep(1000);threadA1.interrupt();threadA2.interrupt();} catch (InterruptedException x) {}}
}

以上程序执行结果为:

List is empty...
List is empty...
Opening...
New Element:'Hello!'
notifyAll called!
Closing...
Waiting...
Waiting...
List is empty...
Hello!
Interrupted Exception!

二、线程唤醒

notify()

一个线程调用共享对象的 notify() 方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程。

一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。

此外,被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁

后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会

获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争

到了共享变量的监视器锁后才可以继续执行。

类似wait系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的

notify() 方法,否则会抛出 IllegalMonitorStateException 异常 。

notifyAll()

不同于在共享变量上调用 notify() 函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法

则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。

生成汉堡包问题

public class Cooker extends Thread {
//    生产者步骤:
//            1,判断桌子上是否有汉堡包
//    如果有就等待,如果没有才生产。
//            2,把汉堡包放在桌子上。
//            3,叫醒等待的消费者开吃。@Overridepublic void run() {while(true){synchronized (Desk.lock){if(Desk.count == 0){break;}else{if(!Desk.flag){//生产System.out.println("厨师正在生产汉堡包");Desk.flag = true;Desk.lock.notifyAll();}else{try {Desk.lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}}}
}

吃货

public class Foodie extends Thread {@Overridepublic void run() {
//        1,判断桌子上是否有汉堡包。
//        2,如果没有就等待。
//        3,如果有就开吃
//        4,吃完之后,桌子上的汉堡包就没有了
//                叫醒等待的生产者继续生产
//        汉堡包的总数量减一//套路://1. while(true)死循环//2. synchronized 锁,锁对象要唯一//3. 判断,共享数据是否结束. 结束//4. 判断,共享数据是否结束. 没有结束while(true){synchronized (Desk.lock){if(Desk.count == 0){break;}else{if(Desk.flag){//有System.out.println("吃货在吃汉堡包");Desk.flag = false;Desk.lock.notifyAll();Desk.count--;}else{//没有就等待//使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.try {Desk.lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}}}
}

桌子

public class Desk {//定义一个标记//true 就表示桌子上有汉堡包的,此时允许吃货执行//false 就表示桌子上没有汉堡包的,此时允许厨师执行public static boolean flag = false;//汉堡包的总数量public static int count = 10;//锁对象public static final Object lock = new Object();
}

演示

public class Demo {public static void main(String[] args) {/*消费者步骤:1,判断桌子上是否有汉堡包。2,如果没有就等待。3,如果有就开吃4,吃完之后,桌子上的汉堡包就没有了叫醒等待的生产者继续生产汉堡包的总数量减一*//*生产者步骤:1,判断桌子上是否有汉堡包如果有就等待,如果没有才生产。2,把汉堡包放在桌子上。3,叫醒等待的消费者开吃。*/Foodie f = new Foodie();Cooker c = new Cooker();f.start();c.start();}
}

三、等待线程执行终止的join

1. 简介

2. 作用

join()方法是Thread类中的一个方法,可以由线程对象调用。

在线程操作中,可以使用join()方法让一个线程强制运行,线程强制运行期间,其他线程无法运

行,必须等待此线程完成之后才可以继续执行,即等待线程执行终止。

在当前线程调用join()方法时并不需要提前自己写同步代码块来获取对象锁,调用了join()方法也

不会释放当前线程所持有的对象锁。

因为join方法在源码层面就是一个被synchronized修饰的同步方法,所以会进入到方法时会获取

到对象锁的,具体在后面的源码中会讲解。

适用场景:需要等待某几件事情完成后才能继续往下执行,比如多个线程加载资源,需要等待多

个线程全部加载完毕再汇总处理。

作用:当线程任务量大时,保证 main 线程在这些线程运行结束后再结束、可控制子线程间执行

顺序.

Java中如何让多线程按照自己指定的顺序执行?

这个问题最简单的回答是通过Thread.join来实现,Thread.join的作用之一是用来保证线程的顺序

性的。

下面这段代码演示了Thread.join的作用:

public class JoinDemo extends Thread{int i;Thread previousThread; //上一个线程public JoinDemo(Thread previousThread,int i){this.previousThread=previousThread;this.i=i;}@Overridepublic void run() {try {//调用上一个线程的join方法,大家可以自己演示的时候可以把这行代码注释掉previousThread.join(); } catch (InterruptedException e) {e.printStackTrace();}System.out.println("num:"+i);}public static void main(String[] args) {Thread previousThread=Thread.currentThread();for(int i=0;i<10;i++){JoinDemo joinDemo=new JoinDemo(previousThread,i);joinDemo.start();previousThread=joinDemo;}}
}

上面的代码,注意 previousThread.join部分,大家可以把这行代码注释以后看看运行效果,

在没有加join的时候运行的结果是不确定的。加了join以后,运行结果按照递增的顺序展示出来。

thread.join的含义是当前线程需要等待previousThread线程终止之后才从thread.join返回。

简单来说,就是线程没有执行完之前,会一直阻塞在join方法处。

再来一个例子:

在main方法中,创建启动了三个子线程,也就是说这三个线程是main线程的子线程,

如果就是正常的启动这三个线程,那么这三个线程的执行顺序是不固定的。

但是如果在每一个启动线程之间加上join()方法,如下:

main{thread1.start();thread1.join();thread2.start();thread2.join();thread3.start();thread3.join();
}

这个时候三个线程的执行顺序就固定了,变成了1,2,3.只有当调用join的线程执行完,

主线程才会被唤醒继续向下执行,也就达到了控制线程执行顺序的效果。

下面的图表现了join对于线程的作用:

3. join() & join(long timeout)

例子:

public static void main(String[] args){...thread1.join();thread2.join();System.out.println("all child thread over!");
}

主线程首先会在调用thread1.join() 后被阻塞,等待thread1执行完毕后,调用thread2.join(),等

待thread2 执行完毕(有可能),以此类推,最终会等所有子线程都结束后main函数才会返回。

如果其他线程调用了被阻塞线程的 interrupt() 方法,被阻塞线程会抛出 InterruptedException 异

常而返回。

通过上面的例子,我们就能知道join()方法是线程对象调用的,哪个线程对象调用这个方法,就

会让哪个线程强制执行,也就是除了这个调用join方法的线程对象对应的线程以外,其他线程都

被阻塞,等待这个被强制执行的线程执行完毕后,其他线程才能继续向下执行。

join()方法还提供可以传入时间参数的方法,即指定的等待时间内被join的线程还没执行完,就不

再等待,继续向下执行。

带参和不带参数方法的区别在于等待方式的不同:

  • 当调用join()方法时,当前线程会被阻塞,进入到WAITING状态(join方法就是在当前线程的环境下被另一个线程对象调用的),直到调用join方法的线程对象对应的线程执行完毕后才会继续执行。如果调用join方法的线程对象对应的线程已经执行完毕,那么当前线程会立即继续执行。
  • 当调用join(long timeout)方法时,当前线程也会被阻塞,进入到TIMED_WAITING状态,但是最多只会等待指定的时间(以毫秒为单位),如果等待的时间超过了指定的时间或者调用join方法的线程对象对应的线程已经执行完毕,那么当前线程会立即继续执行。

这里要注意,虽然是通过一个线程对象调用的join方法,但是这个join方法还是在当前线程的环境

下调用的,所以其实调用join方法的线程还是当前线程,并不是那个线程对象的线程调用的join方

法。

给出一个实例帮助理解:

public class JoinExample {private static final int TIMES = 100;private class JoinThread extends Thread {JoinThread(String name){super(name);}@Overridepublic void run() {for (int i = 0; i < TIMES; i++) {System.out.println(getName() + " " + i);}}}public static void main(String[] args) {JoinExample example = new JoinExample();example.test();}private void test() {for (int i = 0; i < TIMES; i++) {if (i == 20) {Thread jt1 = new JoinThread("子线程1");Thread jt2 = new JoinThread("子线程2");jt1.start();jt2.start();// main 线程调用了jt1线程和jt2线程的join()方法// main 线程必须等到 jt1和jt2线程 执行完之后才会向下执行try {// 需要等到jt1执行完成之后,才向下执行。在jt1没有执行完期间,其他线程无法运行。jt1.join();// 需要等到jt2执行完成之后,才向下执行。在jt2没有执行完期间,其他线程无法运行。jt2.join();// join还存在可以传入时间参数的方法:join(long mills) - 等待时间内被join的线程还没执行,就不再等待,继续向下执行} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + "  " + i);}}
}

4. 方法源码

上面的例子中主线程是如何被阻塞的?又是通过什么方法唤醒的呢?下面我们就通过源码来看看

Thread.join方法做了什么事情。

在底层源码不管是无参数,还是有参数的join都是使用的同一个带参源码方法。

无参数方法

    // 无参数join()方法public final void join() throws InterruptedException {// 不指定等待时间,就传入0,就会一直等待调用的线程执行完后才会继续向下执行join(0);}

有参数方法

join源码底层本质是利用当前线程的线程对象的 isAlive() 方法和 wait() 方法实现的。

源码如下:

// join方法向外抛出了异常,所以使用join方法需要在外层处理异常或者继续向上抛出异常
// 我们可以看到这个方法是被synchronized修饰的方法,假设当前线程是A,线程A执行了线程对象B的join方法,
// 那么线程A进入到join方法中,就会获取到对象B的对象锁,之所以要用synchronized修饰join方法,就是为了获取B的对象锁,
// 因为join方法底层是利用wait方法实现的,而调用某个对象的wait方法需要持有该对象的锁才行
// 下面的所有讲解就是按照假设当前线程是A,线程A执行了线程对象B的join方法的前提下讲的,此时join方法就是在线程对象B中
public final synchronized void join(long millis)throws InterruptedException {// 下面整个代码环境都是在对象B中的,所以里面直接调用的方法按照Java语法规则,只要没有指定方法所在的对象,那么就都是调用的当前所在对象中的方法,也就是线程对象B中的方法// 获取当前时间long base = System.currentTimeMillis();long now = 0;// 传参小于0,非法参数,抛出异常if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");}// 传入0超时时间意味着永远等待,直到B线程执行完毕if (millis == 0) {// 这个isAlive()是线程对象B中的方法// isAlive()方法检查的是调用该方法的线程对象对应的线程是否还在运行,也就是检查线程B(被等待线程)是否在还在运行while (isAlive()) {// 如果B线程还在执行,则当前线程A会调用wait()方法使自己阻塞(等待),直到被等待的B线程执行结束当前线程才会继续执行。// 下面这个wait放大,是线程对象B的wait方法,当前线程A执行了对象B的wait方法,使得当前线程阻塞等待。因为join方法被synchronized修饰,所以当前线程已经持有了对象B的锁了,才能调用B对象的wait方法// 传入0也表示没有等待期限,只有当某个线程调用了B对象的notify()/notifyAll()方法,才有可能会使在对象B上的等待队列中等待的线程A唤醒wait(0);}// 传入的超时时间不为0,意味着如果当前线程A等待了超过millis毫秒了,线程对象B对应的线程还没有执行完,那么也会自动被唤醒继续向下执行,不会一直等待了  } else {// 最多等待 millis 毫秒// isAlive()判断线程B是否执行完成while (isAlive()) {// 每一轮循环都会重新计算还剩下多少等待时间,用最多的等待时间减去当前的时间long delay = millis - now;// 如果delay小于等于0了,说明已经到了等待时间了,这个时候不管线程B是否执行完了,都直接跳出循环,后续会去唤醒当前线程Aif (delay <= 0) {break;}// 如果此时还没有超时并且线程B没有执行完,那么就当前线程A就继续调用线程对象B的wait方法,并且传入最多还要等待的时间,来使当前线程阻塞指定的时间wait(delay);// 每一轮循环都会获取一次当前的时间now = System.currentTimeMillis() - base;}}// join方法执行到最后,JVM底层会去执行让当前线程A唤醒的操作,源码中并没有显式调用notify()/notifyAll()方法,整个唤醒操作是在JVM底层实现的
}

join()方法是用于让一个线程等待另一个线程执行完毕的方法。

当一个线程A执行了另一个线程B的join()方法后,线程A将会被挂起,直到线程B执行完毕。

join()方法的底层实现原理是基于对象的wait()和notify()方法来实现的。当一个线程A执行另一个

线程B的join()方法时,线程A会进入等待状态(WAITING),线程B会运行直到执行完毕。当线

程B执行完毕后,JVM底层会自动调用对象的notifyAll()方法来通知所有等待在该对象上的线程,

包括线程A。此时,线程A会重新进入就绪状态(在Java线程中其实就是进入到RUNNABLE状

态),等待获取CPU资源继续向下执行。

5. 注意点

注意点一:join阻塞的是当前线程,并不是join方法的线程对象对应的线程

有很多人不理解join为什么阻塞的是当前线程,而不是调用join方法的线程对象对应的线程呢?

不理解的原因是阻塞当前线程A的wait()方法是放在线程对象B这个实例中被调用的,让大家误以

为应该阻塞B线程。但实际上当前线程会持有线程对象B的对象的锁(因为join方法使用

synchronized修饰的,所以当前线程也就获取了线程对象B的锁),在线程对象B调用的wait()方

法时,而这个wait()方法的调用者线程对象B是在当前线程环境中的。所以造成当前线程阻塞。

注意点二:唤醒当前线程的操作是在JVM底层实现的,并没有显式调用notifyAll()方法

为什么线程B执行完毕就能够唤醒当前线程呢?或者说是在什么时候唤醒的?

这里我们要注意一点,被等待的线程并不会真正地调用notifyAll()方法来唤醒其他等待线程,而是

由底层的JVM代码实现自动唤醒等待线程的功能。这个功能在底层被称为“monitor enter”和

“monitor exit”,是由JVM来负责管理的。具体实现细节比较复杂,但是对于Java开发者来说,只

需要知道在使用join()方法等待线程执行完毕时,等待的线程会被自动唤醒,不需要手动调用

notify()或notifyAll()方法。

在Thread类的join()方法的源码中,没有直接调用notify()或notifyAll()方法的代码。

但是,join()方法的底层实现确实是基于对象的wait()和notify()方法来实现的。

如果想要知道实现唤醒的具体细节,我们就得翻jdk的源码,但是如果大家对线程有一定的基本

了解的话,通过wait方法阻塞的线程,需要通过notify或者notifyall来唤醒。所以在线程执行完毕

以后会有一个唤醒的操作,只不过并不是显式调用,而是在JVM底层代码实现的。

接下来在hotspot的源码中找到 thread.cpp,看看被等待线程执行结束以后有没有做相关的事情

来证明我们的猜想

void JavaThread::exit(bool destroy_vm, ExitType exit_type) {assert(this == JavaThread::current(),  "thread consistency check");...// Notify waiters on thread object. This has to be done after exit() is called// on the thread (if the thread is the last thread in a daemon ThreadGroup the// group should have the destroyed bit set before waiters are notified).ensure_join(this); assert(!this->has_pending_exception(), "ensure_join should have cleared");...
}

观察一下 ensure_join(this)这行代码上的注释,唤醒处于等待的线程对象,

这个是在线程终止之后做的清理工作,这个方法的定义代码片段如下:

static void ensure_join(JavaThread* thread) {// We do not need to grap the Threads_lock, since we are operating on ourself.Handle threadObj(thread, thread->threadObj());assert(threadObj.not_null(), "java thread object must exist");ObjectLocker lock(threadObj, thread);// Ignore pending exception (ThreadDeath), since we are exiting anywaythread->clear_pending_exception();// Thread is exiting. So set thread_status field in  java.lang.Thread class to TERMINATED.java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);// Clear the native thread instance - this makes isAlive return false and allows the join()// to complete once we've done the notify_all below//这里是清除native线程,这个操作会导致isAlive()方法返回falsejava_lang_Thread::set_thread(threadObj(), NULL);lock.notify_all(thread);//注意这里// Ignore pending exception (ThreadDeath), since we are exiting anywaythread->clear_pending_exception();
}

ensure_join方法中,调用 lock.notify_all(thread); 唤醒所有等待thread锁的线程,意味着调用了join方法被阻塞

的主线程会被唤醒;也就是说当子线程执行完run方法之后,

底层在jvm源码里,会自动执行线程的exit方法,里面会调用notifyAll方法,唤醒所有的线程,而

这其中就包含了被阻塞的主线程。

注意点三:当前线程A在进入到线程对象B的join方法中使获取了线程对象B的锁,在join内部调用wait()方法时又会释放掉线程对象B的锁,在线程B执行完后当前线程才会再次获取线程对象B的锁

在Java中,每个对象都有一个相关联的锁,也称为监视器锁。当一个线程需要访问被该锁保护的

对象时,它必须先获得该锁的所有权。只有获得锁的线程才能调用wait()、notify()和notifyAll()方

法。因此,在join()方法中,使用了synchronized关键字来使当前线程获取到Thread对象的锁,

确保只有一个线程可以进入join()方法,避免出现竞态条件。

当前线程A调用线程对象B的join方法时,当前线程会尝试获取线程对象B的锁,获取对象B的锁成

功后,就会在join方法内部会调用对象B的wait方法,此时当前线程A就会释放线程对象B的锁,

使得线程A进入等待状态。此时线程B就会又获取到线程对象B的锁,当线程对B执行完毕后,

JVM底层会调用notifyAll方法唤醒所有等待线程,包括线程A,底层调用了唤醒方法后,线程B又

会释放掉线程对象B的锁,此时线程A会重新获取线程对象B的锁(因为是线程A调用的线程对象

B的join方法,join方法又是被synchronized修饰的方法,如果线程A唤醒后不再次持有对象B的

锁,就没有办法继续在join方法内部继续向下执行了),然后继续执行后面的代码。

注意点四:线程A调用线程对象B的join方法时,只有当此时线程对象已经被启动并且还没有执行完时才会起作用

当一个线程对象被创建后,调用该线程对象的start()方法可以启动该线程,线程开始执行。

而当一个线程在执行过程中调用另一个线程对象的join()方法时,会让该线程执行完毕后再继续

执行当前线程。

start()方法会创建一个新的线程,并且让这个新线程执行run()方法中的代码。如果没有调用

start()方法,那么线程对象B只是一个普通的对象,并没有对应的线程。如果线程对象还没有执

行start()方法,那么在当前线程中调用该线程对象的join()方法不会有任何效果,因为线程还没有

开始执行。这种情况下当前线程调用另一个线程对象的join()方法会使当前线程进入等待状态,

直到被等待的线程结束或者超时,但是被等待的线程并没有开始执行,甚至都不存在这个线程,

所以当前线程会一直处于等待状态,直到超时或者被中断。

还有一点需要注意的是,当线程A执行join()方法等待线程B执行完毕时,如果线程B已经执行完毕

了,那么线程A并不会阻塞,而是直接退出join()方法,继续执行下面的代码。

所以,在使用join()方法之前,一定要确保调用了start()方法来启动相应的线程。

以上join()方法的简单实现原理。实际上,Java虚拟机在实现中还考虑了许多细节,以确保线程

的正确协作和优化执行效率。

6. 总结

  • 当一个线程对象调用了join方法后,它就会让当前线程(即调用join方法的线程)进入等待状态,直到这个线程对象对应的线程执行完毕为止。
  • join方法的底层实现原理是基于对象的wait和notify方法来实现的。在当前线程的环境下当线程对象调用了join方法时,当前线程会获取这个对象的锁,并且调用这个对象的wait方法来使当前线程阻塞等待。当这个对象对应的线程执行完毕后,JVM底层会调用notifyAll方法来唤醒所有等待在这个对象上的线程,包括当前线程。这个唤醒过程是在JVM底层实现的,作为用户的我们看不到显式的调用唤醒方法的代码。
  • 源码中有相关的体现,可以参考Thread类中join和isAlive方法以及JVM中ensure_join和lock.notify_all方法(就是JVM中的这两个方法实现了join方法中唤醒当前线程的操作)。

四、让线程睡眠的sleep

Thread类中有一个静态的 sleep 方法,当一个执行中的线程调用了 Thread 的 sleep 方法后,调

用线程会暂时让出指定时间的执行权,也就是在这期间不参与 CPU 的调度,但是该线程所拥有

的监视器资源,比如锁还是持有不让出的。

指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与 CPU 的调度,获取

到 CPU 资源后就可以继续运行了。

如果在睡眠期间其他线程调用了该线程的 interrupt()方法中断了该线程,则该线程会在调用 sleep

方法的地方抛出 InterruptedException 异常而返回。

简单来说:sleep被称为让线程休眠,即让当前线程进入休眠,进入“阻塞”状态,放弃占有CPU

时间片,让给其他线程使用

五、让出CPU执行权的yield

Thread 类中有一个静态的 yield 方法,当一个线程调用 yield 方法时,实际就是在暗示线程调度

器当前线程请求让出自己的 CPU 使用,但是线程调度器可以无条件忽略这个暗示。

我们知道操作系统是为每个线程分配一个时间片来占有 CPU 的,正常情况下当一个线程把分配

给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了

Thread类的静态方法 yield 时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自

己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。

当一个线程调用 yield方法时,当前线程会让出 CPU 使用权,然后处于就绪状态,线程调度器会

从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出 CPU 的

那个线程来获取 CPU执行权。

注意:让出时间片,不会释放锁

六、设置线程优先级的priority

我们知道线程调度有两种调度模型:

① 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片

② 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程优先级相同,那么会随机选择

一个,优先级高的线程获取CPU时间片相对多一些

线程的优先级(priority) ,无非就是是一个常量,

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,

线程调度器按照优先级决定按照优先级决定应该调度哪个线程来执行

线程的优先级用数字表示,范围从1-10,

线程优先级高的资源会多一些, 但是线程优先级高并不一定先执行,但是权重就大了,比如1张彩票

和10张彩票,中奖率就会提高

三个常量值:

Thread.MIN_PRIORITY = 1;

Thread.MAX_PRIORITY = 10;

Thread.NORM_PRIORITY = 5;

获取/更改线程优先级:

getPriority()

setPriority(int xxx)

线程优先级使用:

不可以将优先级设置负数或者大于10的数 只能1-10 不然会报错,我们需要要先设置线程优先级

再启动线程

如果哪行代码比较重要,我们就可恶意把它的优先级提升,优先级高并不是每次都会优先执行,只

是权重大了些,更加容易先执行,具体要看cpu的调度,优先级低只是意味着调度的概率低,并不是

优先级低就不会被调用了。

七、线程中断

1. 什么是中断线程

线程的 thread.interrupt() 方法是中断线程,将会设置该线程的中断状态位,即设置为true,中断

的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。

线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。

它并不像stop方法那样会中断一个正在运行的线程。

2. 判断线程是否被中断

判断某个线程是否已被发送过中断请求,请使用 Thread.currentThread().isInterrupted() 方法

(因为它将线程中断标示位设置为true后,不会立刻清除中断标示位,即不会将中断标设置为

false),而不要使用Thread.interrupted()(静态方法,该方法调用后会将中断标示位清除,即重

新设置为false)方法来判断,下面是线程在循环中时的中断方式:

while(!Thread.currentThread().isInterrupted() && more work to do){do more work
}

3. 中断原理和中断线程用法的模板

如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait、1.5中的

condition.await、以及可中断的通道上的 I/O 操作方法后可进入阻塞状态),则在线程在检查中

断标示时如果发现中断标示为true,则会在这些阻塞方法(sleep、join、wait、1.5中的

condition.await及可中断的通道上的 I/O 操作方法)调用处抛出InterruptedException异常,并且

在抛出异常后立即将线程的中断标示位清除,即重新设置为false。抛出异常是为了线程从阻塞状

态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。

注,synchronized在获锁的过程中是不能被中断的,意思是说如果产生了死锁,则不可能被中断

(请参考后面的测试例子)。与 synchronized 功能相似的reentrantLock.lock()方法也是一样,

它也不可中断的,即如果发生死锁,那么reentrantLock.lock()方法无法终止,如果调用时被阻

塞,则它一直阻塞到它获取到锁为止。

但是如果调用带超时的tryLock方法reentrantLock.tryLock(long timeout, TimeUnit unit),那么如

果线程在等待时被中断,将抛出一个InterruptedException异常,这是一个非常有用的特性,因为

它允许程序打破死锁。你也可以调用 reentrantLock.lockInterruptibly() 方法,它就相当于一个超

时设为无限的tryLock方法。

没有任何语言方面的需求一个被中断的线程应该终止。中断一个线程只是为了引起该线程的注

意,被中断线程可以决定如何应对中断。

某些线程非常重要,以至于它们应该不理会中断,而是在处理完抛出的异常之后继续执行,但是

更普遍的情况是,一个线程将把中断看作一个终止请求,这种线程的run方法遵循如下形式:

try catch在while外层的场景处理模板:

public void run() {while (!Thread.currentThread().isInterrupted()&& more work to do) {try {...sleep(delay);} catch (InterruptedException e) {//重新设置中断标示,如果后续地方用不到中止状态标识也可以不用再设置Thread.currentThread().interrupt();}}
}

如果异常在循环体的内部,由于触发的中断异常会清除标识,此时如果没有主动的break语句退

出循环并且后续的代码会有用到状态标识的地方,比如想让程序再执行一些清理操作后,再由上

层的while判断语句触发结束线程,会失效。

4. 底层中断异常处理方式

另外不要在你的底层代码里捕获InterruptedException异常后不处理,会处理不当,如下:

void mySubTask(){...try{sleep(delay);}catch(InterruptedException e){}//不要这样做//一旦底层的代码trycatch了异常,导致上层无法感知...
}

如果你不知道抛InterruptedException异常后如何处理,那么你有如下好的建议处理方式:

1、在catch子句中,调用Thread.currentThread.interrupt()来设置中断状态(因为抛出异常后中

断标示会被清除),让外界通过判断Thread.currentThread().isInterrupted()标示来决定是否终止

线程还是继续下去,应该这样做:

void mySubTask() {...try {sleep(delay);} catch (InterruptedException e) {Thread.currentThread().isInterrupted();}...
}

2、或者,更好的做法就是,不使用try来捕获这样的异常,让方法直接抛出:

void mySubTask() throws InterruptedException {...sleep(delay);...
}

5. 中断应用最佳实践

5.1. 使用中断信号量中断’非阻塞’状态的线程

中断线程最好的,最受推荐的方式是,使用共享变量(shared variable)发出信号,告诉线程必

须停止正在运行的任务。线程必须周期性的核查这一变量,然后有秩序地中止任务。

Example2描述了这一方式:

class Example2 extends Thread {volatile boolean stop = false;// 线程中断信号量public static void main(String args[]) throws Exception {Example2 thread = new Example2();System.out.println("Starting thread...");thread.start();Thread.sleep(3000);System.out.println("Asking thread to stop...");// 设置中断信号量thread.stop = true;Thread.sleep(3000);System.out.println("Stopping application...");}public void run() {// 每隔一秒检测一下中断信号量while (!stop) {System.out.println("Thread is running...");long time = System.currentTimeMillis();/** 使用while循环模拟 耗时 方法,这里不要使用sleep,否则在阻塞时会 抛* InterruptedException异常而退出循环,这样while检测stop条件就不会执行,* 失去了意义。*/while ((System.currentTimeMillis() - time < 1000)) {}//[code1]}System.out.println("Thread exiting under request...");}
}

适应于非阻塞状态的线程的常见做法[code1]处如果是阻塞的代码,例如sleep或wait,会导致即

使修改了stop的状态标识,但是由于线程阻塞了,永远不会执行到while循环体的判断条件语句,

线程也就不会终止掉。这也就是interrupt()的意义,3.3 例子正好演示了阻塞状态下的终止语法。

5.2. 使用thread.interrupt()中断’非阻塞’状态线程

和3.1是同一个场景,只不过不需要额外增加stop标识,直接利用中断标识

虽然Example2该方法要求一些编码,但并不难实现。同时,它给予线程机会进行必要的清理工

作。这里需注意一点的是需将共享变量定义成volatile 类型或将对它的一切访问封入同步的块/方

法(synchronizedblocks/methods)中。

上面是中断一个非阻塞状态的线程的常见做法,但对非检测isInterrupted()条件会更简洁:

class Example2 extends Thread {public static void main(String args[]) throws Exception {Example2 thread = new Example2();System.out.println("Starting thread...");thread.start();Thread.sleep(3000);System.out.println("Asking thread to stop...");// 发出中断请求thread.interrupt();Thread.sleep(3000);System.out.println("Stopping application...");}public void run() {// 每隔一秒检测是否设置了中断标示while (!Thread.currentThread().isInterrupted()) {System.out.println("Thread is running...");long time = System.currentTimeMillis();// 使用while循环模拟 sleepwhile ((System.currentTimeMillis() - time < 1000) ) {}}System.out.println("Thread exiting under request...");}
}

5.3. 使用thread.interrupt()中断阻塞状态线程

Thread.interrupt()方法不会中断一个正在运行的线程。这一方法实际上完成的是,设置线程的中断标示位,在线

程受到阻塞的地方(如调用sleep、wait、join等地方)抛出一个异常InterruptedException,并且中断状态也将

被清除,这样线程就得以退出阻塞的状态。

下面是具体实现:

class Example3 extends Thread {public static void main(String args[]) throws Exception {Example3 thread = new Example3();System.out.println("Starting thread...");thread.start();Thread.sleep(3000);System.out.println("Asking thread to stop...");thread.interrupt();// 等中断信号量设置后再调用Thread.sleep(3000);System.out.println("Stopping application...");}public void run() {while (!Thread.currentThread().isInterrupted()) {System.out.println("Thread running...");try {/** 如果线程阻塞,将不会去检查中断信号量stop变量,所 以thread.interrupt()* 会使阻塞线程从阻塞的地方抛出异常,让阻塞线程从阻塞状态逃离出来,并* 进行异常块进行 相应的处理*/Thread.sleep(1000);// 线程阻塞,如果线程收到中断操作信号将抛出异常} catch (InterruptedException e) {System.out.println("Thread interrupted...");/** 如果线程在调用 Object.wait()方法,或者该类的 join() 、sleep()方法* 过程中受阻,则其中断状态将被清除*/System.out.println(this.isInterrupted());// false//中不中断由自己决定,如果需要真真中断线程,则需要重新设置中断位,如果//不需要,则不用调用Thread.currentThread().interrupt();}}System.out.println("Thread exiting under request...");}
}

一旦Example3中的Thread.interrupt()被调用,线程便收到一个异常,于是逃离了阻塞状态并确定

应该停止。

上面我们还可以使用共享信号量来替换!Thread.currentThread().isInterrupted()条件,但不如它

简洁。

5.4. 死锁状态线程无法被中断

Example4试着去中断处于死锁状态的两个线程,但这两个线都没有收到任何中断信号(抛出异

常),所以 interrupt() 方法是不能中断死锁线程的,因为锁定的位置根本无法抛出异常:

class Example4 extends Thread {public static void main(String args[]) throws Exception {final Object lock1 = new Object();final Object lock2 = new Object();Thread thread1 = new Thread() {public void run() {deathLock(lock1, lock2);}};Thread thread2 = new Thread() {public void run() {// 注意,这里在交换了一下位置deathLock(lock2, lock1);}};System.out.println("Starting thread...");thread1.start();thread2.start();Thread.sleep(3000);System.out.println("Interrupting thread...");thread1.interrupt();thread2.interrupt();Thread.sleep(3000);System.out.println("Stopping application...");}static void deathLock(Object lock1, Object lock2) {try {synchronized (lock1) {Thread.sleep(10);// 不会在这里死掉synchronized (lock2) {// 会锁在这里,虽然阻塞了,但不会抛异常System.out.println(Thread.currentThread());}}} catch (InterruptedException e) {e.printStackTrace();System.exit(1);}}
}

5.5. 中断I/O操作

然而,如果线程在I/O操作进行时被阻塞,又会如何?

I/O操作可以阻塞线程一段相当长的时间,特别是牵扯到网络应用时。

例如,服务器可能需要等待一个请求(request),又或者,一个网络应用程序可能要等待远端

主机的响应。

实现此InterruptibleChannel接口的通道是可中断的:如果某个线程在可中断通道上因调用某个阻

塞的 I/O 操作(常见的操作一般有这些:serverSocketChannel. accept()、

socketChannel.connect、socketChannel.open、socketChannel.read、socketChannel.write、

fileChannel.read、fileChannel.write)而进入阻塞状态,而另一个线程又调用了该阻塞线程的

interrupt 方法,这将导致该通道被关闭,并且已阻塞线程接将会收到

ClosedByInterruptException,并且设置已阻塞线程的中断状态。

另外,如果已设置某个线程的中断状态并且它在通道上调用某个阻塞的 I/O 操作,则该通道将关

闭并且该线程立即接收到 ClosedByInterruptException;并仍然设置其中断状态。如果情况是这

样,其代码的逻辑和第三个例子中的是一样的,只是异常不同而已。

如果你正使用通道(channels)(这是在Java 1.4中引入的新的I/O API),那么被阻塞的线程将

收到一个ClosedByInterruptException异常。但是,你可能正使用Java1.0之前就存在的传统的

I/O,而且要求更多的工作。既然这样,Thread.interrupt()将不起作用,因为线程将不会退出被阻

塞状态。Example5描述了这一行为。

尽管interrupt()被调用,线程也不会退出被阻塞状态,比如ServerSocket的accept方法根本不抛

出异常。

很幸运,Java平台为这种情形提供了一项解决方案,即调用阻塞该线程的套接字的close()方法。

在这种情形下,如果线程被I/O操作阻塞,当调用该套接字的close方法时,该线程在调用accept

地方法将接收到一个SocketException(SocketException为IOException的子异常)异常,这与使

用interrupt()方法引起一个InterruptedException异常被抛出非常相似,(注,如果是流因读写阻

塞后,调用流的close方法也会被阻塞,根本不能调用,更不会抛IOExcepiton,此种情况下怎样

中断?

我想可以转换为通道来操作流可以解决,比如文件通道)。

下面是具体实现:

class Example6 extends Thread {volatile ServerSocket socket;public static void main(String args[]) throws Exception {Example6 thread = new Example6();System.out.println("Starting thread...");thread.start();Thread.sleep(3000);System.out.println("Asking thread to stop...");Thread.currentThread().interrupt();// 再调用interrupt方法thread.socket.close();// 再调用close方法try {Thread.sleep(3000);} catch (InterruptedException e) {}System.out.println("Stopping application...");}public void run() {try {socket = new ServerSocket(8888);} catch (IOException e) {System.out.println("Could not create the socket...");return;}while (!Thread.currentThread().isInterrupted()) {System.out.println("Waiting for connection...");try {socket.accept();} catch (IOException e) {System.out.println("accept() failed or interrupted...");Thread.currentThread().interrupt();//重新设置中断标示位}}System.out.println("Thread exiting under request...");}
}

6. 中断用法总结

一、没有任何语言方面的需求一个被中断的线程应该终止

中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断。

中断但不一定要正真的终止,上层代码可以决定终止或继续运行

二、中断标识重置问题,有2种场景会导致true变为false的情况

  • 对于处于sleep,join等操作的线程,如果被调用interrupt()后,会抛出InterruptedException,然后线程的中断标志位会由true重置为false,因为线程为了处理异常已经重新处于就绪状态。底层代码如果catch住InterruptedException,会重置标识为fasle,记得重新设置中断标识true,否则出现意想不到的错误
  • 判断某个线程是否已被发送过中断请求,请使用 Thread.currentThread().isInterrupted() 方法(因为它将线程中断标示位设置为true后,不会立刻清除中断标示位,即不会将中断标设置为false),而不要使用 Thread.interrupted()(该方法调用后会将中断标示位清除,即重新设置为false)方法来判断
//建议使用该方法,实例方法
Thread.currentThread().isInterrupted()
//不建议使用该方法,静态方法
Thread.interrupted()

三、不可中断的操作,包括进入synchronized段以及Lock.lock(),inputSteam.read()等,

调用interrupt()对于这几个问题无效,因为它们都不抛出中断异常。如果拿不到资源,它们会无

限期阻塞下去。

对于Lock.lock(),可以改用Lock.lockInterruptibly(),可被中断的加锁操作,它可以抛出中断异

常。等同于等待时间无限长的Lock.tryLock(long time, TimeUnit unit)。

对于inputStream等资源,有些(实现了interruptibleChannel接口)可以通过close()方法将资源关

闭,对应的阻塞也会被放开。

6.1. 常见误区,interrupt一定能终止线程吗

答案是否定的

首先,看看Thread类里的几个方法:

方法

描述

public static boolean interrupted

测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false。

public boolean isInterrupted()

测试线程是否已经中断。线程的中断状态不受该方法的影响。

public void interrupt()

中断线程。

上面列出了与中断有关的几个方法及其行为,可以看到interrupt是中断线程。

如果不了解Java的中断机制,这样的一种解释极容易造成误解,认为调用了线程的interrupt方法

就一定会中断线

其实,Java的中断是一种协作机制。也就是说调用线程对象的interrupt方法并不一定就中断了正

在运行的线程,它只是要求线程自己在合适的时机中断自己。每个线程都有一个boolean的中断

状态(这个状态不在Thread的属性上),interrupt方法仅仅只是将该状态置为true。

比如对正常运行的线程调用interrupt()并不能终止他,只是改变了interrupt标示符。

一般说来,如果一个方法声明抛出InterruptedException,表示该方法是可中断的,比如

wait,sleep,join,也就是说可中断方法会对interrupt调用做出响应(例如sleep响应interrupt的操作

包括清除中断状态,抛出InterruptedException),异常都是由可中断方法自己抛出来的,并不是

直接由interrupt方法直接引起的。

Object.wait, Thread.sleep方法,会不断的轮询监听 interrupted 标志位,发现其设置为true后,

会停止阻塞并抛出InterruptedException异常。

只有阻塞时,才能真正的响应中断,如果没有阻塞,就不会响应中断

6.2. jvm线程中断底层机制

看了以上的说明,对java中断的使用肯定是会了,但我想知道的是阻塞了的线程是如何通过

interuppt方法完成停止阻塞并抛出interruptedException的,这就要看 Thread 中 native 的

interuppt0 方法了。

第一步学习Java的JNI调用Native方法。

第二步下载openjdk的源代码,找到目录结构里的

openjdk-src\jdk\src\share\native\java\lang\Thread.c文件。

#include "jni.h"
#include "jvm.h"#include "java_lang_Thread.h"#define THD "Ljava/lang/Thread;"
#define OBJ "Ljava/lang/Object;"
#define STE "Ljava/lang/StackTraceElement;"#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))static JNINativeMethod methods[] = {{"start0",           "()V",        (void *)&JVM_StartThread},{"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},{"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},{"suspend0",         "()V",        (void *)&JVM_SuspendThread},{"resume0",          "()V",        (void *)&JVM_ResumeThread},{"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},{"yield",            "()V",        (void *)&JVM_Yield},{"sleep",            "(J)V",       (void *)&JVM_Sleep},{"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},{"countStackFrames", "()I",        (void *)&JVM_CountStackFrames},{"interrupt0",       "()V",        (void *)&JVM_Interrupt},{"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},{"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},{"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},{"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
};#undef THD
#undef OBJ
#undef STE
JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}

八、守护线程与用户线程

1. 前言

本节内容主要是对守护线程与用户线程进行深入的讲解,具体内容点如下:

  • 了解守护线程与用户线程的定义及区别,使我们学习本节内容的基础知识点;
  • 了解守护线程的特点,是我们掌握守护线程的第一步;
  • 掌握守护线程的创建,是本节内容的重点;
  • 通过守护线程与 JVM 的退出实验,更加深入的理解守护线程的地位以及作用,为本节内容次重点;
  • 了解守护线程的作用及使用场景,为后续开发过程中提供守护线程创建的知识基础。

2. 守护线程与用户线程的定义及区别

Java 中的线程分为两类,分别为 daemon 线程(守护线程〉和 user 线程(用户线程)

在 JVM 启动时会调用 main 函数, main 函数所在的线程就是一个用户线程,

其实在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。

守护线程定义:所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程。比如垃

圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。

因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。

反过来说,只要任何非守护线程还在运行,程序就不会终止。

用户线程定义:某种意义上的主要用户线程,只要有用户线程未执行完毕,JVM 虚拟机不会退

出。

区别: 在本质上,用户线程和守护线程并没有太大区别,唯一的区别就是当最后一个非守护线

程结束时,JVM 会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影

响 JVM 的退出。

言外之意,只要有一个用户线程还没结束, 正常情况下 JVM 就不会退出。

3. 守护线程的特点

Java 中的守护线程和 Linux 中的守护进程是有些区别的,Linux 守护进程是系统级别的,当系统

退出时,才会终止。而 Java 中的守护线程是 JVM 级别的,当 JVM 中无任何用户进程时,守护

进程销毁,JVM 退出,程序终止。

总结来说,Java 守护进程的最主要的特点有:

  • 守护线程是运行在程序后台的线程;
  • 守护线程创建的线程,依然是守护线程;
  • 守护线程不会影响 JVM 的退出,当 JVM 只剩余守护线程时,JVM 进行退出;
  • 守护线程在 JVM 退出时,自动销毁。

4. 守护线程的创建

创建细节:

  • thread.setDaemon (true) 必须在 thread.start () 之前设置,否则会跑出一个 llegalThreadStateException 异常。你不能把正在运行的常规线程设置为守护线程;
  • 在 Daemon 线程中产生的新线程也是 Daemon 的;
  • 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

线程创建代码示例:

public class DemoTest {public static void main(String[] args) throws InterruptedException {Thread threadOne = new Thread(new Runnable() {@Overridepublic void run() {//代码执行逻辑}});threadOne.setDaemon(true); //设置threadOne为守护线程threadOne. start();}
}

5. 守护线程与 JVM 的退出实验

为了更好的了解守护线程与 JVM 是否退出的关系,

我们首先来设计一个守护线程正在运行,但用户线程执行完毕导致的 JVM 退出的场景。

场景设计:

  • 创建 1 个线程,线程名为 threadOne;
  • run 方法线程 sleep 1000 毫秒后,进行求和计算,求解 1 + 2 + 3 + … + 100 的值;
  • 将线程 threadOne 设置为守护线程;
  • 执行代码,最终打印的结果;
  • 加入 join 方法,强制让用户线程等待守护线程 threadOne;
  • 执行代码,最终打印的结果。

期望结果:

  • 未加入 join 方法之前,threadOne 不能执行求和逻辑,无打印输出,因为 main 函数线程执行完毕后,JVM 退出,守护线程也就随之死亡,无打印结果;
  • 加入 join 方法后,可以打印求和结果,因为 main 函数线程需要等待 threadOne 线程执行完毕后才继续向下执行,main 函数执行完毕,JVM 退出。

Tips:main 函数就是一个用户线程,main 方法执行时,

只有一个用户线程,如果 main 函数执行完毕,用户线程销毁,

JVM 退出,此时不会考虑守护线程是否执行完毕,直接退出。

代码实现 - 不加入 join 方法:

public class DemoTest {public static void main(String[] args){Thread threadOne = new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}int sum = 0;for (int i = 1; i  <= 100; i++) {sum = sum + i;}System.out.println("守护线程,最终求和的值为: " + sum);}});threadOne.setDaemon(true); //设置threadOne为守护线程threadOne. start();System.out.println("main 函数线程执行完毕, JVM 退出。");}
}

执行结果验证:

main 函数线程执行完毕, JVM 退出。

从结果上可以看到,JVM 退出了,守护线程还没来得及执行,也就随着 JVM 的退出而消亡了。

代码实现 - 加入 join 方法:

public class DemoTest {public static void main(String[] args){Thread threadOne = new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}int sum = 0;for (int i = 1; i  <= 100; i++) {sum = sum + i;}System.out.println("守护线程,最终求和的值为: " + sum);}});threadOne.setDaemon(true); //设置threadOne为守护线程threadOne. start();try {threadOne.join(); // 加入join 方法} catch (InterruptedException e) {e.printStackTrace();}System.out.println("main 函数线程执行完毕, JVM 退出。");}
}

执行结果验证:

守护线程,最终求和的值为: 5050

main 函数线程执行完毕, JVM 退出。

从结果来看,守护线程不决定 JVM 的退出,除非强制使用 join 方法使用户线程等待守护线程的

执行结果,但是实际的开发过程中,这样的操作是不允许的,因为守护线程,默认就是不需要被

用户线程等待的,是服务于用户线程的。

6. 守护线程的作用及使用场

作用:我们以 GC 垃圾回收线程举例,它就是一个经典的守护线程,当我们的程序中不再有任何

运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程时JVM上仅剩的线程时,

垃圾回收线程会自动离开。

它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

应用场景:

  • 为其它线程提供服务支持的情况,可选用守护线程;
  • 根据开发需求,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;
  • 如果一个正在执行某个操作的线程必须要执行完毕后再释放,否则就会出现不良的后果的话,那么这个线程就不能是守护线程,而是用户线程;
  • 正常开发过程中,一般心跳监听,垃圾回收,临时数据清理等通用服务会选择守护线程。

7. 总结

掌握用户线程和守护线程的区别点非常重要,在实际的工作开发中,对一些服务型,通用型的线

程服务可以根据需要选择守护线程进行执行,这样可以减少 JVM 不可退出的现象,并且可以更

好地协调不同种类的线程之间的协作,减少守护线程对高优先级的用户线程的资源争夺,使系统

更加的稳定。

本节的重中之重是掌握守护线程的创建以及创建需要注意的事项,了解守护线程与用户线程的区

别使我们掌握守护线程的前提。

九、线程状态

1. 六种状态

  1. NEW:初始状态,线程被创建,但是还没有调用 start()方法
  2. RUNNABLE:运行状态,Java将操作系统中的就绪和运行两种状态笼统地称作“运行中”
  3. BLOCKED:阻塞状态,表示线程阻塞于锁
  4. WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
  5. TIME_WAITING:超时等待状态,该状态不同于WAIITING,它是可以在指定的时间自行返回的
  6. TERMINATED:终止状态,表示当前线程已经执行完毕

我们可以通过线程对象的getState()函数获取线程状态

2. 状态变迁

3. getState:获取线程状态

十、其它

1. currentThread():获取当前线程对象

返回代码段正在被哪个线程调用的信息

2. getName():获取当前线程名称

返回代码段正在被哪个线程调用的线程名称

3. setName:设置线程名称

可以动态设置线程名称

4. start():启动线程

start()方法用来启动一个线程(也就是开启多线程模式)

此时线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法!

5. stop:停止线程

强制停止一个正在运行的线程,无论此时线程是何种状态,不安全,

例如:造成数据不完整性问题!

例如:利用字符缓冲输出流写数据到指定文件,数据未写完,线程就已终止,造成数据不完整

性,代码已演示!

6. run:指定线程任务

run 方法只是 thread 的一个普通方法调用时,还是在主线程里执行,还是一条执行路径,单线程

运行,run()方法中,线程调用start()方法自动调用 run()方法时,此时多线程开启,这是由 jvm 的

内存机制规定的,且 run()方法必须是 public 访问权

十一、面试题

1. join和wait方法的区别是

  • wait方法会让当前线程释放对象锁,并进入等待状态,直到被其他线程唤醒或者超时时间到达。
  • join方法不会让当前线程释放对象锁,而是让当前线程进入到等待状态等待目标线程执行完毕或者超时时间到达。

2. 调用yield() 、sleep()、wait()、notify()等方法对锁有何影响?

yield:让出时间片,不会释放锁

sleep:线程进入睡眠状态,不会释放锁

wait:调动方法之前,必须要持有锁。调用了wait()方法以后,锁就会被释放,进入锁的等待队

列,方法返回后重新拿到锁

notify:调动方法之前,必须要持有锁,调用notify()方法本身不会释放锁的。

而是通知等待队列中的某一个线程,同步代码块执行完毕后才会释放锁

notifyAll:同notify,有一点不同在于,notifyAll会发出n个信号(n=等待线程数),

而notify只会发出一个信号,通常情况下,尽量选择notifyAll

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 数学建模笔记(四):熵权
  • 排序算法-堆排序
  • 甲方(北汽)渗透测试面试经验分享
  • Nginx: 负载均衡场景下上游服务器异常时的容错机制
  • github访问加速项目@一键部署自动更改host修改加速Github访问
  • k8s调度器Scheduler
  • Lodash——JavaScript中的工具库
  • buuctf [MRCTF2020]hello_world_go
  • 速盾:服务器接入cdn后上传图片失败怎么解决?
  • 主控和从控!!!
  • (二) 初入MySQL 【数据库管理】
  • C语言试题(含答案解析)
  • 发布npm包到GitLab教程
  • 树莓派+艺术品,有没有搞头?
  • 网络安全之DC-1靶机渗透实验
  • [ JavaScript ] 数据结构与算法 —— 链表
  • 【跃迁之路】【519天】程序员高效学习方法论探索系列(实验阶段276-2018.07.09)...
  • CODING 缺陷管理功能正式开始公测
  • SpringCloud集成分布式事务LCN (一)
  • Vue实战(四)登录/注册页的实现
  • 从 Android Sample ApiDemos 中学习 android.animation API 的用法
  • 动手做个聊天室,前端工程师百无聊赖的人生
  • 搞机器学习要哪些技能
  • 让你的分享飞起来——极光推出社会化分享组件
  • 数据库写操作弃用“SELECT ... FOR UPDATE”解决方案
  • 网络应用优化——时延与带宽
  • 微信开放平台全网发布【失败】的几点排查方法
  • 项目实战-Api的解决方案
  • 1.Ext JS 建立web开发工程
  • ​DB-Engines 11月数据库排名:PostgreSQL坐稳同期涨幅榜冠军宝座
  • # Apache SeaTunnel 究竟是什么?
  • #define MODIFY_REG(REG, CLEARMASK, SETMASK)
  • #if和#ifdef区别
  • #include到底该写在哪
  • #Linux杂记--将Python3的源码编译为.so文件方法与Linux环境下的交叉编译方法
  • #在线报价接单​再坚持一下 明天是真的周六.出现货 实单来谈
  • #职场发展#其他
  • $(document).ready(function(){}), $().ready(function(){})和$(function(){})三者区别
  • (C语言)fread与fwrite详解
  • (day 12)JavaScript学习笔记(数组3)
  • (Redis使用系列) Springboot 使用redis的List数据结构实现简单的排队功能场景 九
  • (二刷)代码随想录第16天|104.二叉树的最大深度 559.n叉树的最大深度● 111.二叉树的最小深度● 222.完全二叉树的节点个数
  • (附源码)ssm失物招领系统 毕业设计 182317
  • (十三)Java springcloud B2B2C o2o多用户商城 springcloud架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)...
  • (最简单,详细,直接上手)uniapp/vue中英文多语言切换
  • .bat批处理(三):变量声明、设置、拼接、截取
  • .gitignore文件_Git:.gitignore
  • .NET Core日志内容详解,详解不同日志级别的区别和有关日志记录的实用工具和第三方库详解与示例
  • .NET Framework与.NET Framework SDK有什么不同?
  • .NET 发展历程
  • .NET业务框架的构建
  • @cacheable 是否缓存成功_让我们来学习学习SpringCache分布式缓存,为什么用?
  • [2019/05/17]解决springboot测试List接口时JSON传参异常
  • [240607] Jina AI 发布多模态嵌入模型 | PHP 曝新漏洞 | TypeScript 5.5 RC 发布公告
  • [android] 看博客学习hashCode()和equals()