目录
创建线程的四种方式
线程的状态和生命周期
扩展知识
线程的调度
线程状态的基本操作
协作机制
实例
线程插队
实例
线程休眠
实例
扩展小知识
线程让步
实例
扩展
进程和线程
线程的优先级
守护线程和用户线程
用户线程(User Thread):
守护线程(Daemon Thread):
关于守护线程和用户线程的要点:
实例
线程死锁
认识线程死锁
如何避免线程死锁
创建线程的四种方式
创建线程的具体实现可以参考Java进阶篇--创建线程的四种方式
在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。当Thread对象创建完成时,线程的生命周期便开始了。当run()方法中代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。线程整个生命周期可以分为五个阶段,分别是新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Terminated),线程的不同状态表明了线程当前正在进行的活动。
在程序中,通过一些操作,可以使线程在不同状态之间转换。
上图展示了线程各种状态的转换关系,箭头表示可转换的方向,其中,单箭头表示状态只能单向的转换,例如,线程只能从新建状态转换到就绪状态,反之则不能;双箭头表示两种状态可以互相转换,例如,就绪状态和运行状态可以互相转换。
接下来针对线程生命周期中的五种状态分别进行详细讲解,具体如下:
1.新建状态(New)
创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,没有表现出任何线程的动态特征。
2.就绪状态(Runnable)
当线程对象调用了start()方法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度。
3.运行状态(Running)
如果处于就绪状态的线程获得了CPU的使用权,并开始执行run()方法中的线程执行体,则该线程处于运行状态。一个线程启动后,它可能不会一直处于运行状态,当运行状态的线程使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态。
4.阻塞状态(Blocked)
一个正在执行的线程在某些特殊情况下,如被人为挂起或执行耗时的输入/输出操作时,会让出CPU的使用权并暂时中止自己的执行,进入阻塞状态。线程进入阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
5.死亡状态(Terminated)
当线程调用stop()方法或run()方法正常执行完毕后,或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。
线程在运行状态与阻塞状态之间的转换是由于特定的条件和操作引起的。下面是一些线程从运行状态转换为阻塞状态的常见原因:
要将线程从阻塞状态转换为就绪状态,需要满足特定的条件或操作:
注意:线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,也就是说结束阻塞的线程需要重新进入可运行池中,等待系统的调度。
在计算机系统中,线程调度是操作系统或虚拟机对线程执行顺序和运行时间的管理。
注意:通常情况下,程序员不需要关心这个过程,因为Java虚拟机会自动处理。但是,在一些特定的需求下,可能需要改变这种模式,由程序自己来控制CPU的调度。这可以通过使用Java的Thread类和相关的API来实现。
中断是一种协作机制,用于在多线程环境中控制程序的执行。中断标志位是一种内部状态,用于表示线程是否被中断。当一个线程被中断时,中断标志位将被设置为true,并且会抛出InterruptedException异常。
在使用这些方法时需要注意以下几点:
下面结合具体的实例来看一看
public class Main {
public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start(); // 启动线程
try {
Thread.sleep(2000); // 主线程休眠2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
myThread.interrupt(); // 中断线程
}
private static class MyThread extends Thread {
@Override
public void run() {
while (!isInterrupted()) {
System.out.println("线程正在运行...");
try {
Thread.sleep(500); // 线程休眠500毫秒
} catch (InterruptedException e) {
e.printStackTrace();
break; // 捕获到InterruptedException时退出循环,结束线程执行
}
}
System.out.println("线程已经中断...");
}
}
}
输出结果
线程正在运行...
线程正在运行...
线程正在运行...
线程正在运行...
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at 练习.Main$MyThread.run(Main.java:22)
线程已经中断...
在上面的示例中,我们创建了一个继承自Thread类的MyThread类,它重写了run()方法来定义线程的执行逻辑。在run()方法中,我们通过检查线程的中断状态(isInterrupted())来决定是否退出循环。同时,在每次循环中,线程会休眠500毫秒(Thread.sleep(500))。
在Main类的main()方法中,我们创建了一个MyThread的实例,并调用start()方法开启线程。主线程随后休眠2秒,然后调用myThread.interrupt()方法中断线程。当线程被中断时,它将捕获到InterruptedException并退出循环,最后输出一条提示信息。
因此,中断操作可以看做线程间一种简便的交互方式。一般在结束线程时通过中断标志位或者标志位的方式可以有机会去清理资源,相对于武断而直接的结束线程,这种方式要优雅和安全。
join()是Java中的一个方法,它用于让一个线程等待另一个线程执行完成。当一个线程调用另一个线程的join()方法时,它将会被阻塞,直到被调用的线程执行完毕。
join()方法有以下几种重载形式:
通过使用join()方法,可以实现多个线程之间的协同工作和结果的合并。例如,主线程可以调用某个子线程的join()方法来等待子线程执行完毕,然后再继续执行主线程的后续逻辑。
下面是一个简单示例,演示了join()方法的用法:
public class Main {
public static void main(String[] args) {
Thread thread1 = new MyThread("Thread 1");
Thread thread2 = new MyThread("Thread 2");
thread1.start();
thread2.start();
try {
thread1.join(); // 主线程等待thread1执行完成
thread2.join(); // 主线程等待thread2执行完成
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("所有线程都已完成.");
}
private static class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println(getName() + " 已启动.");
try {
Thread.sleep(2000); // 线程休眠2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + " 已完成.");
}
}
}
输出结果为:
Thread 1 已启动.
Thread 2 已启动.
Thread 1 已完成.
Thread 2 已完成.
所有线程都已完成.
在Java中,sleep()方法用于使当前线程休眠(暂停执行)一段时间。它是Thread类的静态方法,可以通过线程对象或直接通过类名调用。
sleep()方法有两种重载形式:
使用sleep()方法时需要处理InterruptedException异常,因为其他线程调用了当前线程的interrupt()方法会中断当前线程的休眠。
下面是一个简单的示例,演示了如何使用sleep()方法:
public class Main {
public static void main(String[] args) {
System.out.println("主线程开始执行");
try {
Thread.sleep(2000); // 当前线程休眠2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程继续执行");
}
}
输出结果可能如下所示:
主线程开始执行 //(等待2秒)
主线程继续执行
通过sleep()方法,我们可以控制线程的暂停时间,用于实现一些需要等待一段时间后再执行的逻辑。
需要注意的是,sleep()方法不会释放对象锁,因此其他线程无法获得被当前线程持有的锁。如果在多线程环境下使用sleep()方法,需要注意并发访问共享资源的同步问题。
sleep方法经常拿来与Object.wait()方法进行比较,这也是面试经常被问的地方。
sleep()方法和wait()方法在Java中用于不同的目的,尽管它们都可以暂停线程的执行,但有一些重要的区别。
相同点:
区别点:
1.来源和使用方式:
2.调用位置:
3.被唤醒机制:
4.锁的释放:
综上所述,sleep()方法主要用于线程的时间调度和暂停执行一段时间,而wait()方法主要用于线程间的协作和等待特定条件满足后再继续执行。选择使用哪种方法取决于具体的需求和场景。
yield()是Java中的一个方法,它用于提示线程调度器当前线程愿意放弃对CPU的使用权。当一个线程调用yield()方法时,它就会让出自己的时间片,告诉调度器可以先执行其他优先级相同或更高的线程。
1.调用方式:
2.功能和作用:
3.调度器行为:
4.适用场景:
需要注意的是,yield()方法不能保证在多线程程序中达到精确的任务调度顺序。它只是一种提示机制,告诉调度器当前线程有一定的让步意愿。实际上,调度器可以忽略这个提示而继续执行当前线程。
总结:yield()方法允许当前线程主动放弃对CPU的使用权,以促进其他线程的执行。然而,由于具体的调度行为取决于操作系统和虚拟机的实现,因此不应将yield()方法作为实现严格的线程间协作和任务调度顺序的方式。
另外需要注意的是,sleep()和yield()方法,同样都是当前线程会交出处理器资源,而它们不同的是,sleep()交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。而yield()方法只允许与当前线程具有相同优先级的线程能够获得释放出来的CPU时间片。
以下是一个简单的Java代码示例,演示了如何使用yield()方法:
public class Main implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
// 使用yield()方法让出CPU资源
Thread.yield();
}
}
public static void main(String[] args) {
// 创建两个线程对象
Thread thread1 = new Thread(new Main());
Thread thread2 = new Thread(new Main());
// 启动两个线程
thread1.start();
thread2.start();
}
}
在上述示例中,我们创建了一个名为YieldExample的类,实现了Runnable接口。在run()方法中,每个线程都会打印数字1到5,并在每次循环后调用yield()方法来让出CPU资源。
在main()方法中,我们创建了两个线程对象并启动它们。由于线程调度器的具体行为无法确定,因此无法预测哪个线程在某一次循环中会先执行。然而,通过使用yield()方法,我们鼓励线程之间进行公平的CPU资源共享。
请注意,由于线程调度的不确定性,运行示例代码可能会产生不同的输出结果。可以通过多次运行代码来观察这种不确定性和共享CPU资源的行为。
线程状态的基本操作还包括以下几种:
这些操作都是线程状态转换的基本操作,可以帮助我们更好地管理和控制线程的执行。
进程和线程的详细区别请参考并发编程基础知识篇--进程和线程的区别
Java中的线程优先级用于指定线程对CPU资源的获取优先级。每个线程都有一个默认的优先级,范围从1(最低)到10(最高)。可以使用setPriority(int priority)方法设置线程的优先级,其中priority参数表示新的线程优先级。
以下是关于线程优先级的一些要点:
虽然Java中提供了10个线程优先级,但是这些优先级需要操作系统的支持,不同的操作系统对优先级的支持是不一样的,不会和Java中线程优先级一一对应,因此,在设计多线程应用程序时,其功能的实现一定不能依赖于线程的优先级,而只能把线程优先级作为一种提高程序效率的手段。
记住:Java中的线程优先级只是一种提示机制,不能保证绝对的执行顺序。具体的调度行为取决于操作系统和虚拟机的实现。
以下的代码示例,演示如何设置和使用线程优先级:
public class Main {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyRunnable(), "Thread 1");
Thread thread2 = new Thread(new MyRunnable(), "Thread 2");
// 设置线程优先级
thread1.setPriority(Thread.MAX_PRIORITY); // 设置较高的优先级
thread2.setPriority(Thread.MIN_PRIORITY); // 设置较低的优先级
// 启动线程
thread1.start();
thread2.start();
}
static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
输出结果(请注意,由于线程调度器的具体行为,每次运行的输出可能会有所不同。):
Thread 1: 1
Thread 1: 2
Thread 1: 3
Thread 1: 4
Thread 1: 5
Thread 2: 1
Thread 2: 2
Thread 2: 3
Thread 2: 4
Thread 2: 5
在Java中,线程可以分为两种类型:守护线程(Daemon Thread)和用户线程(User Thread)。
IllegalThreadStateException
异常。请注意,如果将所有的用户线程都设置为守护线程,那么JVM 在用户线程结束后就会自动退出,不会等待守护线程执行完毕。
以下是守护线程和用户线程的代码示例:
public class MyClass {
public static void main(String[] args) {
//守护线程
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("守护线程在后台运行...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//用户线程
Thread userThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("用户线程执行第 " + i + " 次");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start(); // 启动守护线程
userThread.start(); // 启动用户线程
// 主线程等待用户线程执行完毕
try {
userThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("用户线程执行完毕,主线程结束");
}
}
注意:在上述代码示例中,守护线程确实在后台运行并打印信息,但用户线程和守护线程之间的执行顺序是不确定的。这是因为线程调度是由操作系统决定的,可能会导致输出结果的顺序与代码中的顺序不完全一致。
由于用户线程和守护线程之间的交替执行顺序是不可预测的,因此最终的输出结果可能每次运行都会略有差异。
以下是上述代码示例的输出结果:
守护线程在后台运行...
用户线程执行第 0 次
守护线程在后台运行...
用户线程执行第 1 次
守护线程在后台运行...
用户线程执行第 2 次
用户线程执行第 3 次
守护线程在后台运行...
用户线程执行第 4 次
守护线程在后台运行...
守护线程在后台运行...
用户线程执行完毕,主线程结束
线程死锁是指在多线程编程中,两个或多个线程互相等待对方释放资源而无法继续执行的情况。当线程之间存在循环依赖性,并且每个线程都在等待其他线程释放锁或资源时,就会发生死锁。
如上图所示,线程 1 持有资源 2,线程 2 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
通常,线程死锁发生的原因包括以下几种情况:
下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况:
public class MyClass {
public static void main(String[] args) {
final Object resource1 = new Object(); // 定义资源1
final Object resource2 = new Object(); // 定义资源2
Thread thread1 = new Thread(() -> {
synchronized (resource1) { // 获取资源1的锁
System.out.println("Thread 1: 持有资源 1...");
try {
Thread.sleep(1000); // 模拟执行任务所需的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: 正在等待资源 2...");
synchronized (resource2) { // 尝试获取资源2的锁
System.out.println("Thread 1: 持有资源1和资源 2...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource2) { // 获取资源2的锁
System.out.println("Thread 2: 持有资源 2...");
try {
Thread.sleep(1000); // 模拟执行任务所需的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: 正在等待资源 1...");
synchronized (resource1) { // 尝试获取资源1的锁
System.out.println("Thread 2: 持有资源1和资源 2...");
}
}
});
thread1.start(); // 启动线程1
thread2.start(); // 启动线程2
}
}
在上述代码中,thread1和thread2分别使用synchronized关键字来获取resource1和resource2的锁。它们都持有一个资源的锁并尝试获取另一个资源的锁,导致了相互之间的循环等待。运行这段代码时,可能会发现程序卡住并无法继续执行,这就是线程死锁。在这个示例中,thread1持有resource1的锁,并试图获取resource2的锁,而thread2持有resource2的锁,并试图获取resource1的锁。由于相互之间无法释放对方正在等待的资源,导致双方都无法继续执行,从而形成了死锁。
需要注意的是,死锁并不一定总会发生,它取决于线程竞争资源的时机和顺序。
为了避免线程死锁,可以采取以下策略:
要避免线程死锁,可以通过改变线程获取资源的顺序来解决。以下是修改后的示例代码:
public class MyClass {
public static void main(String[] args) {
final Object resource1 = new Object(); // 定义资源1
final Object resource2 = new Object(); // 定义资源2
Thread thread1 = new Thread(() -> {
synchronized (resource1) { // 获取资源1的锁
System.out.println("Thread 1: 持有资源 1...");
try {
Thread.sleep(1000); // 模拟执行任务所需的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: 正在等待资源 2...");
synchronized (resource2) { // 获取资源2的锁
System.out.println("Thread 1: 持有资源1和资源 2...");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource1) { // 获取资源1的锁
System.out.println("Thread 2: 持有资源 1...");
try {
Thread.sleep(1000); // 模拟执行任务所需的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: 正在等待资源 2...");
synchronized (resource2) { // 获取资源2的锁
System.out.println("Thread 2:持有资源1和资源2...");
}
}
});
thread1.start(); // 启动线程1
thread2.start(); // 启动线程2
}
}
在修改后的代码中,线程1和线程2都先获取资源1的锁,然后再尝试获取资源2的锁。通过统一的资源获取顺序,可以避免死锁的发生。