作者 :孙玉昌,昵称【一一哥 】,另外【壹壹哥】也是我哦
千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者
前言
在上一篇文章中,壹哥 给大家介绍了进程、线程的概念,以及创建线程的几种方式。现在我们对线程已经有了基本的了解,但是有些同学看了昨天的文章就来问壹哥 ,说线程的方法只有start()和run()吗?怎么没有看到别的线程方法?所以,今天壹哥要给大家介绍一些别的核心API方法,这些方法与我们的多线程开发都息息相关哦。
------------------------------前戏已做完,精彩即开始----------------------------
全文大约【5100】 字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......
配套开源项目资料
Github: github.com/SunLtd/Lear...
Gitee: gitee.com/sunyiyi/Lea...
一. 线程相关的核心API方法
为了让大家有个清晰的认知,壹哥会给大家分类绘制表格,来展示与线程相关的常用核心API方法及其用法。
1. Thread中的方法
Thread是Java中最基本的线程类,它提供了许多用于创建、管理和控制线程的方法,如下表所示:
方法名 | 返回类型 | 描述 |
---|---|---|
start() | void | 启动线程,JVM会调用线程的run()方法 |
run() | void | 执行线程的核心代码块 |
sleep(long millis) | static void | 指定线程休眠的毫秒数 |
interrupt() | void | 中断线程 |
isInterrupted() | boolean | 判断线程是否被中断 |
join() | void | 把指定的线程加入到当前线程,将两个交替执行的线程合并为顺序执行的线程,并可以让线程陷入等待 |
getName() | String | 获取某个线程的名称 |
setName(String name) | void | 设置线程的名称 |
yield() | static void | 暂停当前线程,让出CPU执行时间 |
isAlive() | boolean | 判断线程是否存活 |
setPriority(int priority) | void | 设置线程的优先级 |
getPriority() | int | 获取线程的优先级 |
wait() | final void | 线程等待,释放锁 |
notify() | final void | 唤醒等待中的线程 |
notifyAll() | final void | 唤醒等待中的所有线程 |
注意:以上这些方法都是在Thread类中定义的,因此我们需要先创建出Thread类的实例才能使用它们。
2. Object中的方法
虽然Object本身不是线程类,但它是Java中所有类的基类,它也给我们提供了几个用于线程操作的方法,如下所示:
方法名称 | 方法描述 |
---|---|
wait() | 使当前线程等待,直到另一个线程调用该对象的notify()或notifyAll()方法,或者指定的时间到期。 |
notify() | 唤醒正在等待该对象的一个线程。 |
notifyAll() | 唤醒正在等待该对象的所有线程。 |
3. Runnable中的方法
Runnable接口是Java中另一个重要的线程接口,它用于定义线程的任务,常用方法如下:
方法名称 | 方法描述 |
---|---|
run() | 定义线程的任务。 |
4. Callable中的方法
Callable接口是Java 5中引入的一个新接口,它与Runnable接口类似,但它可以返回一个值。常用方法如下:
方法名称 | 方法描述 |
---|---|
call() | 定义线程的任务,并返回一个值。 |
5. Executor中的方法
Executor接口是Java中线程池的核心接口,它定义了一组执行任务的方法。常用方法如下:
方法名称 | 方法描述 |
---|---|
execute(Runnable command) | 将一个任务提交到线程池中执行。 |
submit(Callable task) | 将一个带有返回值的任务提交到线程池中执行。 |
6. Executors中的方法
Executors类是Java中用于创建线程池的工厂类,它提供了许多静态方法来创建不同类型的线程池。常用方法如下:
方法名称 | 方法描述 |
---|---|
newFixedThreadPool(int nThreads) | 创建一个固定大小的线程池。 |
newCachedThreadPool() | 创建一个有缓存的线程池。 |
newSingleThreadExecutor() | 创建一个单线程的线程池。 |
newScheduledThreadPool(int corePoolSize) | 创建一个支持定时及周期性执行任务的线程池。 |
接下来壹哥就给大家简单演示一下,以上方法中几个重点方法的用法。
二. 核心方法的使用
1. sleep()方法
sleep()方法是Thread类中的一个静态方法,可以使线程暂停指定的时间,该方法接受一个long类型的参数,表示线程需要暂停的毫秒数。调用sleep()方法会让当前线程进入阻塞状态,暂停执行,等到指定的时间过去后会被唤醒继续执行当前任务。示例代码如下:
java
/**
* @author 一一哥Sun
*/
public class Demo05 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
System.out.println("当前线程:" + Thread.currentThread().getName() + ",i = " + i);
Thread.sleep(1000); // 暂停一秒
}
}
}
该上面的案例中,主线程会输出数字0-9,每个数字之间暂停一秒,实现了每秒输出一次的效果。
2. join()方法
Thread.join()方法可以使一个线程等待另一个线程执行完毕后再继续执行,该方法会阻塞当前线程,直到调用该方法的线程执行完毕。比如我们在主线程中调用了另一个线程的join()方法,则主线程就会被阻塞,直到另一个线程执行完成,主线程才能继续执行。示例代码如下:
java
/**
* @author 一一哥Sun
*/
public class Demo06 {
public static void main(String[] args) throws InterruptedException {
// 采用Lambda表达式创建线程
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("子线程1执行,i = " + i + ",当前线程:" + Thread.currentThread());
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("子线程2执行,i = " + i + ",当前线程:" + Thread.currentThread());
}
});
thread1.start();
thread2.start();
//主线程会等待两个子线程执行完毕后再继续执行
thread1.join();
thread2.join();
System.out.println("主线程执行完毕,当前线程:" + Thread.currentThread());
}
}
在上面的案例中,壹哥在主线程中启动了两个子线程,子线程分别输出数字0-4。在启动子线程后,主线程调用了thread1.join()和thread2.join()方法,使主线程等待两个子线程执行完毕后再继续执行。
3. yield()方法
Thread.yield()方法可以使当前线程放弃CPU资源,让其他线程优先执行。这就好比两个人在高速上开车,一个开奔奔的主动把路权让给了开库里南的,等库里南通过后自己才走,这就是让出了"CPU资源"。yield方法没有参数,调用该方法后,当前线程会进入就绪状态,等待重新获得CPU资源后又会继续执行。
但大家要注意,yield()方法只是告诉调度器当前线程愿意放弃CPU资源,以便其他线程能够运行。但该方法并不会阻塞当前线程,它只是让当前线程从运行状态转为就绪状态,让CPU重新调度安排。这就好比壹哥前面给大家举的开车的例子,奔奔愿意把路权让给库里南,但并不是说奔奔直接就把路权让给了库里南,而是奔奔告诉交警说:"我把路权让出来,您安排给别人使用吧"。此时交警看奔奔发扬了风格让出了路权,于是就重新调度安排了路权,让库里南先走。
java
/**
* @author 一一哥Sun
*/
public class Demo07 {
public static void main(String[] args) throws InterruptedException {
// 采用Lambda表达式创建线程
new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("子线程1执行,i = " + i);
// 当前子线程放弃CPU资源
Thread.yield();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("子线程2执行,i = " + i);
// 当前子线程放弃CPU资源
Thread.yield();
}
}).start();
// 主线程
for (int i = 0; i < 10; i++) {
System.out.println("主线程执行,i = " + i);
// 当前线程放弃CPU资源
Thread.yield();
}
}
}
在这个案例中,我们有3个线程,1个主线程,2个子线程,我们分别在这3个线程中循环调用yield()方法,这就相当于3个人都分别发扬风格让出了自己的CPU资源,大家互相谦让。所以在多次执行时就可能会出现不同的结果,一会A线程在前,一会B线程在前,一会又可能是C线程在前。
4. interrupt()方法
当某个线程调用了interrupt()方法时,该线程就会收到一个中断信号,中断状态会被设置为true,所以interrupt()方法可以用于中断一个线程。当前线程可以在中断信号的提示下进行一些清理工作,然后结束线程的执行。如果调用了interrupt()方法的线程正在被阻塞(如调用了sleep()、wait()、join()等方法),则该线程会抛出一个 InterruptedException异常,从而提前结束阻塞状态。
java
/**
* @author 一一哥Sun
*/
public class Demo08 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
//判断当前线程是否中断了
while (!Thread.currentThread().isInterrupted()) {
System.out.println("Thread 正在运行...");
try {
//休眠1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
// 发生异常时,中断线程
Thread.currentThread().interrupt();
System.out.println("Thread 中断了");
}
}
});
//开始执行
t.start();
// 主线程等待5秒
Thread.sleep(5000);
//中断t线程,如果发现线程t正在被阻塞,则t线程会抛出InterruptedException异常
t.interrupt();
}
}
在上面的案例中,我们开启了一个线程t,在该线程内部判断当前线程是否处于中断状态,如果没有中断则每隔1秒钟打印输出一句话。在主线程等待5秒后,线程t调用interrupt()方法进行中断,因为线程t每隔1秒会sleep()一下,所以线程t收到中断信号后就立即抛出了InterruptedException异常。
5. 优先级方法
setPriority()方法用于设置线程的优先级,getPriority()方法用于获取当前线程的优先级。优先级越高的线程在竞争CPU资源时获得的机会越大,所以就可以优先执行。Java中线程的优先级范围是1~10,其中1是最低优先级,10是最高优先级。
java
/**
* @author 一一哥Sun
*/
public class Demo09 {
public static void main(String[] args) throws InterruptedException {
// 采用Lambda表达式创建线程
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("子线程1执行,i = " + i+",优先级:"+Thread.currentThread().getPriority());
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("子线程2执行,i = " + i+",优先级:"+Thread.currentThread().getPriority());
}
});
// 设置线程优先级,我们可以直接使用Thread中自带的常量值
t1.setPriority(Thread.NORM_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
在Thread类,有3个内置的优先级的常量:
- Thread .*MIN_PRIORITY: *对应的值是1;
- Thread .*NORM_PRIORITY: *对应的值是5;
- Thread .*MAX_PRIORITY: *对应的值是10。
6. isAlive()方法
isAlive()方法用于判断当前线程是否处于活动状态,即线程是否还在执行中。该方法会返回一个布尔值,用于表示该线程是否正在运行中,如果线程已经终止或还未启动,则返回false。
当一个线程启动后,它会执行其中的run()方法。只要run()方法没有执行完毕,该线程就处于活动状态,即处于运行中。一旦run()方法执行完毕,线程就会停止运行,即不再处于活动状态。所以当线程已经启动并处于活动状态时,isAlive()方法就会返回true。如果一个线程还没有启动,或者已经执行完毕停止运行,调用isAlive()方法就会返回false
在这种情况下,如果我们需要确定一个线程是否已经停止运行,就可以使用isAlive()方法来进行判断。如果该方法返回true,说明线程还在运行;如果返回false,说明线程已经停止运行。
java
/**
* @author 一一哥Sun
*/
public class Demo10 {
public static void main(String[] args) throws InterruptedException {
MyThread2 thread = new MyThread2();
System.out.println("Thread is alive: " + thread.isAlive()); // 输出 false
thread.start();
System.out.println("Thread is alive: " + thread.isAlive()); // 输出 true
// 等待2秒,等待线程执行完成
Thread.sleep(2000);
System.out.println("Thread is alive: " + thread.isAlive()); // 输出 false
}
}
class MyThread2 extends Thread {
public void run() {
System.out.println("Thread is running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread is finished.");
}
}
在上面的示例中,壹哥 创建了一个继承自Thread类的自定义线程MyThread2。在main方法中,壹哥首先创建了一个MyThread2对象,并打印出该线程是否处于活动状态(即是否调用了start方法)。由于线程还没有开始运行,所以isAlive()方法返回false。接着,我们调用了start()方法启动了该线程,并再次调用isAlive()方法,此时返回true,因为线程已经开始运行。最后,我们又通过Thread.sleep()方法等待2秒,等待线程执行完成,再次调用isAlive()方法,此时返回false,因为线程已经执行完毕。
7. wait()和notify()方法
wait()方法是Java中Object类提供的方法,可以使当前线程进入到等待状态,等其他线程发出唤醒通知后,该线程会继续执行。当一个线程调用了wait()方法后,它会释放当前持有的锁,进入到等待状态,直到其他线程调用了相应对象的notify()或 notifyAll()方法来唤醒它 。接下来壹哥会设计一个wait()方法的案例,实现两个线程之间的通信。
在接下来的案例中,壹哥 在Demo11这个类中定义了一个变量number和两个方法increment()和decrement()。increment()方法会增加变量number的值,而decrement()方法会减少它的值。在主方法中,壹哥会创建两个线程ThreadA和ThreadB,它们分别调用increment()和decrement()方法,并使用wait()和notify()方法实现线程之间的协作。具体就是,当变量number的值为0时,ThreadA线程调用increment()方法增加它的值,并使用notify()方法通知ThreadB线程开始执行decrement()方法。当变量number的值为1时,ThreadB线程会调用decrement()方法来减少它的值,并使用wait()方法等待ThreadA线程发来的通知,直到变量number的值再次变为0。这样,两个线程之间就可以协作地增加和减少变量number的值。
java
/**
* @author 一一哥Sun
*/
public class Demo11 {
//定义一个数字
private int number = 0;
//数字加1
//添加synchronized同步锁,以后壹哥再细讲synchronized的作用
public synchronized void increment() {
number++;
// 完成操作后,通知其他线程可以执行了
notify();
}
//数字减1
public synchronized void decrement() throws InterruptedException {
while (number == 0) {
// 进入等待状态,等待其他线程的通知
wait();
}
//数字减1
number--;
}
public static void main(String[] args) {
Demo11 demo = new Demo11();
//创建A线程
Thread threadA = new Thread(() -> {
for (int i = 0; i < 5; i++) {
//数字增加
demo.increment();
System.out.println("Thread A: " + demo.number);
}
});
//创建B线程
Thread threadB = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
//数字减少
demo.decrement();
System.out.println("Thread B: " + demo.number);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动线程
threadB.start();
threadA.start();
}
}
在上面的案例中,壹哥使用了synchronized关键字来保证两个方法的原子性操作,避免了线程的安全问题。同时,我们在decrement()方法中使用while循环来避免虚假的唤醒问题。当变量number为0时,ThreadB线程调用wait()方法等待ThreadA线程的通知,且只有变量number再次变为0时,才会从wait()方法中返回,继续执行decrement()方法。在这个过程中,两个线程之间通过notify()和wait()方法协作,避免了死锁和数据竞争的问题。
8. 其他方法
除了以上方法之外,还有一些其他与线程操作相关的方法,壹哥就不再一一展示对应的代码了,大家可以自行尝试一下。
比如setDaemon()方法用于设置线程是否为守护线程。守护线程是一种特殊的线程,当一个Java应用程序中只剩下守护线程时,JVM就会自动退出。因此通常情况下,我们不应该在守护线程中执行一些需要必须完成的任务,如数据存储等。
getState()方法用于获取线程的状态。线程的状态包括:NEW(新建)、RUNNABLE(可运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(超时等待)和TERMINATED(终止) 。关于线程状态,壹哥后面再详细介绍。
getName()和setName()方法则分别用于获取和设置线程的名称。
------------------------------正片已结束,来根事后烟----------------------------
三. 结语
今天的这篇文章,壹哥 重点给大家介绍了线程中核心方法的使用,请大家多敲代码多熟悉这些方法的使用,并仔细体会这些方法的具体含义,以后这些方法我们都会经常用到。尤其是sleep()、join()、yield()、wait()、interrupt()这几个控制线程执行的方法,我们在面试时经常会被面试官问到这几个方法的区别:
- sleep()方法:使当前线程进入休眠阻塞状态,让出CPU的执行权,不会释放对象的锁,指定时间之后线程会重新进入可运行的就绪状态。常用于模拟耗时操作。
- join()方法:使得当前线程等待指定的线程执行结束后再继续执行,常用于等待子线程执行完毕后再执行主线程。
- yield()方法:让出当前线程占用的CPU时间,让其他线程有机会运行。与sleep()不同的是,yield()方法不会进入休眠阻塞状态,而是进入可运行的就绪状态,CPU执行权有可能被立即重新分配给该线程。常用于调试或测试多线程程序。
- wait()方法:使当前线程开始等待,直到其他线程调用notify()或notifyAll()方法唤醒它,或者等到指定的时间之后自动唤醒。wait()方法必须在同步块或同步方法中调用,调用wait()方法会释放对象的锁。
- interrupt()方法:用于中断线程的执行。当线程调用interrupt()方法时,该线程会收到一个中断信号,线程可以在中断信号的提示下进行一些清理工作,然后结束线程的执行。
简而言之,sleep()和yield()主要用于控制线程的运行时间和调度,join()方法主要用于等待其他线程的执行结果,wait()主要用于线程间的通信和协作。现在你明白了吗?
如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。