预见猿份
主题
首页面试题在线工具关于我们老苗一对一私教学员评价
实战项目
项目前置基础创新WMS项目Java微服务框架与实战云岚到家项目闪聚支付项目学成在线项目青橙电商项目JVM原理与实战调优分布式事务专题Java高频面试题MySQL从入门到精通Java数据结构与算法Java 并发编程(JUC)实战课程老苗一对一私教学员评价blog
blog
  • Java 并发编程(JUC)实战课程

    • 课程介绍
    • 01 多线程入门
    • 02 线程的常用方法
    • 03 线程池
    • 04 synchronized教程
    • 05 ReentrantLock教程
    • 06 读写锁
    • 07 JMM教程
    • 08 CAS教程
    • 09 wait&notify
    • 10 常用的并发集合
    • 11 异步任务控制
    • 12 常用并发工具







----- 到底线了 -----

第2章 线程的常用方法 ​

1.2 线程的常用方法 ​

1.2.1 线程休眠 ​

什么是线程休眠 ​

线程休眠是当前线程暂时让出CPU的时间片,当休眠时间到达后操作系统会继续分配时间片给它,但不保证立即执行。

线程休眠有什么用?

作一些延迟处理,比如: 短信验证码30秒后过期处理。

定时任务,比如:淘宝每隔24小时发签到红包。

线程休眠的常用方法 ​

查看Thread类中的API,sleep方法定义如下:

static void sleep(long millis) 
使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。  
static void sleep(long millis, int nanos) 
导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精度和准确性。
1
2
3
4

两个方法基本一样,推荐第一个方法。

Thread.sleep()单位是毫秒,面对繁多的业务需求,需要休眠的时间单位可能是秒、分、小时、天等,在使用Thread.sleep()一般会按照如下方法写:

休眠1天
Thread.sleep(60*60*24*1000)
休眠1小时 
Thread.sleep(60*60*1000)
1
2
3
4

这样的代码不方便阅读。

java.util.concurrent.TimeUnit枚举类提供了各种时间单位,使用TimeUnit类进行线程休眠的好处是可以指定时间单位,常用的单位如下:

DAYS 
时间单位天,代表二十四小时  
HOURS 
时间单位小时,代表六十分钟  
MICROSECONDS 
时间单位微秒,代表千分之一毫秒  
MILLISECONDS 
时间单位毫秒,为千分之一秒  
MINUTES 
时间单位分钟,代表60秒  
NANOSECONDS 
时间单位纳秒,代表十亿分之一秒  
SECONDS 
时间单位秒,代表一秒
1
2
3
4
5
6
7
8
9
10
11
12
13
14
线程休眠-sleep代码演示 ​

下边程序使用Thread类的sleep方法让主线程在执行过程中休眠3秒,代码如下:

package com.yjoffer.javase.thread.basic;

import com.yjoffer.javase.config.Logger;

/**
 * 线程常用方法测试
 * @author 预见猿份(yjoffer.com)
 * @version 1.0
 **/
public class ThreadFucntionTest {

    private static Logger logger = Logger.getLogger(ThreadFucntionTest.class);

    //测试sleep
    public static void test_sleep(){
        logger.info("开始执行");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.info("结束执行");
    }

