目录
创建线程的四种方式
线程的状态和生命周期
扩展知识
线程的调度
线程状态的基本操作
interrupted
实例
join
实例
sleep
实例
扩展小知识
yield
实例
扩展
创建线程的四种方式
创建线程的四种方式
- 继承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来实现。
线程状态的基本操作
interrupted
中断是一种协作机制,用于在多线程环境中控制程序的执行。中断标志位是一种内部状态,用于表示线程是否被中断。当一个线程被中断时,中断标志位将被设置为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
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 已完成.
所有线程都已完成.
sleep
在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
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:销毁线程对象,使该线程处于死亡状态。
这些操作都是线程状态转换的基本操作,可以帮助我们更好地管理和控制线程的执行。