文章目录
一. Thread
1. Thread 类常见方法
- 构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象并命名 |
Thread(Runnable target, String name) | 使用Runnable对象创建线程对象并命名 |
- 属性方法
属性 | 获取方法 | 说明 |
---|---|---|
ID | getId() | ID 是线程的唯⼀标识,不同线程不会重复 |
名称 | getName() | 名称在各种调试⼯具将被⽤到 |
状态 | getState() | 获取线程当前所处的⼀个状态 |
优先级 | getPriority() | 优先级⾼的线程理论上来说更容易被调度到 |
是否置为后台线程 | isDaemon() | JVM会在⼀个进程的所有⾮后台线程结束后,才会结束运⾏。(后文重点) |
是否存活 | isAlive() | run ⽅法是否运⾏结束了 |
是否被中断 | isInterrupted() | 判断一个线程是否被中断。(后文重点) |
2. 线程的创建
- 通过Thread 子类创建线程
// thread 子类
class MyThread extends Thread {
@Override
public void run() {
while(true) {
System.out.println("hello thread");
}
}
}
public class Demo2 {
public static void main(String[] args) {
Thread t = new MyThread();
//start() 方法启动t线程
t.start();
while(true) {
System.out.println("hello main");
}
}
}
在以上代码中,通过 Thread子类 重写 Run 方法,创建 t 线程,其中 main 函数也是线程且为主线程,主线程最先被创建和执行。
- 使用Runnable对象创建线程
// 实现Runnable 接口
class MyRunnable implements Runnable{
@Override
public void run() {
while(true) {
System.out.println("hello Thread");
}
}
}
public class Demo1 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
while(true){
System.out.println("hello main");
}
}
}
创建一个实现了Runnable接口的类并重写Run方法,通过给Thread构造方法中传入该类的实例来创建线程。
- 通过Thread子类创建线程(匿名内部类)
// thread 子类 匿名内部类
public class Demo3 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
while(true) {
System.out.println("hello thread");
}
}
};
t.start();;
while(true) {
System.out.println("hello main");
}
}
}
该方法本质上还是创建Thread子类,只不过该子类为匿名内部类,是对Thread子类创建方式的一种简化,即不关注Thread子类,创建完子类实例后便不在使用该子类,适合于轻量级编程,但在run方法比较复杂的情景下不建议使用。
- 使用Runnable对象创建线程(匿名内部类)
// runnable 匿名内部类
public class Demo4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
System.out.println("hello thread");
}
}
});
while(true) {
System.out.println("hello main");
}
}
}
通过匿名内部类创建Runnable实例的方法与通过匿名内部类创建Thread类实例的方法一致。
- 使用lambda 表达式创建线程
// lambda 表达式
public class Demo5 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while(true){
System.out.println("hello thread");
}
});
t.start();
while(true) {
System.out.println("hello main");
}
}
}
lambda 表达式是通过匿名内部类创建实例的又一次简化,lambda 表达式只关注 run 方法的实现,而忽略该实例所属的类,这使得代码的可读性更强,但是同样不适用于run方法复杂的情景。
- start 方法 和 run 方法的区别
在以上代码中,你或许会有这样的疑问,既然已经重写了run方法,为啥不直接调用重写的 run方法 而选择使用 start方法 呢?这就与多线程机制密切相关了。
-
start
当你调用一个线程的start()方法时,Java虚拟机(JVM)会为该线程分配必要的系统资源,并调度该线程的执行。start()方法会自动调用该线程的run()方法。但需要注意的是,start()方法只会被调用一次,多次调用会导致IllegalThreadStateException。
start()方法是异步的,即它不会等待run()方法执行完成就返回。
-
run
run()方法是Thread类的一个普通方法,但它是被设计为线程执行的入口点。当你继承Thread类并创建线程对象时,通常需要覆盖(Override)run()方法,以定义线程需要执行的任务。
直接调用run()方法会在调用它的线程中执行run()方法体中的代码,而不是启动一个新的线程来执行。
run()方法的调用是同步的,即它会按照代码顺序执行,直到执行完毕。
多线程机制:
使用start()方法时,会启动一个新的线程来执行run()方法中的代码。这意呀着run()方法中的代码会在新的线程中并发执行,不会阻塞调用start()方法的线程。这使得程序能够同时执行多个任务,提高了程序的执行效率。
直接调用run()方法:直接调用run()方法时,并不会启动新的线程,而是直接在当前线程中顺序执行run()方法中的代码。这意味着run()方法中的代码执行会阻塞调用它的线程,直到run()方法执行完毕。这与多线程的初衷相悖,因为并没有实现真正的并发执行。
3. 线程的休眠
方法 | 说明 |
---|---|
public static void sleep(long millis) throws InterruptedException | 让当前正在执行的线程暂停执行指定的毫秒数(ms) |
public static void sleep(long millis, int nanos) throws InterruptedException | millis 是暂停的时间(以毫秒为单位),而 nanos 是额外的暂停时间(以纳秒为单位) |
注意:
- 调用 sleep 方法时,如果当前线程被中断(即收到了中断信号),那么 sleep 方法会抛出一个 InterruptedException 异常。这个异常是一个受检异常(checked exception),所以你需要捕获它或者在你的方法签名中声明抛出它。
- sleep 方法是静态的,意味着它可以直接通过类名来调用,而不是通过类的实例。这意味着你可以在任何地方调用 Thread.sleep(),而不需要先创建一个 Thread 对象。
- sleep 方法是Java中处理线程暂停的一种方式,但它并不会释放任何锁。如果你想要释放锁并让其他线程有机会执行,那么可能需要考虑使用其他同步机制,比如 wait/notify 或者 Lock/Condition。
- 因为线程的调度是不可控的,所以线程休眠的时间会大于或者等于所设定的毫秒数。
示例:
public class SleepExample {
public static void main(String[] args) {
Thread t = new Thread(() -> {
try {
System.out.println("线程开始执行,时间:" + System.currentTimeMillis());
Thread.sleep(3000); // 让线程暂停3秒
System.out.println("线程继续执行,时间:" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
}
}
4. 线程的等待
方法 | 说明 |
---|---|
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等millis毫秒 |
public void join(long millis, int nanos) | 等待线程结束,但精度更高 |
案例:
public class JoinExample {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println("线程1开始执行");
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1执行完毕");
});
Thread thread2 = new Thread(() -> {
System.out.println("线程2开始执行");
try {
// 等待线程1执行完毕
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2在线程1之后执行完毕");
});
thread1.start();
thread2.start();
}
}
在这个示例中,thread2 在开始执行后会立即尝试调用 thread1.join(),这会使得 thread2 暂停执行,直到 thread1 执行完毕。因此,输出将会是:
线程1开始执行
线程2开始执行
线程1执行完毕
线程2在线程1之后执行完毕
如果把上述代码稍作修改,将线程 thread1 和 thread2 的启动顺序调换位置,即:
thread2.start();
thread1.start();
就会出现 thread2 启动时,thread1 还没有启动的情况,根据join方法的源码,如果join方法被执行时,对应被等待的线程没有启动,则join方法就会认为该线程已经执行完毕,所以如果 thread2中 join方法 被执行时 thread1 还没有启动,join方法 会认为 thread1 已经执行完毕,thread2 将不会等待 thread1 继续执行,我们将看到以下结果:
线程2开始执行
线程2在线程1之后执行完毕
线程1开始执行
线程1执行完毕
但是,如果我们将以上修改后的代码在自己的idea上运行,我们可能也会看到,这样的结果:
线程2开始执行
线程1开始执行
线程1执行完毕
线程2在线程1之后执行完毕
看到这,也许有的读者也跟我一样,已经开始晕头转向了:为什么修改后 thread2 仍然会等待 thread1 执行完毕呢?
这是多线程的调度机制在作怪。当 thread2.start()
被执行后,程序就会出现两个线程,主线程和 thread2 线程,但是因为多线程调度的不可控性,系统会根据运行环境给这两个线程分配cpu时间,但这是我们无法预知的,因此在执行完 thread2.start()
后,可能是 thread2 占据cpu时间执行run方法,也可能是 main 主线程 占据cpu时间继续执行 thread1.start()
,如果是后者,那么在执行thread1.join()
前,thread1就已经启动,所以就会出现以上线程2继续等待线程1执行完的结果了。
5. 获取当前线程的引用
方法 | 说明 |
---|---|
public static Thread currentThread() | 返回当前线程的引用 |
示例:
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
6. 线程的中断
Thread 类提供了三个与中断相关的方法:
方法 | 说明 |
---|---|
public void interrupt() | 中断对象关联的线程 |
public static boolean interrupted() | 判断当前线程是否处于中断状态,如果是返回true,且清除该线程的中断状态 |
public boolean isInterrupted() | 判断对象关联的线程是否处于中断状态 |
案例:
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
// 两种⽅法均可以
while (!Thread.interrupted()) {
//while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()
+ ": 有内⻤,终⽌交易!");
// 注意此处的 break
break;
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了⼤事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": ⽼板来电话了,得赶紧通知李四对⽅是个骗⼦!");
thread.interrupt();
}
}
解析:
这段代码展示了如何在Java中使用Thread
类和Runnable
接口来创建并控制线程。在这个例子中,我们创建了一个名为MyRunnable
的类,它实现了Runnable
接口,并重写了run
方法。run
方法是线程执行时调用的方法。
-
线程中断:
Thread.interrupt()
方法用于请求中断线程。这并不意味着线程会立即停止执行,而是设置了一个中断状态。- 线程中的代码需要检查这个中断状态,并适当地响应。这通常通过检查
Thread.interrupted()
或Thread.currentThread().isInterrupted()
方法来完成。 Thread.interrupted()
会清除当前线程的中断状态并返回之前的中断状态。Thread.currentThread().isInterrupted()
仅检查当前线程的中断状态,不会清除它。
-
循环控制:
- 在
run
方法中,使用了一个while
循环来模拟一个持续进行的任务(如转账)。 - 循环条件使用了
Thread.interrupted()
(或注释中的Thread.currentThread().isInterrupted()
),这决定了线程是否应该继续执行。 - 当线程被中断时(通过
interrupt()
方法),Thread.interrupted()
将返回true
,循环结束。
- 在
-
异常处理:
- 在循环内部,
Thread.sleep(1000)
用于模拟耗时操作。如果线程在睡眠时被中断,InterruptedException
将被抛出。 - 捕获
InterruptedException
后,打印一条消息表示交易被终止,并通过break
跳出循环。
- 在循环内部,
-
主线程控制:
- 在
main
方法中,创建了一个MyRunnable
实例和一个线程thread
,将MyRunnable
作为任务传递给thread
。 - 调用
thread.start()
启动线程。 - 主线程通过
Thread.sleep(10 * 1000)
等待一段时间(10秒),然后调用thread.interrupt()
来请求中断thread
线程。
- 在
注意事项:
-
当你选择使用
Thread.interrupted()
或Thread.currentThread().isInterrupted()
时,应考虑它们的行为差异。在这个例子中,使用Thread.interrupted()
是合适的,因为它在检查中断状态后会清除它,这有助于防止在捕获InterruptedException
之后立即进行的检查再次误判为中断状态。 -
捕获
InterruptedException
后,通常的做法是通过设置某种状态或抛出另一种异常来向上层报告中断,或者像这里一样,简单地终止循环。 -
线程的中断是一个协作机制,它依赖于线程代码定期检查中断状态并适当地响应。如果线程代码没有检查中断状态,那么中断请求可能会被忽略。
-
当捕获
InterruptedException
后,中断状态本身不会自动消除。InterruptedException
的抛出是对线程已经处于中断状态的一个响应,而这个中断状态是由调用Thread.interrupt()
方法设置的。捕获InterruptedException
只是意味着你的代码已经注意到了这个中断事件,并有机会做出相应的处理(比如清理资源、记录日志、重新尝试操作等),但它并不改变中断状态。
因此,如果你在捕获InterruptedException
后需要基于中断状态做出决策,你应该直接使用Thread.currentThread().isInterrupted()
来检查中断状态,因为它不会清除中断状态。如果你只是想响应中断并清除中断状态以便继续执行其他操作,那么使用Thread.interrupted()
是可以的,但请注意它会改变中断状态。
7. 后台线程
后台线程(Daemon Thread),也称为守护线程,是在Java等编程语言中用于支持程序运行的线程,但它不会阻止程序的终止。当JVM(Java虚拟机)中只剩下后台线程时,JVM会退出。这意味着后台线程的存在不会阻止程序的正常关闭过程。
7.1 后台线程的特点:
-
不阻止JVM退出:当所有非后台线程结束时,JVM会自动退出,即使后台线程仍在运行。这是后台线程与非后台线程(也称为用户线程)的主要区别。
-
用途:后台线程通常用于执行一些后台任务,如垃圾收集、数据库连接池维护、定时任务等。这些任务不需要在用户程序结束时还继续运行。
-
设置方法:在Java中,可以通过调用线程实例的
setDaemon(true)
方法将线程设置为后台线程。但是,这个方法必须在线程启动之前调用,否则会抛出IllegalThreadStateException
异常。
7.2 示例代码:
public class DaemonThreadExample {
public static void main(String[] args) {
// 创建一个线程
Thread daemonThread = new Thread(() -> {
while (true) {
try {
// 模拟后台任务
System.out.println("Daemon Thread is running...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 将线程设置为后台线程
daemonThread.setDaemon(true);
// 启动线程
daemonThread.start();
// main线程(非后台线程)执行完毕,JVM会退出,即使后台线程仍在运行
try {
// 主线程休眠2秒,以便看到后台线程的输出
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread is finished.");
// 当main线程结束时,由于没有其他非后台线程在运行,JVM将退出,尽管daemonThread仍在运行
}
}
注意事项:
- 程序中至少要有一个非后台线程(通常是main线程),否则JVM会立即退出,因为没有非后台线程来执行程序。
- 后台线程通常用于执行那些对程序运行不太关键的任务,例如,日志记录、状态监视等。
- 如果JVM在后台线程还在运行时退出,这些后台线程会立即停止运行,不会进行正常的清理操作。因此,在设计后台线程时,应确保它们能够优雅地处理这种情况,避免数据损坏或资源泄露。
结语:
以上大部分代码案例都没有给出实际的输出结果,这里希望各位读者能够多动手实践,熟能生巧。以上资料有任何错误的地方可以私信我或者在评论区留言。