    public static void main(String[] args) {
        test_sleep();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

输出:

主线程执行test_sleep()方法休眠2秒,下边是输出日志。

18:24:23.911 INFO com.yjoffer.javase.thread.basic.ThreadFucntionTest[main] - 开始执行
18:24:25.933 INFO com.yjoffer.javase.thread.basic.ThreadFucntionTest[main] - 结束执行
1
2

为什么线程休眠需要抛出异常InterruptedException,因为线程在休眠时可以被中断,线程收到中断请求则会抛出InterruptedException异常,线程中断稍后介绍。

线程休眠-sleep代码演示2 ​

如果有两个线程,每个线程的休眠的时间不同,下边测试这个效果。

主线程休眠2秒,启动一个新线程休眠3秒,代码如下:

//测试sleep,两个线程休眠时间不同
    public static void test_sleep2(){
        new Thread(()->{
            logger.info("新线程开始执行");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            logger.info("新线程结束执行");
        }).start();
        logger.info("主线程开始执行");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.info("主线程结束执行");
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

运行,输出:

18:29:48.612 INFO com.yjoffer.javase.thread.basic.ThreadFucntionTest[Thread-1] - 新线程开始执行
18:29:48.612 INFO com.yjoffer.javase.thread.basic.ThreadFucntionTest[main] - 主线程开始执行
18:29:50.638 INFO com.yjoffer.javase.thread.basic.ThreadFucntionTest[main] - 主线程结束执行
18:29:51.638 INFO com.yjoffer.javase.thread.basic.ThreadFucntionTest[Thread-1] - 新线程结束执行
1
2
3
4

从输出结果可以看出,每个线程的可以设置不同的休眠时间。

线程休眠-TimeUnit代码演示 ​

TimeUnit例子如下:

// 休眠1天
TimeUnit.DAYS.sleep(1);		
// 休眠2小时
TimeUnit.HOURS.sleep(2);		
// 休眠3分钟
TimeUnit.MINUTES.sleep(3);
// 休眠4秒
TimeUnit.SECONDS.sleep(4);			
// 休眠500毫秒
TimeUnit.MILLISECONDS.sleep(500);
1
2
3
4
5
6
7
8
9
10

尝试将上边的程序修改为使用TimeUnit实现。

代码如下:

public static void test_sleep_timeunit(){
        new Thread(()->{
            logger.info("新线程开始执行");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            logger.info("新线程结束执行");
        }).start();
        logger.info("主线程开始执行");
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.info("主线程结束执行");
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
小结 ​

线程休眠是当前线程暂时让出CPU的时间片,当休眠时间到达后操作系统会继续分配时间片给它,但不保证立即执行。

Thread.sleep()和TimeUnit都可以实现线程休眠,推荐使用TimeUnit,它可以更灵活的指定休眠时间单位。

1.2.2 什么是线程中断 ​

什么是线程中断 ​

线程中断是一种与线程交互的方法,外界可以向线程发出中断请求,线程收到中断请求决定是结束运行还是继续运行,这个过程就是线程中断。如下图:

image-20220124105348411

线程中断有什么用?

控制线程的运行,比如:用户点击停止任务,通过向线程发送中断请求,线程收到中断请求终止任务。

线程中断实现方法 ​

向线程发起中断请求非常简单,方法如下:

Thread thread = new Thread();
//发送中断请求
thread.interrupt();
1
2
3

可以通过isInterrupted()方法判断是否发生中断,如下代码:

if(isInterrupted()){
  //收到中断
}
1
2
3
4

线程收到中断请求后设置中断状态为true,isInterrupted()方法返回true。

下边的代码实现了收到中断请求退出,测试代码如下:

 //测试线程中断
    public static void test_interrupt_first(){

         Thread thread = new Thread(() -> {
            logger.debug("新线程开始运行");
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    logger.debug("线程收到中断请求");
                    break;
                }
            }
            logger.debug("新线程结束运行");
        }, "t1");
        thread.start();
        //主线程1秒后发起中断
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.debug("主线程发起中断");
        thread.interrupt();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

输出 :

10:53:51.769 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[t1] - 新线程开始运行
10:53:53.769 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[main] - 主线程发起中断
10:53:53.769 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[t1] - 线程收到中断请求
10:53:53.769 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[t1] - 新线程结束运行
1
2
3
4
判断中断的两种方法 ​

如何收到中断请求不退出继续运行如何实现?

如果将break去掉如下代码所示,发现每次循环都会进入isInterrupted()条件,因为中断状态仍为true。

		while (true){
                //判断是否发生中断
                if(Thread.currentThread().isInterrupted()){
                    logger.debug("收到中断请求");

                }
            }
1
2
3
4
5
6
7

如果使用Thread.interrupted()判断判断则可以清除中断状态,代码如下:

while (true){
                //判断是否发生中断
                if(Thread.interrupted()){
                    logger.debug("收到中断请求");

                }
            }
1
2
3
4
5
6
7

完整代码如下:

	//收到中断不退出
    //测试判断线程中断的两种方法
    public static void test_interrupt_first2(){
        Thread thread = new Thread(() -> {
            logger.debug("新线程开始运行");
            while (true) {
                if (Thread.interrupted()) {
                    logger.debug("线程收到中断请求");
                }
            }
        }, "t1");
        thread.start();
        //主线程1秒后发起中断
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.debug("主线程发起中断");
        thread.interrupt();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在Thread中有两个方法可以判断线程是否中断:

测试当前线程是否中断,true表示中断,false未中断,执行此方法后中断标记为false。 
static boolean interrupted() 
测试这个线程是否被中断,true表示中断,false未中断。
boolean isInterrupted()
1
2
3
4

上边两个方法的区别如下:

1、interrupted()为静态方法,isInterrupted() 为实例方法。

2、isInterrupted() 方法判断当前线程是否发生中断,interrupted()也可以判断中断并且 执行后可以清除中断标记,所以如果收到中断请求后想清除中断状态可以调用interrupted()。

1.2.3 线程中断退出的正确姿势 ​

sleep中断 ​

线程休眠后如何唤醒线程呢?

除了休眠时间过后自动恢复运行以外,通过线程中断也可以唤醒休眠中的线程。

如下代码实现线程休眠1秒 :

try {
	TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
	e.printStackTrace();
}
1
2
3
4
5

线程休眠期间可以向其发送中断请求,收到中断请求后线程抛出InterruptedException异常,这也是为什么sleep()方法必须声明InterruptedException异常的原因。

测试代码如下,在线程休眠时向它发起中断请求:

package com.yjoffer.javase.thread.basic;

import com.yjoffer.javase.config.Logger;

import java.util.Scanner;
import java.util.concurrent.TimeUnit;

/**
 * 测试线程常用方法
 * @author 预见猿份(yjoffer.com)
 * @version 1.0
 **/
public class ThreadFucntionTest {

    private static Logger logger = Logger.getLogger(ThreadFucntionTest.class);

    //测试sleep休眠时中断退出
    public static void test_interrup_exit(){
        Thread thread = new Thread(()->{
            logger.debug("开始运行");
            while(true){
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    logger.debug("sleep时中断");
                    break;
                }
            }
            logger.debug("结束运行");
        },"t1");
        thread.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //线程中断请求
        logger.debug("主线程请求中断");
        thread.interrupt();

    }
    ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

输出:

17:19:11.21 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[t1] - 开始运行
17:19:12.22 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[main] - 主线程请求中断
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.yjoffer.javase.thread2.basic.ThreadFucntionTest.lambda$test_interrup_exit$8(ThreadFucntionTest.java:207)
	at java.lang.Thread.run(Thread.java:748)
17:19:12.23 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[t1] - sleep时中断
17:19:12.23 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[t1] - 结束运行
1
2
3
4
5
6
7
8

开启新线程休眠2秒,主线程在1秒后发起中断,新线程收到中断请求抛出InterruptedException异常(响应中断)。

中断退出的正确姿势 ​

当线程在执行过程中发生中断,在退出前需要作一些善后工作,比如:关闭打开的文件、清理工作等。下边的代码固定在线程执行的最开头进行善后工作,思考会正常中断退出吗?

//中断退出作一些善后工作
    public static void test_interrup_exit2(){
        Thread thread = new Thread(()->{
            logger.debug("开始运行");
            while(true){
                if(Thread.interrupted()){
                    logger.debug("发生中断");
                    logger.debug("做一些善后工作");
                    break;
                }
                //其它工作...
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    logger.debug("sleep时中断");
                }
            }
            logger.debug("结束运行");
        },"t1");
        thread.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //线程中断请求
        logger.debug("主线程请求中断");
        thread.interrupt();

    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

通过运行发现并不能正确退出,输出日志如下:

17:22:56.1 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[t1] - 开始运行
17:22:57.1 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[main] - 主线程请求中断
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.yjoffer.javase.thread2.basic.ThreadFucntionTest.lambda$test_interrup_exit2$9(ThreadFucntionTest.java:240)
	at java.lang.Thread.run(Thread.java:748)
17:22:57.1 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[t1] - sleep时中断
1
2
3
4
5
6
7

分析原因:

从日志可以看出是在sleep 休眠时收到了中断请求,sleep休眠时收到中断请求会抛出InterruptedException异常并且将中断状态清除,所以重新进入while循环并不能判断出当前处于中断状态。

如何解决呢?

当sleep时收到中断请求,再次调用interrupt()方法向自己触发中断触发中断,这样再次进入while循环后则判断处于中断状态退出线程。

代码如下:

//中断退出作一些善后工作
    public static void test_interrup_exit2(){
        Thread thread = new Thread(()->{
            logger.debug("开始运行");
            while(true){
                if(Thread.interrupted()){
                    logger.debug("发生中断");
                    logger.debug("做一些善后工作");
                    break;
                }
                //还有其它的代码...
                //...
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    logger.debug("sleep时中断");
                    //由于清除了中断状态重新向自己发起中断
                    Thread.currentThread().interrupt();
                }
            }
            logger.debug("结束运行");
        },"t1");
        thread.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //线程中断请求
        logger.debug("主线程请求中断");
        thread.interrupt();

    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

1.2.3 join ​

线程执行顺序的需求 ​

多线程的优点是充分利用CPU资源,由操作系统来调度执行,为线程分配时间片,线程的执行没有特定的顺序,在生产中一些场景要求线程按一定的顺序执行,比如:统计企业全年的销售总额,先统计每个月的销售额,此任务完成后再统计全年销售额;大型软件系统启动,启动前会加载它的一些组件,待组件加载完毕方可启动成功。

下图是全部销售额统计简图,主线程启动t1、t2...子线程去统计每个月的销售额,统计完成主线程进行汇总。

线程优先级和线程让步 ​

在Thread的API中有两个方法似乎可以控制线程的执行顺序,它们是线程优先级和线程让步:

1)线程优先级

每个线程都有一个优先级,优先级的取值范围为从1到10,高优先级的线程比低优先级的有优先执行权,通过调用thread对象的setPriority(int newPriority) 方法设置线程优先级。

需要注意的是:优先级高只是可以大概率的优先执行,但不保证一定优先执行,最终由操作系统的任务调度来决定。

2)线程让步

调用Thread对象的yield()方法向线程调度器请求线程让步,表示当前线程已处理完成,可以调度其它线程执行。

即使发出线程让步消息,任务调度程序不保证响应。

线程让步有什么用?

在测试多线程程序时通过线程让步来干扰调度程序的执行,重现软件中的错误等,在生产中不建议使用线程让步方法来决定线程调度。

通过学习可知,线程优先级和线程让步不能保证线程的执行顺序。

线程等待 ​

线程等待是当前线程调用要等待线程的join()方法,等待线程执行完成后当前线程再继续执行。如下:

thread01线程为当前线程,调用thread02线程(要等待的线程)的join()方法,此时thread01线程会等待thread02线程,当thread02线程执行完成后thread01线程恢复执行。

如果thread02线程的执行时间不确定或时间太长可以在调用join()方法指定一个超时时间,时间一到不管thread02线程是否执行完成thread01线程都会继续执行,如下图:

线程等待测试 ​

下边启动t1、t2两个线程分别去统计1月、2月的销售额,最后在主线程汇总。

代码如下:

public static void test_join(){
        //启动t1统计1月份数据
        Thread t1 = new Thread(() -> {
            logger.debug("统计1月份数据");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            logger.debug("统计1月份结束");
        });
        //启动t2
        Thread t2 = new Thread(() -> {
            logger.debug("统计2月份数据");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            logger.debug("统计2月份结束");
        });
        t1.start();
        t2.start();
        logger.debug("汇总");
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

输出 :

12:03:12.51 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-1] - 统计1月份数据
12:03:12.51 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-2] - 统计2月份数据
12:03:12.51 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[main] - 汇总
12:03:15.73 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-1] - 统计1月份结束
12:03:17.73 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-2] - 统计2月份结束
1
2
3
4
5

