目录
创建线程的四种方式
创建线程的四种方式
- 继承Thread类
- 实现Runnable接口
- 使用Callable和Future创建线程
- 使用Executor框架创建线程池
创建线程的具体实现可以参考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),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。
扩展知识
线程在运行状态与阻塞状态之间的转换是由于特定的条件和操作引起的。下面是一些线程从运行状态转换为阻塞状态的常见原因:
- 等待I/O:当线程执行需要等待输入/输出操作完成时,例如读取文件、网络通信等,它会被阻塞。
- 获得锁失败:当线程尝试获取一个被其他线程持有的锁时,它会被阻塞,直到锁可用。
- 等待其他线程完成:线程可能需要等待其他线程执行完特定的操作或达到特定的条件,才能继续执行。
- 调用 sleep() 方法:线程可以调用 Thread.sleep() 方法进入阻塞状态,暂停一段指定时间。
- 调用 wait() 方法:线程可以在对象上调用 wait() 方法,使其进入阻塞状态,直到其他线程调用相同对象的 notify() 或 notifyAll() 方法。
要将线程从阻塞状态转换为就绪状态,需要满足特定的条件或操作:
- I/O 操作完成:当线程所需的输入/输出操作完成时,它会从阻塞状态返回到就绪状态。
- 锁可用:当线程等待的锁变为可用时,它会从阻塞状态返回到就绪状态,并尝试再次获取锁以继续执行。
- 其他线程通知:当其他线程调用相同对象的 notify() 或 notifyAll() 方法时,等待该对象的线程会从阻塞状态返回到就绪状态,然后竞争获得对象的锁以继续执行。
- sleep() 时间到期:当调用 Thread.sleep() 方法的线程休眠时间到期时,它会从阻塞状态返回到就绪状态。
- wait() 被唤醒:当其他线程调用相同对象的 notify() 或 notifyAll() 方法,或者调用 wait(long timeout) 的时间到期时,等待该对象的线程会从阻塞状态返回到就绪状态。
注意:线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,也就是说结束阻塞的线程需要重新进入可运行池中,等待系统的调度。
线程的调度
在计算机系统中,线程调度是操作系统或虚拟机对线程执行顺序和运行时间的管理。
- 分时调度模型:在这种模型中,所有的线程会轮流地获得CPU的使用权。每个线程会被分配一个时间片,当这个时间片用完之后,下一个线程就会获得CPU的使用权。这种模型的优点是可以让每个线程都有机会运行,从而实现公平的资源分配。然而,如果一个线程需要长时间运行,那么其他线程就需要等待它完成,这可能会导致程序的响应速度变慢。
- 抢占式调度模型:在这种模型中,操作系统会选择优先级高的线程来运行。如果线程的优先级相同,那么操作系统会随机选择一个线程来运行。当这个线程失去了CPU的使用权后,操作系统会选择另一个线程来运行。这种模型的优点是可以提高程序的响应速度,因为优先级高的线程可以更快地获得CPU的使用权。然而,如果一个线程长时间运行,那么其他线程就需要等待它完成,这可能会导致资源的浪费。
- Java虚拟机的调度模型:Java虚拟机默认采用抢占式调度模型。它使用了优先级调度算法来决定线程的执行顺序,并且可以通过设置线程的优先级来影响调度的结果。Java提供了一些方法来控制线程的调度,如Thread.yield() 方法可以让当前线程主动放弃CPU的使用权,以便给其他线程执行的机会。
注意:通常情况下,程序员不需要关心这个过程,因为Java虚拟机会自动处理。但是,在一些特定的需求下,可能需要改变这种模式,由程序自己来控制CPU的调度。这可以通过使用Java的Thread类和相关的API来实现。
线程状态的基本操作
协作机制
中断是一种协作机制,用于在多线程环境中控制程序的执行。中断标志位是一种内部状态,用于表示线程是否被中断。当一个线程被中断时,中断标志位将被设置为true,并且会抛出InterruptedException异常。
- public void interrupt():用于中断该线程对象。调用该方法会将线程的中断标志位设置为true。如果该线程被调用了Object wait()、Object wait(long)、Thread.sleep(long)、Thread.join()、Thread.join(long)等方法时,会抛出InterruptedException并清除中断标志位。
- public boolean isInterrupted():用于测试该线程对象是否被中断。调用该方法可以检查当前线程的中断状态,并返回一个boolean值,表示线程是否被中断。调用该方法不会清除中断标志位。
- public static boolean interrupted():用于测试当前线程是否被中断。调用该静态方法可以检查当前线程的中断状态,并返回一个boolean值,表示当前线程是否被中断。调用该方法会清除中断标志位(即将中断标志位设置为false)。
在使用这些方法时需要注意以下几点:
- 调用interrupt()方法会将线程的中断标志位设置为true。
- 被中断的线程可以通过调用isInterrupted()方法来感知其他线程对其自身的中断操作,并做出相应的响应。
- 当抛出InterruptedException时,会清除线程的中断标志位。
- 调用interrupted()方法会清除当前线程的中断标志位。
实例
下面结合具体的实例来看一看
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()方法有以下几种重载形式:
- public final void join() throws InterruptedException:当前线程调用另一个线程的join()方法,会使当前线程进入阻塞状态,直到被调用的线程执行完成。如果被调用的线程发生中断,则会抛出InterruptedException异常。
- public final synchronized void join(long millis) throws InterruptedException:当前线程调用另一个线程的join(long millis)方法,会使当前线程进入阻塞状态,最多等待指定的时间(毫秒)。如果被调用的线程在指定的时间内执行完毕,则当前线程恢复运行;如果超过指定的时间仍未执行完毕,当前线程也会恢复运行。
- public final synchronized void join(long millis, int nanos) throws InterruptedException:与上一种形式类似,但允许指定纳秒级别的额外等待时间。
通过使用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(long millis):这种形式表示当前线程休眠指定的毫秒数。
- sleep(long millis, int nanos):这种形式表示当前线程休眠指定的毫秒数和纳秒数。纳秒数范围是0到999999。
使用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.来源和使用方式:
- sleep()方法是Thread类的静态方法,可以直接通过线程对象或类名调用。它用于在指定的时间段内使当前线程休眠。
- wait()方法是Object类的实例方法,必须在同步代码块/方法中使用,通过调用对象的wait()方法来暂停当前线程,同时释放对象锁。
2.调用位置:
- sleep()方法可以在任意位置的代码中调用,无需获得对象锁。
- wait()方法必须在同步代码块/方法中调用,因为它会释放对象锁,并等待被其他线程通过notify()、notifyAll()方法唤醒。
3.被唤醒机制:
- sleep()方法在指定的时间到达后自动唤醒当前线程,然后该线程进入就绪状态等待CPU时间片。其他线程无法直接唤醒通过sleep()方法暂停的线程。
- wait()方法需要等待其他线程调用相同对象的notify()或notifyAll()方法来唤醒当前线程。
4.锁的释放:
- sleep()方法不释放对象锁,即使线程休眠,其他线程仍无法获得被当前线程持有的锁。
- wait()方法会释放对象锁,让其他线程进入对象的同步代码块/方法。
综上所述,sleep()方法主要用于线程的时间调度和暂停执行一段时间,而wait()方法主要用于线程间的协作和等待特定条件满足后再继续执行。选择使用哪种方法取决于具体的需求和场景。
线程让步
yield()是Java中的一个方法,它用于提示线程调度器当前线程愿意放弃对CPU的使用权。当一个线程调用yield()方法时,它就会让出自己的时间片,告诉调度器可以先执行其他优先级相同或更高的线程。
1.调用方式:
- yield()方法是Thread类的静态方法,可以直接通过线程对象或类名调用。
- 例如,Thread.yield(); 或者 Thread.currentThread().yield();
2.功能和作用:
- yield()的作用是暂停当前正在执行的线程,并给予其他等待线程执行的机会。
- 调用yield()方法的线程进入就绪状态,等待调度器重新选择执行。
3.调度器行为:
- 调用yield()方法不保证当前线程会立即暂停执行,也无法保证其他线程会立即得到执行。
- 具体的调度器行为取决于操作系统和Java虚拟机的实现,可能有一定的优化策略。
4.适用场景:
- yield()方法常用于协助线程间的合理调度,尤其在具有相同优先级且需要公平共享CPU资源的情况下。
- 它可以用于避免某个线程过度占用CPU资源,提高系统的整体性能和公平性。
需要注意的是,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资源的行为。
扩展
线程状态的基本操作还包括以下几种:
- start:启动线程,调用线程对象的start()方法。
- run:在start()方法调用后,线程进入可运行状态,当线程调度选中该线程时,run()方法开始执行。
- resume:恢复阻塞状态的线程,使该线程回到就绪状态。
- block:将当前线程放入一个阻塞队列中,进入阻塞状态。
- newLock:获取新的锁,使线程进入等待状态,直到获得锁为止。
- lock:获取锁,如果锁已经被其他线程持有,则当前线程进入等待状态,直到获得锁为止。
- unlock:释放锁,使等待该锁的线程能够获得锁并继续执行。
- destroy:销毁线程对象,使该线程处于死亡状态。
这些操作都是线程状态转换的基本操作,可以帮助我们更好地管理和控制线程的执行。
进程和线程
进程和线程的详细区别请参考并发编程基础知识篇--进程和线程的区别
线程的优先级
Java中的线程优先级用于指定线程对CPU资源的获取优先级。每个线程都有一个默认的优先级,范围从1(最低)到10(最高)。可以使用setPriority(int priority)方法设置线程的优先级,其中priority参数表示新的线程优先级。
以下是关于线程优先级的一些要点:
- 默认优先级:
- 每个线程在创建时都会继承其父线程的优先级。
- 主线程的默认优先级为Thread.NORM_PRIORITY(常数 5)。
- 优先级范围:
- 线程优先级的范围是从1到10。
- 具有较高优先级的线程更有可能获得CPU时间片,但并不保证绝对顺序执行。
- 设置方法:
- 使用setPriority(int priority)方法设置线程的优先级。
- 优先级值必须在Thread.MIN_PRIORITY(常数 1)和Thread.MAX_PRIORITY(常数 10)之间。
- 调度器行为:
- 调度器会尽量根据线程的优先级安排处理器资源。
- 但是,具体的调度行为依赖于操作系统和Java虚拟机的实现,可能因平台而异。
- 注意事项:
-
虽然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)。
用户线程(User Thread):
- 用户线程是最常见的线程类型。
- 当创建一个线程时,默认情况下它是用户线程。
- 用户线程的目标是完成特定的任务或工作,并且当所有的用户线程都结束时,JVM 就会退出。
守护线程(Daemon Thread):
- 守护线程是一种在后台运行的线程,它的作用是为其他线程提供服务支持。
- 守护线程的目标是处理一些系统级的任务,如垃圾回收(Garbage Collection)等。
- 当所有的用户线程都结束时,无论守护线程是否执行完毕,JVM 都会自动退出。
关于守护线程和用户线程的要点:
- 当线程被创建时,默认情况下它继承了父线程的属性,包括守护状态。
- 在守护线程中产生的新线程也是守护线程
- 可以使用setDaemon(boolean on)方法将线程设置为守护线程或非守护线程。该方法必须在线程启动之前(start( ))调用,否则会抛出
IllegalThreadStateException
异常。 - 守护线程的优先级通常较低,因为它们主要负责后台任务,并不需要太多的CPU资源。
- 守护线程不能访问关键资源或执行需要确保完整性的任务,比如读写操作或者计算逻辑,因为它们可能在任何时候被强制停止。
- 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为上面也说过了一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行。
- 用户线程和守护线程在运行行为上没有明显的区别,唯一的区别是守护线程存在的目的是为了支持其他线程。
请注意,如果将所有的用户线程都设置为守护线程,那么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的锁。通过统一的资源获取顺序,可以避免死锁的发生。