文章目录
1. 基本概念
我们先来了解下什么是进程和线程?他们之间的关系是什么?
- 进程:进程是程序的运行实例(例如:一个运行的Eclipse就是一个进程),进程是程序向操作系统申请资源(如内存空间和文件句柄)的基本单位。
- 线程:线程是操作系统能够进行运算调度的最小单位。
- 关系:一个进程可以包含多个线程,同一个进程中的所有线程共享改进程中的资源,如内存空间、文件句柄等。
使用多线程的优势:
- 在我们多核的系统中,可以充分利用提高CPU利用率。
- 提高系统的吞吐率,当一个线程因为I/O操作而处于等待时,其它线程仍然可以执行其操作。
- 提高响应性,对于Web应用程序而言,一个请求的处理慢了并不会影响其它请求的操作。
2. 线程创建与启动
创建线程有如下两种方式:
- 继承Thread类
- 实现Runnable接口
继承Thread类
下面的代码,就是以定义Thread类子类的方式创建并启动线程:
public class CreateThreadDemo {
public static void main(String[] args) {
//创建线程
MyThread myThread = new MyThread();
//启动线程
myThread.start();
}
}
// 定义Thread类的子类
class MyThread extends Thread{
@Override
public void run() {
System.out.println("myThread begin running, threadName:" + Thread.currentThread().getName());
}
}
实现Runnable接口
下面的代码,就是以实现Runnable接口的方式创建并启动线程:
public class CreateThreadByRunnable {
public static void main(String[] args) {
//创建线程
Thread myThread = new Thread(new MyTask());
//启动线程
myThread.start();
}
}
// 实现Runnable接口
class MyTask implements Runnable{
@Override
public void run() {
System.out.println("myThread begin running, threadName:" + Thread.currentThread().getName());
}
}
3. 常用方法
下表列出了线程的一些常用方法:
方法 | 功能 | 备注 |
---|---|---|
currentThread | 返回当前代码的执行线程 | 同一段代码对Thread.currentThread()调用,返回值可能对应不同的线程 |
yield | 使当前线程主动放弃其对处理的占用,可能导致当前线程被暂停 | 这个方法是不可靠的,被调用时当前线程可能仍然继续运行 |
sleep | 使当前线程休眠指定时间 | |
start | 启动相应线程 | 一个Thread实例的start方法只能调用一次,多次调用会导致异常抛出 |
run | 用于实现线程的任务处理逻辑 | 由Java虚拟机直接调用,一般情况下应用程序不应该调用 |
interrupt | 中断线程 | 将线程的中断标记设置为false |
interrupted | 判断的是当前线程是否处于中断状态,同时会清除线程的中断状态 | |
isInterrupted | 判断调用线程是否处于中断状态 | 线程可以通过此方法来响应中断 |
getName | 获取线程的名称 | |
join | 等待相应线程运行结束 | 若线程A调用线程B的方法,那么线程A的运行会被暂停,知道线程B运行结束 |
getId | 获取线程的标识符 |
4. 线程生命周期
一个线程从其创建、启动到其运行结束的整个生命周期可能经历若干状态,如下图所示:
- NEW:一个已创建(调用线程start方法)而未启动的线程处于该状态,由于一个线程只能启动一次,所有只可能有一次处于该状态。
- RUNNABLE:该状态可以看成一个复合状态,包含REAY和RUNNING两个子状态。READY表示可以被线程调度器进行调度从而进入RUNNING状态。一个处于RUNNING状态的线程可以通过Thread.yield()方法变为READY状态。
- BLOCKED:一个线程发起一个阻塞IO操作,或者申请独占资源,未获取到锁时,会进入阻塞BLOCKED状态。当阻塞IO操作完成或获取到锁后,则状态转为RUNNABLE。
- WAITING:当一个线程执行了Object.wait()或LockSupport.part(Object)方法后,会进去WAITING状态。此状态必须通过其它线程调用Object.notify()/LockSupport.unpart(Object)方法来进行唤醒,从而再次转换为RUNNABLE状态。
- TIMED_WAITING:当一个线程调用了Thread.sleep(long),Object.wait(long),LockSupport.parkNanos(long)等方法后,会进入带有时间限制的等待状态TIMED_WAITING,当等待的时间满足后,会自动转换为RUNNABLE状态。
- TERMINATED:已经执行结束的线程处于TERMINATED状态。由于一个线程只能启动一次,所有只可能有一次处于该状态。
5. 线程中断
什么是中断
断可以看作由一个线程(发起线程)发送给另一个线程(目标线程)的一种指示,该指示用于表示发起线程希望目标线程停止其正在执行的操作。
中断仅仅代表发起线程的一个诉求,而这个诉求能够被满足则取决于目标线程自身,目标线程可能会满足发起线程的诉求,也可能根本不理会发起线程的诉求。
使用方法
看下面这段代码:
public class ThreadInterrupt {
public static void main(String[] args) {
// 创建子线程
Thread myThread = new Thread(new InterruptTask());
// 启动子线程
myThread.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中断子线程
myThread.interrupt();
}
}
// 实现Runnable接口
class InterruptTask implements Runnable{
@Override
public void run() {
// while条件是检测当前线程是否被中断
while(!Thread.currentThread().isInterrupted()){
System.out.println("thread is running...");
}
}
}
- 主线程通过调用子线程的interrupt()方法,将子线程的中断标记设置为true。
- 子线程通过调用Thread.currentThread().isInterrupted()来获取自身的中断标记值,若检测到被中断,则可以作响应的处理。
中断响应
Java标准库中许多阻塞方法对中断的响应方式都是抛出InterruptedException异常。
我们需要注意的是,抛出异常后,中断标志位会被清空(线程的中断标志位会由true重置为false)。我们看下面这个例子:
public class ThreadInterrupt {
public static void main(String[] args) {
// 创建子线程
Thread myThread = new Thread(new InterruptTask());
// 启动子线程
myThread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中断子线程
myThread.interrupt();
}
}
// 实现Runnable接口
class InterruptTask implements Runnable{
@Override
public void run() {
try {
// 子线程挂起3秒
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("子线程中断标记:" + Thread.currentThread().isInterrupted());
e.printStackTrace();
}
}
}
程序运行后结果:
子线程中断标记:false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.demo.InterruptTask.run(ThreadInterrupt.java:26)
at java.lang.Thread.run(Thread.java:745)
我们看到子线程在挂起的过程中被主线程中断,抛出了InterruptedException异常,且中断标记被重置为false。
由于中断标记被重置了,如果我们处理不当,可能会带来一些问题,看下面这个例子:
public class ThreadInterrupt {
public static void main(String[] args) {
// 创建子线程
Thread myThread = new Thread(new InterruptTask());
// 启动子线程
myThread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中断子线程
myThread.interrupt();
}
}
// 实现Runnable接口
class InterruptTask implements Runnable{
@Override
public void run() {
// 业务方法挂起期间被中断
service();
if(Thread.currentThread().isInterrupted()){
System.out.println("子线程被中断了");
}else{
System.out.println("子线程未被中断");
}
}
// 子线程执行的业务方法
public void service(){
try {
// 这边模拟子线程在执行的过程中挂起
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
// 这边捕获到异常什么也不做
}
}
}
上面的例子演示了,当我们捕获到InterruptedException 异常后(中断标记被重置为false),我们什么都没做,这边导致调用方无法正确判断中断标记,所以导致错误的逻辑。我们可以通过如下方法处理:
// 子线程执行的业务方法
public void service(){
try {
// 这边模拟子线程在执行的过程中挂起
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
// 保留线程中断标记
Thread.currentThread().interrupt();
}
}
当捕获到InterruptedException异常后,通过调用Thread.currentThread().interrupt()方法保留中断标记,这样调用者就可以知道当前线程被中断了。
6. 等待(wait)/通知(notify)
在Java中,Object.wait及Object.nofity可用于实现和通知。Object.wait的作用是使其执行线程被暂停(生命周期状态变为WAITING),该方法可用来实现等待;Object.notify的作用是唤醒一个被暂停的线程,调用该方法可实现通知。我们看下使用方法:
public class ThreadWaitNofity {
public static void main(String[] args) {
Object o = new Object();
// 新建一个等待线程
Thread waitThread = new Thread(() -> {
// 注意:Object.wait方法只能在synchronized块中执行
synchronized (o){
try {
System.out.println("线程进入了等待,开始等待时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
o.wait();
System.out.println("线程被唤醒了,被唤醒时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
// 恢复线程中断标记
Thread.currentThread().interrupt();
}
}
});
// 新建一个通知线程
Thread notifyThread = new Thread(() -> {
try {
// 延迟3秒钟
Thread.sleep(3000);
// 注意:Object.notify方法只能在synchronized块中执行
synchronized (o){
o.notify();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 启动等待线程
waitThread.start();
// 启动唤醒线程
notifyThread.start();
}
}
运行结果如下:
线程进入了等待,开始等待时间:2020-03-11 14:49:14
线程被唤醒了,被唤醒时间:2020-03-11 14:49:17
关于wait方法与sleep方法的区别在面试中经常被问及,我们整理如下:
wait | sleep | |
---|---|---|
使用限制 | 只能在同步synchronized中调用wait方法 | 不需要在同步中调用 |
作用对象 | wait方法定义在Object类中,作用于对象本身 | sleep方法定义在Thread中,作用于当前线程 |
释放锁资源 | 释放锁 | 不释放锁 |
唤醒条件 | 其他线程调用对象的notify()或者notifyAll()方法 | 超时或者中断 |
方法属性 | wait是实例方法 | sleep是静态方法 |