从输出 结果可以看出主线程并没有等到t1和t2完成去汇总。

根据学习的线程等待知识,主线程在汇总前需要调用t1和t2的join方法等待t1和t2完成再去执行。

修改后的代码如下:

public static void test_join(){
        //启动t1统计1月份数据
        Thread t1 = new Thread(() -> {
            logger.debug("统计1月份数据");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            logger.debug("统计1月份结束");
        });
        //启动t2
        Thread t2 = new Thread(() -> {
            logger.debug("统计2月份数据");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            logger.debug("统计2月份结束");
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        logger.debug("汇总");
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

运行程序,输出 :

12:06:25.418 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-2] - 统计2月份数据
12:06:25.418 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-1] - 统计1月份数据
12:06:28.441 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-1] - 统计1月份结束
12:06:30.441 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-2] - 统计2月份结束
12:06:30.442 FINE com.yjoffer.javase.thread2.basic.ThreadFucntionTest[main] - 汇总
1
2
3
4
5

通过测试发现主线程等待t1和t2完成后再去执行汇总操作。

join方法还有一个可以设置超时的重载方法:

void join(long millis) 
等待这个线程死亡最多 millis毫秒。
1
2

下边的代码表示等待t1和t2线程1秒后继续执行

t1.join(1000);
t2.join(1000);
1
2

