写这篇文章主要是为了梳理一下多线的基础知识以及各个知识点的使用,平时的工作中基本用不到多线程,慢慢的总会遗忘,把知识点梳理出来,一来是为了巩固,查漏补缺,二是为了方便将来忘记的时候回来翻看。
一、基本概念
1、进程
进程是指一个内存中运行的应用程序,每个进程都有自己一块独立的运行空间,一个进程中可以启动多个线程。比如Windows系统中,一个运行的exe就是一个进程。
进程是资源分配的最小单位
2、线程
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如Windows系统中的一个exe进程中可以运行很多线程,线程总是属于某个进程,进程中的多个线程共享进程的内存。
线程是cpu调度的最小单元
3、线程的生命周期
java线程的生命周期就是线程的生老病死,即线程的各个阶段的状态。Java中的线程共有6个状态:初始状态、运行状态、阻塞状态、等待状态、超时等待状态、终止状态。
- 初始状态(NEW):线程被构建,但是还没有调用start()方法
- 运行状态(RUNNABLE):java线程将操作系统中就绪和运行中两种状态笼统地称作运行状态。
- READY:就绪状态,调用线程的start()方法后,线程处于可运行状态,等待cpu分配资源;当调用yield()方法,主动放弃cpu的执行权限,也会进入可运行状态。
- RUNNING:运行中状态,当处于就绪的线程被调度并获取cpu的执行权限时,便进入了运行中状态,执行run()方法。
- 阻塞状态(BLOCKED):当线程发起阻塞的I/O操作,或者线程进入同步块拿到锁权限,其他线程只能等待,此时线程进入了阻塞状态。当I/O操作完成、释放锁,线程从阻塞状态转为运行状态。
- 等待状态(WAITING):线程由RUNNABLE状态转换为WAITING等待状态有三种情况:
- 线程在synchronized代码块中调用Object.wait()方法时,线程进入等待状态。调用Object.notify()或者Object.notifyAll()线程可重新转换为可运行状态。
- join()方法,当线程A调用线程B的join()方法,此时线程A进入等待状态,只有线程B执行完后,线程A转换为可运行状态
- LockSupport.park(),并发包中的锁都是基于此方实现的,当调用LockSupport.park()次方法后线程由可运行状态转换为等待状态,调用LockSupport.unpark(Thread)后,唤醒目标,线程由等待状态转换为可运行状态。
- 超时等待状态(TIME_WAITING):超时等待状态与等待状态类似,大致有5种可导致进入超时等待状态
- 调用带有超时参数的Thread.sleep(long m)
- 在synchronized同步代码中调用Object.wait(long m)
- 调用带有超时参数的Thread.join(long m)
- 调用带有超时参数的LickSupport.parkNanos(long m)
- 调用带有超时参数的LockSupport.parkUntil(long m)
- 终止状态(TERMINATED):表示该线程已经执行完毕,当线程的run()方法执行完或者线程执行异常导致线程提前终止,线程可进入终止状态。
二、线程的使用
1、线程的创建和启动
创建线程有继承Thread类、实现Runnable接口和Callable接口三种方式。对于实现Runnable接口或Callable接口实际上可以归为一类,只不过Callable接口可以有返回值,可以申明抛出异常。
线程的启动是在Thread对象上调用start()方法,而不是run()或者其他方法。对于java来说,run()方法没有任何特别之处,就像main()方法一样,它只是线程知道调用的方法签名,因此在Runnable或者Thread上调用run()方法是合法的,但不会启动线程。
- 继承java.lang.Thread类创建线程
/**
* 继承Thread类创建线程
*/
public class ThreadDemo extends Thread{
// 构造函数来接口线程名称
public ThreadDemo(String name){
super(name);
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(this.getName() + " :" + i);
}
}
public static void main(String[] args) {
// 创建线程实例,此时线程进入初始状态
ThreadDemo threadDemo = new ThreadDemo("线程1");
// 调用start() 方法,此时线程进入运行状态,等待cpu分配执行权限
threadDemo.start();
}
}
- 实现Runnable接口创建线程
/**
* 实现runnable接口创建线程
*/
public class RunnableDemo implements Runnable{
@Override
public void run() {
System.out.println("1");
}
public static void main(String[] args) {
// 创建RunnableDemo实例
RunnableDemo runnableDemo = new RunnableDemo();
// 启动RunnableDemo,需要实例化一个Thread,并传入自己的RunnableDemo实例
Thread thread = new Thread(runnableDemo);
// 启动线程
thread.start();
}
}
- 实现callable接口创建线程
/**
* 实现Callable接口
*/
// 1.实现Callable接口
public class CallableDemo implements Callable {
// 2.重写call()方法
@Override
public Object call() throws Exception {
int num = 0;
for (int i = 0; i < 100; i++) {
System.out.println("通过实现callable接口创建线程:" + i);
num++;
}
return num;
}
public static void main(String[] args) {
// 3.创建callable的实现对象
CallableDemo callableDemo = new CallableDemo();
// 4.将实现对象传递给FutureTask
FutureTask futureTask = new FutureTask(callableDemo);
// 5.传递给Thread
Thread thread = new Thread(futureTask);
// 6.调用start方法启动线程
thread.start();
try {
// 7.获取futureTask的返回值
Object num = futureTask.get();
System.out.println("num : " + num);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2、线程休眠-sleep
sleep方法是Thread类中的一个静态方法,当执行的线程调用Thread类的sleep方法后,该线程会让出指定时间的cpu执行权,休眠时间内不参与cpu的调度,但是该线程所拥有的监视器资源,比如锁还是持有且不会让出。指定的休眠时间到期后,会返回到就绪状态,重新参与cpu调度。
线程休眠被中断会抛出 InterruptedException异常
/**
* 线程休眠-sleep
*/
public class SleepDemo {
public static void main(String[] args) {
new Thread(()->{
try {
// sleep()是静态方法,所以最好的调用方法就是Thread.sleep()
// 线程睡眠5秒
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
sleep只能让当前线程睡眠,在A线程中调用B线程的sleep(5000)方法,睡眠的是A线程而不是B线程。
通过以下案例我们演示一下,在main线程中调用线程ThreadA的sleep方法,睡眠的是main线程。
/**
* 线程休眠-sleep
*/
public class SleepDemo {
public static void main(String[] args) {
ThreadA threadA = new ThreadA();
threadA.start();
try {
ThreadA.sleep(5000);
System.out.println("当前执行的线程为" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
System.out.println("main线程执行 : " + i);
}
}
}
class ThreadA extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程A执行 : " + i);
}
}
}
执行结果为
线程A执行 : 0
线程A执行 : 1
线程A执行 : 2
线程A执行 : 3
线程A执行 : 4
当前执行的线程为main
main线程执行 : 0
main线程执行 : 1
main线程执行 : 2
main线程执行 : 3
main线程执行 : 4
3、线程停止-stop/interrupt
对于线程的终止,jdk提供了stop方法,但是不推荐使用此方法,无论线程运行到什么程度,调用此方法都会被强制中断,假如此时线程中还有未处理完的业务,中断会导致业务紊乱,数据丢失,造成严重的后果。
我们推荐让线程自己停止下来,可以通过volatitle关键字修饰的标识位或者interrupt来实现。
- 通过调用stop方法中断线程
/**
* 调用stop终止线程
*/
public class StopThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
int i = 0;
while (i <= 100) {
i++;
System.out.println(Thread.currentThread().getName() + " " + i + " execution...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 线程被强制中断不会输出以下内容,导致数据丢失
System.out.println(Thread.currentThread().getName() + "is stop...");
});
thread.start();
TimeUnit.SECONDS.sleep(10);
thread.stop();
}
}
输出结果
- 通过volatitle关键字修饰的标识位来终止线程
/**
* 通过volatitle标识去判断退出线程
*/
public class VolatitleStopThreadDemo {
// volatitle修饰的标识
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
int i = 0;
while (!flag && i < 100){
i++;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 第" + i + "次运行。。。");
}
// 正常退出线程会输出以下内容
System.out.println(Thread.currentThread().getName() + " 线程终止。。。。");
});
thread.start();
TimeUnit.SECONDS.sleep(10);
flag = true;
}
}
输出结果
- 使用interrupt方法中断线程
Thread类中相关interrupt方法
| public void interrupt() | 中断线程 |
|---|---|
| public boolean isInterrupted() | 测试当前线程是否已经中断,线程的中断状态不受此方法影响 |
| public static boolean interrupted() | 测试当前线程是否已经中断,然后重置中断状态为false,静态方法,与InterruptedException作用一样,只是interrupted为主动,异常为被动 |
该方法中断线程,并不是真正意思的中断,它只会设置该线程的中断状态位为true,至于线程是否真正中断,由线程本身来控制。
下面我们看一个例子,调用线程的interrupt方法来中断线程。
public class InterruptDemo1 {
private static int i;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
System.out.println("线程标识状态 : " + Thread.currentThread().isInterrupted());
while (!Thread.currentThread().isInterrupted()){
i++;
}
System.out.println("调用interrupt后,线程标识状态 : " + Thread.currentThread().isInterrupted());
System.out.println("Num:" + i);
},"interruptDemo1");
thread.start();
// 线程睡眠1秒后执行中断
TimeUnit.SECONDS.sleep(1);
// 线程中断
thread.interrupt();
}
}
执行结果
从以上结果我们可以看出,当线程刚开始执行时,线程中断标识状态为false,线程调用interrupt方法发送中断信号,执行中断操作后,线程的中断标识状态变为true,线程进行了中断,并且中断前将未执行完的任务继续运行完成,友好的结束了当前线程。
从而我们可以知道interrupt并不是直接控制线程进行了中断,真正是否中断线程是由run方法来决定的,它只是发送一个信号(将线程的中断标识状态修改为true)给run方法,当run方法拿到这个信号后,判断是否满足要求,而后按照代码逻辑将run方法执行完成,中断线程。
当线程线程为了等待一些特定条件到来时,一般会调用sleep、wait、join方法来阻塞挂起当前线程。
例如一个线程调用了TimeUnit.SECONDS.sleep(3),那么线程就会被阻塞,直到3秒后才会被激活,但是有可能需要的那些特定条件在1秒后已经满足,这个时候我们就不需要非得等待3秒后才去继续执行,这样有点浪费时间,这个时候调用interrut来强制sleep抛出interruptedException异常。看下面例子:
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
int i = 0;
// Thread.currentThread().isInterrupted()获取中断信号
while(!Thread.currentThread().isInterrupted() && i < 100){
try {
i++;
System.out.println(Thread.currentThread().getName() +" 第" + i + "次执行");
// 睡眠1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Num:"+i);
},"interruptDemo");
thread.start();
// 先让他执行5秒再中断
TimeUnit.SECONDS.sleep(5);
// 调用interrupt执行中断操作
thread.interrupt();
}
}
执行结果:
以上例子理想中是让线程执行5秒后进行中断,结果5秒后抛出异常又重新开始执行了。这是什么原因呢?
5秒后,线程调用thread.interrupt()方法,发出中断信号,此时中断标识状态由原来得fasle被修改成了true,并抛出了interruptedException异常,并且在抛出异常时将标识状态进行了复位(修改为false),所以才会又重新开始执行。
所以我们知道了,调用thread.interrupt()方法在阻塞状态下做了两件事情,一个是使阻塞抛出interruptedExecption异常,二是将中断标识复位。因此我们在捕获异常后是否要中断取决开发者,如果要中断在catch中再次调用interrupt()。
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
int i = 0;
// Thread.currentThread().isInterrupted()获取中断信号
while(!Thread.currentThread().isInterrupted() && i < 100){
try {
i++;
System.out.println(Thread.currentThread().getName() +" 第" + i + "次执行");
// 睡眠1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
// InterruptedException 抛出来后,会恢复中断状态,复位
// 具体是否中断取决于开发者
// 如果想要中断,再次调用Thread.currentThread().interrupt();
Thread.currentThread().interrupt();
}
}
System.out.println("Num:"+i);
},"interruptDemo");
thread.start();
// 先让他执行5秒再中断
TimeUnit.SECONDS.sleep(5);
// 调用interrupt执行中断操作
thread.interrupt();
}
}
运行结果
线程会不时的检查中断标识,来判断线程是否应该被中断,如果一个线程调用了sleep、wait、ioin或者I/O操作进入阻塞状态,则线程在检查中断标识时如果发现中断标识为true时,则会在这些阻塞方法处抛出InterruptedExecption异常,并且在抛出异常后立即将线程的中断标识位清除,重新设置位false,即复位。
4、等待/通知机制-wait/notify/notityAll
等待/通知机制,是指一个线程A调用了对象O得wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或notifyAll()方法,线程A得到通知后从对象O的wait()方法返回,进而执行后续操作。
等待/通知的相关方法是任意java对象的具备的,因为这些方法被定义在了对象的超类Java.lang.Object上,具体的方法描述如下:
- notify():通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提的该线程获得了对象的锁
- notifyAll():通知所有等待在该对象上的线程
- wait():调用该方法的线程进入等待状态,只有等待另外线程通知或者中断才会返回,需要注意,调用wait方法后,会释放对象的锁
- wait(long):超时等待一段时间,这里的参数时间是毫秒,如果等待传入时间后没有得到通知就超时返回
- wait(long,int):对于超时时间更细粒度的控制,可以达到纳秒
等待/通知机制依托于同步机制,其目的就是确保等待线程从wait方法返回时能够感知到通知线程对变量做出的修改,在使用时需要注意的细节如下:
- 使用wait()、notify()和notifyAll()方法时需要先对调用对象加锁
- 调用wait()方法后,线程状态由RUNNING标称WAITING,并将当前线程放置到对象的等待队列
- notify()和notifyAll()方法调用后,等待线程依旧不会从wait()方法返回,需要调用notify()和notifyAll()方法的线程释放锁之后,等待线程才有机会从wait()返回
- notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而 notifyAll() 方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程 状态由 WAITING 变为 BLOCKED
- 从 wait()方法返回的前提是获得了调用对象的锁。
示例:
public class WaitNotifyDemo {
public static void main(String[] args) {
// 定义线程A
Thread threadA = new Thread(()->{
synchronized (WaitNotifyDemo.class) {
// 在同步代码中,调用wait方法进入等待状态,并释放锁
try {
System.out.println("线程" + Thread.currentThread().getName() + "调用 wait");
WaitNotifyDemo.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程" + Thread.currentThread().getName() + "执行完了");
},"threadA");
// 定义线程B
Thread threadB = new Thread(()->{
synchronized (WaitNotifyDemo.class){
// 拿到锁后,调用notify,通知线程A从wait方法返回
System.out.println("线程" + Thread.currentThread().getName() + "调用 notify");
WaitNotifyDemo.class.notify();
}
System.out.println("线程" + Thread.currentThread().getName() + "执行完了");
},"threadB");
threadA.start();
try {
// 睡眠3秒后再启动threadB线程,先让线程threadA拿到锁
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadB.start();
}
}
示例一,生产者与消费者:
分别启动5个生产者和消费者,库存最多为1,生产完即通知消费,消费完即通知生产。
/**
* 生产者与消费者实战
*/
public class Store {
int stock = 0;
// 消费者
synchronized void consumer() {
// 如果库存小于等于0,则进行等待消费者消费
if (stock <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果库存大于0,则进行消费,消费完后通知生产者生产
stock--;
System.out.println(Thread.currentThread().getName() + " 消费成功,库存:" + stock);
notifyAll();
}
// 生产者
synchronized void provider() {
// 如果库存大于0,则等待消费者消费
if (stock >0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果库存小于0,则生产
stock++;
System.out.println(Thread.currentThread().getName() + " 生产成功,库存:"+stock);
notifyAll();
}
public static void main(String[] args) {
Store store = new Store();
for (int i = 0; i < 5; i++) {
new Thread(()->{
store.consumer();
},"消费者-" + i).start();
}
for (int i = 0; i < 5; i++) {
new Thread(()->{
store.provider();
},"生产者-" + i).start();
}
}
}
输出结果
上面的例子输出结果显然有问题,为什么会出现这中情况呢?分析如下:
当程序开始运行,启动main方法,创建5个消费者和5个生产者线程,假如所有消费者都分别先抢占到了锁,然后判断先决条件库存小于等于0,分别执行wait方法,进入等待状态,并释放锁,当其中一个生产者线程“生产者-0”抢占到锁后,其他线程无法抢占锁进入阻塞状态;
“生产者-0”开始生产,生产结束后,库存加1,调用notifyAll方法,通知唤醒所有处于等待状态的线程,run方法执行完毕并释放锁,此时所有消费者被唤醒,从if判断中出来,沿着代码逻辑继续向下执行,最终导致库存出现负数。
当消费者线程全部执行完后,生产者拿到锁,开始执行,增加库存。最终导致出现以上错误的输出,以上分析属于极端分析,但也是错误可能会产生的原因之一。要解决此类问题的发生,我们只需要将先决条件if判断改成while循环就可以了,改成while循环后,当调用notifyAll方法后,被唤醒的线程会继续循环看先决条件是否满足,不满足条件继续调用wait方法,不会向下执行代码,从而解决了以上问题。正确的代码示例如下:
/**
* 生产者与消费者实战
*/
public class Store {
int stock = 0;
// 消费者
synchronized void consumer() {
// 如果库存小于等于0,则进行等待消费者消费
// 将此处if修改为while
while (stock <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果库存大于0,则进行消费,消费完后通知生产者生产
stock--;
System.out.println(Thread.currentThread().getName() + " 消费成功,库存:" + stock);
notifyAll();
}
// 生产者
synchronized void provider() {
// 如果库存大于0,则等待消费者消费
// 将此处if修改为while
while (stock >0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果库存小于0,则生产
stock++;
System.out.println(Thread.currentThread().getName() + " 生产成功,库存:"+stock);
notifyAll();
}
public static void main(String[] args) {
Store store = new Store();
for (int i = 0; i < 5; i++) {
new Thread(()->{
store.consumer();
},"消费者-" + i).start();
}
for (int i = 0; i < 5; i++) {
new Thread(()->{
store.provider();
},"生产者-" + i).start();
}
}
}
正确结果输出
notify和notifyAll具体什么时候用哪个?
notify只会唤醒一个线程,存在漏唤醒,信号丢失的可能,notifyAll效率不高,会唤醒本不应该被唤醒的线程,但是他在正确性方面有保证。使用notifyAll唤醒线程后,进入wait之前的先决条件最好用while判断是否达到了被唤醒的条件,不符合就继续调用wait等待,否则会出现安全性问题,不能用if。
如果满足以下条件,则优先使用notify,否则使用notifyAll
- 每次最多只会唤醒一个线程
- 等待集中的所有线程均为同质线程(线程干的活一摸一样)
5、线程让步-yield
线程的让步含义就是使当前运行着的线程让出cpu资源,但是让给谁不知道,仅仅是让出,线程状态回归到可运行状态。
线程让步使用Thread.yield()方法,yield()方法为静态方法。
需要注意的地方:
- 暂停当前正在执行的线程对象,并执行其他线程
- 意思就是调用yield方法会让当前线程交出cpu权限,让cpu去执行其他线程。它跟sleep方法类似,同样不会释放锁。
- 先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。
- 实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
- 调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的
示例1:
/**
* 线程让步-yield
*/
public class YieldDemo {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 执行第" + i + "次");
}
},"线程1");
Thread t2 = new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 执行第" + i + "次");
Thread.yield();
}
},"线程2");
t1.start();
t2.start();
}
}
运行结果,从结果中可以看出,线程2执行让步后,让出cpu,执行线程1。
示例2:
/**
* 线程让步-yield
* 同步代码块中执行yield
*/
public class YieldDemo {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5; i++) {
synchronized (YieldDemo.class){
System.out.println(Thread.currentThread().getName() + " 执行第" + i + "次");
}
}
},"线程1");
Thread t2 = new Thread(()->{
synchronized (YieldDemo.class){
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 执行第" + i + "次");
Thread.yield();
}
}
},"线程2");
t1.start();
t2.start();
}
}
运行结果,从运行结果看出,线程2执行了让步yield方法,但是没有真正让出cpu执行权,因为yield不会让出锁,因此线程2一直拿着执行权。
6、线程优先级
在操作系统中线程可以划分优先级,优先级较高的线程获得cpu资源的概率要大于优先级级较低的线程,当然优先级较低的线程并非没有机会执行,线程的优先级无法保障线程的执行顺序。
在java中,线程的优先级分为1~10这10个等级,如果小于1或者大于10,则jdk会抛出异常IllegalArgumentException();JDK中使用3个常量来预置定义优先级的值,代码如下:
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
线程的优先级默认为5,通过getPriority()来获取线程优先等级,通过setPriority(int newPriority)来设置线程优先等级。
/**
* 线程的优先级
*/
public class PriorityDemo {
public static void main(String[] args) {
// 获取main线程的优先级
System.out.println(Thread.currentThread().getName() +"默认-->" + Thread.currentThread().getPriority());
// 设置线程优先等级
Thread.currentThread().setPriority(9);
// 输出设置后的main线程优先级
System.out.println(Thread.currentThread().getName() + "设置后-->" + Thread.currentThread().getPriority());
}
}
输出结果
线程的优先级具有继承性,假如线程A中启动了一个线程B,那么线程B就是线程A的子线程,子线程的初始优先级与父亲 线程相同。
/**
* 线程优先级的继承问题
*/
public class PriorityDemo1 {
public static void main(String[] args) {
// 获取主线程的优先级
System.out.println(Thread.currentThread().getName() + "默认-->" + Thread.currentThread().getPriority());
// 设置主线程的优先级
Thread.currentThread().setPriority(7);
// 输出设置后主线程的优先级
System.out.println(Thread.currentThread().getName() + "设置后-->" + Thread.currentThread().getPriority());
// 创建子线程
Thread thread = new Thread(()->{
System.out.println("子线程启动成功");
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出子线程优先级,看是否与main线程设置后的线程优先级相同,如果相同,说明子线程继承了父线程的优先级
System.out.println(thread.getName() + "子线程优先级-->" + thread.getPriority());
}
}
输出结果,从输出结果看,子线程继承了父线程的优先级
优先级高的线程一定比优先级低线程先执行完吗?答案是否定的,因为优先级具有随机性,优先级高只是获得cpu的机率会比优先级低的高而已。
/**
* 优先级高的线程一定比优先级低线程先执行完吗?答案是否定的,因为优先级具有随机性,优先级高只是获得cpu的机率会比优先级低的高而已。
*/
public class PriorityDemo2 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
long startTime = System.currentTimeMillis();
long addResult = 0;
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 50000; j++) {
Random random = new Random();
random.nextInt();
addResult += i;
}
}
long endTime = System.currentTimeMillis();
System.out.println("线程1花费的时间:" + (endTime-startTime));
});
Thread t2 = new Thread(()->{
long startTime = System.currentTimeMillis();
long addResult = 0;
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 50000; j++) {
Random random = new Random();
random.nextInt();
addResult += i;
}
}
long endTime = System.currentTimeMillis();
System.out.println("线程2花费的时间:" + (endTime-startTime));
});
t1.setPriority(6);
t1.start();
t2.setPriority(7);
t2.start();
}
}
输出结果,运行了无数次才出来这个结果,太不容易了,这样也可以说明虽然t2线程的优先级要大于t1的,但是也有可能t1比t2先输出完。
7、线程合并-join
很多情况下,我们都是通过主线程创建子线程,如果子线程需要耗费大量时间计算的话,往往主线程比子线程先结束,但我们希望的是子线程结束后,再执行主线程,这个时间我们就可以通过Thread类中的join来解决。
join方法的作用是把指定的线程加入到当前线程,将两个交替执行的线程合并为两个顺序执行的线程。比如线程B中调用线程A的join方法,知道线程A执行完毕后,才会继续执行线程B。
为了验证上面的内容,接下来我们看一个实例:
/**
* 多线程join方法的使用
* 假设有三个线程,threadA,threadB,threadC,三个线程依次执行,最后执行主线程
*/
public class JoinDemo {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "-->第一个输出");
}, "threadA");
Thread threadB = new Thread(() -> {
// threadB中调用threadA,目的是为了threadA执行完后执行threadB
try {
threadA.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->第二个输出");
}, "threadB");
Thread threadC = new Thread(() -> {
// threadC中调用threadB,目的是等threadB执行完后执行threadC
try {
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->第三个输出");
}, "threadC");
threadA.start();
threadB.start();
threadC.start();
// 主线程中调用threadC,目的是等threadC执行完后执行主线程
try {
threadC.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->主线程最后输出");
}
}
输出结果
带参数的 join 方法使用
等待 millis 毫秒终止线程,假如这段时间内该线程还没执行完,也不会再继续等待。
/**
* join(long millis)方法的使用
*/
public class JoinDemo1 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
System.out.println("子线程输出");
});
thread.start();
// 主线程调用子线程的join(long millis)方法,等待3秒后执行
// 假如这段时间内该线程还没执行完,也不会再继续等待
thread.join(3000);
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "主线程输出");
}
}
输出结果
join方法的源码
/**
* Waits for this thread to die.
*
* <p> An invocation of this method behaves in exactly the same
* way as the invocation
*
* <blockquote>
* {@linkplain #join(long) join}{@code (0)}
* </blockquote>
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final void join() throws InterruptedException {
join(0);
}
/**
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} means to wait forever.
*
* <p> This implementation uses a loop of {@code this.wait} calls
* conditioned on {@code this.isAlive}. As a thread terminates the
* {@code this.notifyAll} method is invoked. It is recommended that
* applications not use {@code wait}, {@code notify}, or
* {@code notifyAll} on {@code Thread} instances.
*
* @param millis
* the time to wait in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
从源码中可以看出,join方法实际上是通过调用wait方法, 来实现同步的效果的,因此调用join方法的时候也会同调用wait方法一样需要捕获interruptExecttion异常。例如,A线程中调用了B线程的join方法,则相当于A线程调用了B线程的wait方法,在调用了B线程的wait方法后,A线程就会进入阻塞状态,因为它相当于放弃了CPU的使用权。需要注意的是,jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。
8、线程死锁
8.1、死锁的概念
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象,若无外力作用,他们都将无法进行下去。也就是说两个线程在拥有锁的情况下,又尝试获取对方的锁,从而导致程序进入阻塞状态。
来看一个例子:
/**
* 死锁
*
*/
public class DeadLockDemo {
private static String lockA = "A";
private static String lockB = "B";
public static void main(String[] args) {
Thread threadA = new Thread(()->{
// 得到lockA锁
synchronized (lockA) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 得到lockB锁
synchronized (lockB){
System.out.println("1111111111");
}
}
});
Thread threadB = new Thread(()->{
// 得到lockB锁
synchronized (lockB){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 得到lockA锁
synchronized (lockA){
System.out.println("22222222222222");
}
}
});
threadA.start();
threadB.start();
}
}
如果我们的线程是像上面一样交错执行的,就可能会出现以下情况:
- 线程threadA启动得到lockA的锁
- 线程threadB启动得到lockB的锁
- 线程threadA和线程threadB都继续执行,threadA需要得到lockB的锁才能继续执行,threadB需要得到lockA的锁才能继续执行。
- 但是线程threadA得到锁lockA并没有释放,线程threadB得到的锁lockB也并没有释放
- 所以他们都只能等待,而这种等待是无期限的,也就是死锁,这个时候只能重新启动才能解决问题
8.2、死锁产生的必要条件
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。
9、守护线程
线程分为用户线程和守护线程,守护线程和普通线程的下发上基本没啥区别,调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。
守护线程使用的情况比较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程,还有就是在做数据库应用过的时候,使用的数据库连接池,连接池本身也包含很多后台进程,监控链接个数、超时时间、状态等。
/**
* 守护线程
*/
public class MyDaemonDemo {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for (int i = 0; i < 7; i++) {
System.out.println("用户线程t1第"+i+"次执行");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 999999; i++) {
System.out.println("守护线程t2第"+i+"次执行");
try {
TimeUnit.SECONDS.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.setDaemon(true);
t1.start();
t2.start();
}
}
执行结果
从上面的例子可以看出,用户线程是保证执行完毕的,后台守护线程还没有执行完毕就结束了。