java 线程的常用操作基本上都在java.lang.Thread 类中进行了定义,基础的操作可以下图进行概括:
接下来我们对每一项基本操作进行详细说明:
线程名称的设置和获取
在java编程规范中要求: 创建线程或线程池时,需要指定有意义的线程名称,方便出现bug的时候回溯.
在Thread 中可以通过两种方式设置线程名称,方式一是通过构造函数Thread(…)初始化设置线程名称,方式二是通过 setName()方法设置线程名称. 想要获取线程名称就只能通过getName()方法进行获取.
设置线程名称和获取线程名称的示例如下
package com.th.thread;
import com.th.util.ThreadUtil;
/**
* @ClassName: ThreadNameDemo
* @Description:
* @Author: 唐欢
* @Date: 2022/8/15 14:29
* @Version 1.0
*/
public class ThreadNameDemo {
//每个线程的执行轮次
private static final int MAX_TURN =3;
// 异步执行目标类
static class RunTarget implements Runnable{
//实现Runnable接口
@Override
public void run() {
//实现run()方法
for (int turn=0 ;turn<MAX_TURN;turn++){
ThreadUtil.sleepMilliSeconds(500);
String cft= "["+Thread.currentThread().getName()+"] :"+"线程执行轮次:"+turn;
System.out.println(cft);
}
}
}
public static void main(String[] args) {
// 实例化Runnable 异步执行目标类
RunTarget target = new RunTarget();
//系统自动设置线程名称
new Thread(target).start();
//系统自动设置线程名称
new Thread(target).start();
//系统自动设置线程名称
new Thread(target).start();
new Thread(target,"手动命名线程-A").start();
new Thread(target,"手动命名线程-B").start();
ThreadUtil.sleepMilliSeconds(Integer.MAX_VALUE);
}
}
运行结果如下:
在设置线程名称的时候,需要注意一下三点:
- 线程名称一般在启动线程钱设置(即调用start()方法前进行设置线程名称),但是也允许为运行的线程设置名称(通常请下不建议这么操作)
- 运行两个Thread对象有相同的名称, 但是应该避免, 两个相同名称的线程会在出现bug的时候增加回溯难度,无法快速准确定位.
- 如果程序没有为指定名称, 系统会自动为线程设置名称. 建议最好是自己设置一个能快速定位的线程名称.
线程的Sleep操作
sleep 的作用是让当前正在执行的线程休眠,让CPU 去执行其他的任务, 从线程状态说,就是从执行状态变成限时阻塞状态.sleep()方法会抛出InterruptExcetion 异常, sleep()示例如下:
package com.th.thread;
import com.th.util.ThreadUtil;
/**
* @ClassName: SleepDemo
* @Description:
* @Author: 唐欢
* @Date: 2022/8/15 15:27
* @Version 1.0
*/
public class SleepDemo {
//睡眠时长5秒
public static final int SLEEP_GAP =5000;
//睡眠次数,值稍微大些方便使用jstack
public static final int MAX_TURN =50;
static class SleepThread extends Thread{
static int threadSeqNumber =1;
public SleepThread(){
super("sleepThread-"+threadSeqNumber);
threadSeqNumber++;
}
@Override
public void run() {
try{
for (int i=1;i<MAX_TURN;i++){
System.out.println(getName()+"睡眠轮次:"+i);
Thread.sleep(SLEEP_GAP);
}
} catch (InterruptedException e) {
System.out.println(getName()+"发生异常被中断");
// e.printStackTrace();
}
System.out.println(getName()+"运行结束.");
}
}
public static void main(String[] args) {
for (int i=0; i<5;i++){
Thread thread =new SleepThread();
thread.start();
}
System.out.println(ThreadUtil.getCurThreadName()+"运行结束.");
}
}
运行结果如下:
使用jstack 查看运行情况如下:
通过jstack指令输出可以看到在进行线程DUMP 的时间点,所创建的线程都处于TIMED_WAITING(sleeping)状态.
总结:当线程sleep时间结束后,不一定会立即得到执行, 因为此时CPU 有可能正在执行其他的任务,此时sleep结束的线程就进入就绪状态,等待分配CPU时间片以便有机会执行.
线程的interrupt 操作
Thread 的interrupt()方法的作用是将线程设置为中断状态.在以下两种情况下调用interrupt() 进行中断:
- 如果此线程处于阻塞状态,就会立即退出阻塞,并抛出InterruptException异常,从而提早终结被阻塞状态.
- 如果此线程 正在处于运行之中,线程就不受任何影响,继续运行,仅仅是线程的中断标记被设置为true.所以,程序可以在适当的位置通过调用isInterrupted()方法来查看自己是否被中断,并执行退出操作.
示例如下:
package com.th.thread;
import com.th.util.ThreadUtil;
/**
* @ClassName: InterruptDemo
* @Description:
* @Author: 唐欢
* @Date: 2022/8/16 15:34
* @Version 1.0
*/
public class InterruptDemo {
// 睡眠时长
public static final int SLEEP_GAR =5000;
//睡眠次数
public static final int MAX_TURN =50;
static class SleepThread extends Thread{
static int threadSeqNumber=1;
public SleepThread(){
super("sleepThread" + threadSeqNumber);
threadSeqNumber++;
}
@Override
public void run() {
System.out.println("["+Thread.currentThread().getName()+"] :"+getName()+"进入睡眠");
// 线程sleep会
try {
Thread.sleep(SLEEP_GAR);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("["+Thread.currentThread().getName()+"] :"+getName()+"发生被异常打断");
return;
}
System.out.println("["+Thread.currentThread().getName()+"] :"+getName()+"运行结束");
}
}
public static void main(String[] args) {
Thread thread1 = new SleepThread();
thread1.start();
Thread thread2 = new SleepThread();
thread2.start();
// 主线程等待2秒
ThreadUtil.sleepSeconds(2);
//打断线程1
thread1.interrupt();
// 主线程等待5秒
ThreadUtil.sleepSeconds(5);
//打断线程2,此时线程2已经终止
thread2.interrupt();
// 主线程等待1秒
ThreadUtil.sleepSeconds(1);
//打断线程2,此时线程2已经终止
System.out.println("["+Thread.currentThread().getName()+"] :"+"程序运行结束");
}
}
运行结果如下:
从运行结果可以看出,sleepThread1 大致sleep了2s后,被主线程打断,被打断的sleepThread1线程停止睡眠,并捕获到了InterruptException异常并抛出. sleepThread2 线程在sleep 7s后, 被主线程中断, 但是由于sleepThread2在被中断的时候已经执行结束了,所有对sleepThread2 没有产生实际性的影响.
线程的join操作
调用join()方法为Thread 线程合并,合并的本质是线程A 需要在合并点等待,一直等到线程B执行完成,或者等待超时. 举个例子, 依赖的线程A 调用被依赖的线程B的join()方法, 在执行流程上将被依赖的线程B 合并到依赖线程A,依赖线程A 等待被依赖线程B 执行完成后,依赖线程A 再继续执行.执行流程如下图所示:
调用join()方法示例:
package com.th.thread;
/**
* @ClassName: JoinDemo
* @Description:
* @Author: 唐欢
* @Date: 2022/8/16 16:37
* @Version 1.0
*/
public class JoinDemo {
// 睡眠时长
public static final int SLEEP_GAR =5000;
//睡眠次数
public static final int MAX_TURN =50;
static class SleepThread extends Thread {
static int threadSeqNumber = 1;
public SleepThread() {
super("sleepThread" + threadSeqNumber);
threadSeqNumber++;
}
@Override
public void run() {
System.out.println("["+Thread.currentThread().getName()+"] :"+getName()+"进入睡眠");
// 线程sleep会
try {
Thread.sleep(SLEEP_GAR);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("["+Thread.currentThread().getName()+"] :"+getName()+"发生被异常打断");
return;
}
System.out.println("["+Thread.currentThread().getName()+"] :"+getName()+"运行结束");
}
}
public static void main(String[] args) {
Thread threadA =new SleepThread();
System.out.println("["+Thread.currentThread().getName()+"] :"+"启动threadA");
threadA.start();
try {
//合并线程A,不限时
threadA.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("["+Thread.currentThread().getName()+"] :"+"启动threadB");
// 启动第二天线程,并且进行限时合并,等待时间为1秒
Thread threadB = new SleepThread();
threadB.start();
try {
//合并线程B,不限时
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("["+Thread.currentThread().getName()+"] :"+"线程运行结束");
}
}
执行结果:
线程的yield操作
yield()方法的作用是让目前正在执行的线程放弃当前的执行,让出cpu的执行权限,使得cpu去执行其他的线程.处于让步状态的JVM 层面的线程状态仍然是RUNNABLE状态,从执行状态变成了就绪状态.示例如下:
package com.th.thread;
import com.th.util.Print;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;
/**
* @ClassName: YieldDemo
* @Description:
* @Author: 唐欢
* @Date: 2022/8/16 17:03
* @Version 1.0
*/
public class YieldDemo {
//执行次数
public static final int MAX_TURN = 100;
//执行编号
public static AtomicInteger index = new AtomicInteger(0);
// 记录线程的执行次数
private static Map<String, AtomicInteger> metric = new HashMap<>();
//输出线程的执行次数
private static void printMetric() {
System.out.println("["+Thread.currentThread().getName()+"] :"+"metric = " + metric);
}
static class YieldThread extends Thread {
static int threadSeqNumber = 1;
public YieldThread() {
super("YieldThread-" + threadSeqNumber);
threadSeqNumber++;
metric.put(this.getName(), new AtomicInteger(0));
}
@Override
public void run() {
for (int i = 1; i < MAX_TURN && index.get() < MAX_TURN; i++) {
System.out.println("["+Thread.currentThread().getName()+"] :"+"线程优先级:" + getPriority());
index.incrementAndGet();
metric.get(this.getName()).incrementAndGet();
if (i % 2 == 0) {
//让步:出让执行的权限
Thread.yield();
}
}
//输出线程的执行次数
printMetric();
System.out.println("["+Thread.currentThread().getName()+"] :"+getName() + " 运行结束.");
}
}
public static void main(String args[]) {
Thread thread1 = new YieldThread();
thread1.setPriority(Thread.MAX_PRIORITY);
Thread thread2 = new YieldThread();
thread2.setPriority(Thread.MIN_PRIORITY);
Print.tco("启动线程.");
System.out.println("["+Thread.currentThread().getName()+"] :"+"启动线程.");
thread1.start();
thread2.start();
LockSupport.parkNanos(100*1000L*1000L*1000L);
}
}
执行结果如下:
从结果可以看出,程序一共启动了两个让步演示线程, 两个线程妹执行两次操作就让出cpu.但是两个线程的的优先级不同, 优先级高的执行次数比优先级低的执行次数多很多. 从而可得出,线程调用yield之后,操作系统在重新进行线程调度时偏向于执行机会让给优先级较高的线程.
yield 和sleep有些容易混淆的地方,但是他们之间还是存在着本质区别的:
(1)sleep会导致当前线程暂停指定的时间,没有CPU时间片的消耗。
(2)yield 只是对CPU调度器的一个提示,如果CPU调度器没有忽略这个提示,它会导致线程上下文的切换。
(3)sleep会使线程短暂block,会在给定时间内释放CPU资源。
(4)yield会使RUNNING状态的Thread进入RUNNABLE状态(如果CPU 调度器没有忽略这个提示的话)
(5)sleep几乎百分之百地完成给定时间的休眠,而yield的提示并不一定担保。
(6)一个线程sleep 另一个线程调用interrupt会捕获到中断信号,而yield则不会。
总结:
- yield仅能使一个线程从运行状态转到就绪状态,而不是阻塞状态。
- yield不能保证使得当前正在运行的线程迅速转换到就绪状态。
- 即使完成了迅速切换,系统通过线程调度机制从所有就绪线程中挑选下一个执行线程时,就绪的线程有可能被选中,也有可能不被选中,其调度的过程受到其他因素的影响。
线程的daemo操作
java线程分为守护线程(后台线程)和用户线程, 在开发中, 实例属性daemon =false ,默认情况是用户线程,可以通过调用setDaemon()方法设置,true是守护线程, false则为用户线程, 还可以通过isDaemon 获取线程的守护状态.示例如下:
package com.th.thread;
import com.th.util.Print;
import java.util.concurrent.locks.LockSupport;
/**
* @ClassName: DaemonDemo
* @Description:
* @Author: 唐欢
* @Date: 2022/8/16 17:14
* @Version 1.0
*/
public class DaemonDemo {
public static final int SLEEP_GAP = 500; //每一轮的睡眠时长
public static final int MAX_TURN = 4; //用户线程执行轮次
//守护线程实现类
static class DaemonThread extends Thread {
public DaemonThread() {
super("daemonThread");
}
@Override
public void run() {
System.out.println("[" + Thread.currentThread().getName() + "]" + ":" +"--daemon线程开始.");
for (int i = 1; ; i++) {
System.out.println("[" + Thread.currentThread().getName() + "]" + ":" +"--轮次:" + i + "--守护状态为:" + isDaemon());
// 线程睡眠一会
LockSupport.parkNanos(SLEEP_GAP * 1000L * 1000L);
}
}
}
public static void main(String args[]) throws InterruptedException {
Thread daemonThread = new DaemonThread();
daemonThread.setDaemon(true);
daemonThread.start();
Thread userThread = new Thread(() ->
{
System.out.println("[" + Thread.currentThread().getName() + "]" + ":" +">>用户线程开始.");
for (int i = 1; i <= MAX_TURN; i++) {
System.out.println("[" + Thread.currentThread().getName() + "]" + ":" +">>轮次:" + i + " -守护状态为:" + Thread.currentThread().isDaemon());
LockSupport.parkNanos(SLEEP_GAP * 1000L * 1000L);
}
System.out.println("[" + Thread.currentThread().getName() + "]" + ":" +">>用户线程结束.");
}, "userThread");
userThread.start();
//主线程合入userThread,等待userThread执行完成
// userThread.join();
System.out.println("[" + Thread.currentThread().getName() + "]" + ":" +" 守护状态为:" + Thread.currentThread().isDaemon());
System.out.println("[" + Thread.currentThread().getName() + "]" + ":" +"运行结束.");
}
}
运行结果如下:
从运行的结果可以看出: main 线程也是一条用户线程.main 线程在创建和启动了daemonThread 和userThread 后,就提前结束了,虽然main线程结束了,但是两条线程还在继续执行,其中就有一条是用户线程,所以进程还不能结束. 当剩下的一条用户线程userThread的run()方法执行完成后,userThread 线程执行结束.这是,所有用户线程执行已经完成,jvm进程就随之退出了. 在JVM退出时,守护线程daemonThread 还没有结束,还在继续死循环的执行中,而jvm并不管这些,强行终止了所有守护线程的执行.
从以上的示例我们可以得出这样一个结论: 用户线程和JVM 进程是主动关系,如果用户线程全部终止,JVM虚拟机进程也随之终止; 守护线程和JVM进程是被动关系,如果JVM进程终止,所有的守护线程也随之终止,下图清晰的表达了它们之间的关系:
在开发过程中, 使用守护线程需要特别注意一下几点:
- 守护线程必须在启动前(即调用start()之前)将其设置为守护状态为true. 启动之后就不能再将用户线程设置为守护线程,否则会抛出InterruptException异常.
- 守护线程存在被JVM强行终止的风险,所以在守护线程中尽量不要去访问系统资源. 守护线程被强行终止时,可能会引发系统资源操作不负责的中断, 从而导致资源不可逆的损坏.
- 守护线程创建的线程也是守护线程.创建完成后可以调用setDaemon(false)设置为用户线程.
到此为止, Thread线程的的基本操作就介绍完成了,你学会了吗?