输出:

12:22:53.154 FINE com.yjoffer.javase.thread.basic.ThreadFunctionTest[Thread-2] - 开始统计2月份
12:22:53.154 FINE com.yjoffer.javase.thread.basic.ThreadFunctionTest[Thread-1] - 开始统计1月份
12:22:55.154 FINE com.yjoffer.javase.thread.basic.ThreadFunctionTest[main] - 汇总
12:22:55.177 FINE com.yjoffer.javase.thread.basic.ThreadFunctionTest[Thread-1] - 统计1月份结束
12:22:58.178 FINE com.yjoffer.javase.thread.basic.ThreadFunctionTest[Thread-2] - 统计2月份结束
1
2
3
4
5

下边的代码表示等待t1完成,等待t2线程1秒后继续执行

t1.join();
t2.join(1000);
1
2

输出 :

12:21:45.612 FINE com.yjoffer.javase.thread.basic.ThreadFunctionTest[Thread-1] - 开始统计1月份
12:21:45.612 FINE com.yjoffer.javase.thread.basic.ThreadFunctionTest[Thread-2] - 开始统计2月份
12:21:47.640 FINE com.yjoffer.javase.thread.basic.ThreadFunctionTest[Thread-1] - 统计1月份结束
12:21:48.640 FINE com.yjoffer.javase.thread.basic.ThreadFunctionTest[main] - 汇总
12:21:50.640 FINE com.yjoffer.javase.thread.basic.ThreadFunctionTest[Thread-2] - 统计2月份结束
1
2
3
4
5

