【JavaEE】多线程(1)
在上篇文章主要引入了线程的概念,并且介绍了创建线程的5种方式,这篇文章将继续对线程的知识进行讲解
一、Thread类及其常见方法
通过上篇文章可以知道:每个线程都对应一个唯一的Thread对象,那么Thread类中有哪些常见的方法
1.1 Thread的构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用Runnable对象创建线程对象,并命名 |
给线程起名字只是方便调试的时候知道哪个线程可能出问题了,对线程的运行效果没有影响
1.2 Thread的常见属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID: 线程的唯一标识,不同的线程不会重复,注意这里的id和系统中pcb上的id是不同的,是jvm自己搞定一套id体系
- 名称:就是创建线程时给其起的名字,默认为Thread-0,Thread-1......
- 状态:后续讲解,有就绪状态,阻塞状态等
- 优先级:优先级⾼的线程理论上来说更容易被调度到
- 是否后台线程:线程分为前台线程和后台线程
- 前台线程:前台线程运行没有结束的话,那么其所在的整个进程也一定不会结束
- 后台线程:后台线程运行不管有没有结束,都对进程的结束与否没有影响
前台线程可有多个,多个前台线程必须最后一个天台线程结束,整个进程才会结束,卖弄线程就属于前台线程,另外我们自己通过那5种方式创建出来的线程默认为前台线程,可以通过setDaemon方法将其设置为后台线程(一般不期望这个线程影响进程结束的就会将其设为后台线程)
- 是否存活:指的是系统中的线程(PCB)是否还存在
注意一个Thread对象对应唯一的一个线程 但是他们两个生命周期并不完全相同
Thread t = new Thread(() ->{
});
上述代码是创建了Thread对象,此时内核中的PCB还没有创建
t.start();
执行完start方法才是真正创建线程,此时PCB才被加入到链表中
Thread t = new Thread(() ->{
});
t.start();
Thread.sleep(1000);
上述代码中,由于线程没有任务执行,所以start方法执行完后线程很快就结束了(内核中的PCB被销毁了)但是由于sleep()方法使主线程睡眠了1秒钟,所以t指向的Thread对象还存在
Thread t = new Thread(() ->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.start();
t = null;
上述代码中,上述代码中线程还没有结束,t指向的Thread对象就被回收了
总结:Thread对象和PCB的生命周期会出现不同,所以要通过isAlive()判断线程是否存活
- 是否被中断
中断线程只是在提醒线程要终止了,但是实际上要不要终止还要看线程自己决定,详细介绍如下
这里讲解2种中断线程的方法
1)自己实现控制线程结束的代码->设置循环条件
public class Demo {
public static boolean isRunning = true; //循环条件
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
while(isRunning) {
System.out.println(isRunning);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(3000);
isRunning = false;
}
}
上述代码通过设置isRunning来结束t线程,正如刚开始说的,终止线程只是一个提醒,比如上述代码如果压根就没有拿isRunning来做循环条件,那不管外界怎么干涉线程都不会终止
除此之外还存在一个问题:t线程中如果沉睡10秒甚至更长,此时main线程是无法及时的把t线程终止掉,所以接下来介绍第二种也是比较推荐的方法
2)使用Thread提供的interrupt方法和isInterrupted方法来实现上述效果
刚刚我们自己定义了一个标志位,其实Thread里面内置了一个标志位:isInterruptted方法
/** * 线程内置的标志位 * @return true 表示线程要终止了 flase 表示线程要继续执行 */ Thread.isInterrupted();
while (!Thread.currentThread().isInterrupted()) {
//如果线程要继续执行,则循环会继续下去
//如果线程中断,则循环终止
}
t.interrupt();通过这个方法就可以设置Thread.isInterrupted()的值为true,其默认为false,除了能设置标志位的值,还可以唤醒sleep方法,下面举一个例子:
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();
}
3s过后,执行结果如下:
这里3s过后抛出了catch中的RuntimeException异常,如果将catch中的语句修改为e.printStackTrace();
3s过后,执行结果如下:
可以看到t线程仍然在执行,那么这里就有一个疑问:明明修改了标志位使得循环条件为false,为什么线程仍然在执行?
其实是因为sleep,当线程正在sleep,sleep被唤醒的同时,就会清除刚才的标志位(改回false)这样的设定实际上还是为了将是否要中断交给线程自己
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
});
t.start();
Thread.sleep(3000); //3s过后t线程会进入catch 报一个RuntimeException异常
t.interrupt();
}
如上述代码,将catch中的语句改为break;此时当sleep被唤醒时,就会直接结束
二、线程等待: join()
之前提到过,线程的执行是无序的,那么程序执行的结果就是随机的,但是我们希望可以控制线程的执行顺序从而得到稳定的执行结果,因此引入线程等待来确定线程结束的先后顺序
通过join()方法来实现线程等待
用法:假如在main线程里使用t.join(); 其效果就是main线程等待t线程结束后才会继续执行t.join()之后的逻辑,代码如下:
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("hello thread");
});
t.start();
t.join();
System.out.println("hello main");
}
上述代码中main线程会等待t线程执行结束后再执行t.join();之后的逻辑,执行结果为:
main线程调用join()有两种可能:
1.main线程调用t.join()后,如果t线程已经结束了,那么join会直接返回
2. 如果线程没有结束,就会使main线程处于阻塞状态(阻塞:线程暂时不参与CPU调度)
接下来再介绍两种带参的join()
void join() | 等待线程结束再继续执行(死等) |
void join(long milis) | 等待时间到了,会继续执行 |
void join(long milis, int nanos) | 等待时间到了,会继续执行,但时间会精确到纳秒 |
三、线程休眠sleep()
sleep()控制的使线程休眠的时间而不是sleep()前后两个代码的间隔时间,也可以理解为线程实际休眠时间往往大于sleep()中的参数设定的时间,举一个例子:
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(1000);
System.out.println(System.currentTimeMillis());
}
执行结果如下:
时间间隔大于1000ms
四、线程的状态
4.1 介绍状态
这里介绍6种线程状态
- NEW:安排了工作但还未开始执行(还没有start)
- RUNNABLE:就绪状态,该状态分为线程正在CPU上执行和线程可以随时调度到CPU上执行
- BLOCKED:进行锁竞争的时候产生的阻塞状态,后面再说
- WAITING:死等进入的阻塞状态
- TIMED_WAITING:带有超时时间的等进入的阻塞状态
- TERMINATED:线程终止,内核中的线程已经销毁了
4.2 状态转移
1条主线,3条支线