1.2.4 线程状态 ​

六种线程状态 ​

一个线程从创建thread对象到start()启动,最后到执行完成经历了线程状态的变化,为了方便理解多线程程序,有必要理解线程的状态。线程共有六种状态,在Thread.State枚举类中定义如下:

JDK 17 中 Thread.State 枚举定义了以下 6 种线程状态:

状态说明
NEW新建状态。线程对象已创建(执行 new Thread())但尚未调用 start() 方法时处于该状态。
RUNNABLE可运行状态。调用 start() 方法后线程进入该状态;线程可能正在 Java 虚拟机中执行,也可能正在等待操作系统为其分配 CPU 时间片。
BLOCKED阻塞状态。线程在等待监视器锁(monitor lock)以进入或重新进入 synchronized 同步方法或同步代码块时处于该状态。
WAITING等待状态。线程无限期地等待另一个线程执行特定动作。调用以下方法会进入该状态:Object.wait()(无超时)、Thread.join()(无超时)、LockSupport.park()。
TIMED_WAITING限时等待状态。以指定等待时间调用以下方法会进入该状态:Thread.sleep(long)、Object.wait(long)、Thread.join(long)、LockSupport.parkNanos()、LockSupport.parkUntil()。
TERMINATED终止状态。线程 run() 方法执行完毕、线程退出后处于该状态。
线程状态测试 ​

上表中的线程状态除了阻塞状态目前都已接触,下边进行测试。

创建一个线程对象,在执行start()前是NEW状态,执行start()后为RUNNABLE状态,调用sleep()方法线程处于休眠于为TIMED_WAITING状态,主线程调用join()方法等待其它线程完成,此时主线程为WAITING状态,TIMED_WAITING与WAITING的区别是前者是有一定期限的等待,后者是无期限等待。线程执行完成为TERMINATED终止状态。

下边对线程的状态进行测试:

 //测试线程状态
    public static void test_threadstate(){
        Thread t1 = new Thread(()->{

        },"t1");

        Thread t2 = new Thread(() -> {
            for (; ; ) ;
        }, "t2");

        Thread t3 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t3");
        Thread t4 = new Thread(() -> {
            try {
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t4");
        Thread t5 = new Thread(() -> {
        }, "t5");
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        logger.debug("t1状态:"+t1.getState());//NEW
        logger.debug("t2状态:"+t2.getState());//RUNNABLE
        logger.debug("t3状态:"+t3.getState());//TIMED_WAITING
        logger.debug("t4状态:"+t4.getState());//WAITING
        logger.debug("t5状态:"+t5.getState());//TERMINATED

    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

运行程序,输出如下:

18:55:40.503 FINE com.yjoffer.javase.thread.basic.ThreadFucntionTest[main] - t1状态:NEW
18:55:40.527 FINE com.yjoffer.javase.thread.basic.ThreadFucntionTest[main] - t2状态:RUNNABLE
18:55:40.527 FINE com.yjoffer.javase.thread.basic.ThreadFucntionTest[main] - t3状态:TIMED_WAITING
18:55:40.527 FINE com.yjoffer.javase.thread.basic.ThreadFucntionTest[main] - t4状态:WAITING
18:55:40.527 FINE com.yjoffer.javase.thread.basic.ThreadFucntionTest[main] - t5状态:TERMINATED
1
2
3
4
5

1.2.5 守护线程 ​

Java中的两类线程 ​

Java中有两类线程,用户线程和守护线程。

用户线程:也叫前台线程,比如:主线程,或通过下边的代码启动一个用户线程

new Thread(任务对象).start();

守护线程:也叫服务线程、后台线程,守护线程专为用户线程服务的,所有的守护线程会随着最后一个用户线程的结束而自动结束。

守护线程通常作一些服务性的工作,它的应用场景有很多,比如:软件版本检测、健康检查、JVM的垃圾回收器等,JVM垃圾回收器的作用是对内存中一些不用的对象所占用的内存空间进行回收,从而释放出内存。当所有的用户线程结束了守护线程也就自动结束了。

守护线程如何定义 ​

定义一个守护线程非常简单,在启动线程前调用setDaemon方法标记为守护线程,代码如下:

Thread t1 = new Thread(任务对象);
t1.setDaemon(true); 
t1.start();
1
2
3
4

下边测试守护线程随着最后一个用户线程的结束而结束,测试代码如下:

package com.yjoffer.javase.thread.basic;

import com.yjoffer.javase.config.Logger;

import java.util.concurrent.TimeUnit;

/**
 * 线程常用方法测试
 * @author 预见猿份(yjoffer.com)
 * @version 1.0
 **/
public class ThreadFucntionTest {

    private static Logger logger = Logger.getLogger(ThreadFucntionTest.class);

	 //测试守护线程随着最后一个用户线程的结束而结束
    public static void test_daemon1() {
        //启动一个用户线程
        new Thread(()->{
            while (true) {
                logger.info("欢迎来预见猿份(yjoffer.com)自学编程");
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //3秒后结束
                break;
            }
            logger.info("最后一个用户线程结束");
        }).start();
        //启动一个守护线程
        Thread t1 = new Thread(()->{
            while (true) {
                logger.info("我是守护线程1号");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.setDaemon(true);//设置为守护线程
        t1.start();
        //启动一个守护线程
        Thread t2 = new Thread(()->{
            while (true) {
                logger.info("我是守护线程2号");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t2.setDaemon(true);//设置为守护线程
        t2.start();
        logger.info("main运行结束");

    }

    public static void main(String[] args) {
        test_daemon1();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

输出:

19:02:48.772 INFO com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-1] - 欢迎来预见猿份(yjoffer.com)自学编程
19:02:48.772 INFO com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-3] - 我是守护线程2号
19:02:48.772 INFO com.yjoffer.javase.thread2.basic.ThreadFucntionTest[main] - main运行结束
19:02:48.772 INFO com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-2] - 我是守护线程1号
19:02:49.798 INFO com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-3] - 我是守护线程2号
19:02:49.798 INFO com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-2] - 我是守护线程1号
19:02:50.798 INFO com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-3] - 我是守护线程2号
19:02:50.798 INFO com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-2] - 我是守护线程1号
19:02:51.798 INFO com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-3] - 我是守护线程2号
19:02:51.798 INFO com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-2] - 我是守护线程1号
19:02:51.798 INFO com.yjoffer.javase.thread2.basic.ThreadFucntionTest[Thread-1] - 最后一个用户线程结束
1
2
3
4
5
6
7
8
9
10
11

从输出结果可以看出,随着最后一个用户线程的结束所有的守护线程都结束了。

小结 ​

1、Java中有两类线程:守护线程和用户线程。

2、用户线程不会随着其它用户线程的结束而结束。

比如:main线程结束了其它用户线程仍然活着。

3、守护线程会随着最后一个用户线程的结束而结束。

4、所有用户线程结束了JVM进程也就结束了,随之所有的守护线程也就结束了。

5、一个守护线程中启动一个线程默认是守护线程,除非手动设置setDaemon(false)。

1.2.6 采集案例 ​

需求分析 ​

下边使用所学的知识实现一个多线程采集程序。

什么是采集程序?

采集程序也称为蜘蛛,它会根据采集策略去抓取一些有用的信息,比如:百度搜索通过采集程序去抓取各网站的网页信息,这样通过百度就可以搜索想要的信息。

再比如电商网站提供价格对比,如下图:

image-20210914171133028

电商网站通过采集程序采集大平台上商品的价格,进行对比展示。

本案例实现的采集程序功能如下:

1、开启两个线程模拟采集程序的执行,一次采集执行3秒。

2、主线程可控制两个采集线程,暂停、启动、退出。

分析:

1、采集执行时间3秒使用sleep休眠模拟实现。

2、主线程向采集线程发中断请求,采集线程收到中断请求暂停、启动。

3、将两个采集线程设置为守护线程,主线程退出两个采集线程自动退出。

案例代码 ​

根据需求分析进行代码实现,如下:

package com.yjoffer.javase.thread.basic;

import com.yjoffer.javase.config.Logger;

import java.util.Scanner;
import java.util.concurrent.TimeUnit;

/**
 * 线程常用方法测试
 * @author 预见猿份(yjoffer.com)
 * @version 1.0
 **/
public class ThreadFucntionTest {

    private static Logger logger = Logger.getLogger(ThreadFucntionTest.class);

 //采集程序
    public static void gather(){
        //线程1
        Thread t1 = new Thread(() -> {
            int a = 1;
            while (true) {
                if(a % 2 ==0){//暂停
                    try {
                        TimeUnit.SECONDS.sleep(30000);
                    } catch (InterruptedException e) {
                        logger.warn(e.getMessage());
                        a++;
                    }
                }
                logger.debug("开始采集");
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    logger.warn(e.getMessage());
                    a++;
                }

                if (Thread.interrupted()){
                    a++;
                }
            }
        },"t1");

        //线程2
        Thread t2 = new Thread(() -> {
            int a = 1;
            while (true) {
                if(a % 2 ==0){//暂停
                    try {
                        TimeUnit.SECONDS.sleep(30000);
                    } catch (InterruptedException e) {
                        logger.warn(e.getMessage());
                        a++;
                    }
                }
                logger.debug("开始采集");
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    logger.warn(e.getMessage());
                    a++;
                }
                if (Thread.interrupted()){
                    a++;
                }
            }
        },"t2");

        t1.setDaemon(true);
        t1.start();
        t2.setDaemon(true);
        t2.start();
        Scanner scanner = new Scanner(System.in);
        while (true){
            System.out.print("请输入线程编号控制或输入0退出:");
            int i = scanner.nextInt();
            switch (i){
                case 1:
                    t1.interrupt();
                    break;
                case 2:
                    t2.interrupt();
                    break;
                default:
                    break;
            }
            if(i==0){
                break;
            }
        }

    }
    public static void main(String[] args) {
        gather();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

说明:

主线程在控制台提示:请输入线程编号控制或输入0退出:

输入1表示控制t1线程,输入2表示控制t2线程,输入0退出程序。

输入一次1则1号采集线程暂停,再输入一次1则1号采集线程启动,继续输入一次1则1号采集线程暂停,依次类推。

2号采集线程的控制逻辑同1号采集线程。

运行程序:

请输入线程编号控制或输入0退出:
16:15:38.19 FINE com.yjoffer.javase.thread.basic.ThreadFucntionTest[t2] - 开始采集
16:15:38.19 FINE com.yjoffer.javase.thread.basic.ThreadFucntionTest[t1] - 开始采集
116:15:41.37 FINE com.yjoffer.javase.thread.basic.ThreadFucntionTest[t2] - 开始采集
16:15:41.37 FINE com.yjoffer.javase.thread.basic.ThreadFucntionTest[t1] - 开始采集
1
2
3
4
5

输入1,回车,控制台提示t1中断 ,t2仍在采集:

16:15:42.755 WARNING com.yjoffer.javase.thread.basic.ThreadFucntionTest[t1] - sleep interrupted
16:15:44.37 FINE com.yjoffer.javase.thread.basic.ThreadFucntionTest[t2] - 开始采集
1
2

输入2,回车,控制台提示t2中断,所以线程暂停采集:

16:15:45.33 WARNING com.yjoffer.javase.thread.basic.ThreadFucntionTest[t2] - sleep interrupted
1

再次输入1,回车,t1恢复采集:

16:15:48.653 WARNING com.yjoffer.javase.thread.basic.ThreadFucntionTest[t1] - sleep interrupted
16:15:48.653 FINE com.yjoffer.javase.thread.basic.ThreadFucntionTest[t1] - 开始采集
16:15:51.653 FINE com.yjoffer.javase.thread.basic.ThreadFucntionTest[t1] - 开始采集
1
2
3

输入0,回车,退出程序:

Process finished with exit code 0
1
← 01 多线程入门03 线程池 →








如果发现文档内容有错误或排版错乱,请及时联系站长老苗修改,不胜感激。联系我们
关于我们 | 隐私政策 | 豫ICP备2026003386号-4 | 豫公网安备41010202004008号
目录

本页无章节