java复习 06
线程还没学会,然后查漏补缺。再学一下泛型,下一篇博客写。
1 线程控制
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
void join() | 等待这个线程死亡 |
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,Java 虚拟机将退出 |
sleep方法的应用,这里用trycatch包围
package PTA_training.Thread_training;public class ThreadSleepDemo {public static void main(String[] args) {ThreadSleep s1 = new ThreadSleep();ThreadSleep s2 = new ThreadSleep();ThreadSleep s3 = new ThreadSleep();s1.setName("啊");s2.setName("a");s3.setName("1");s1.start();s2.start();s3.start();}}
package PTA_training.Thread_training;public class ThreadSleep extends Thread{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(getName() + ":" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
join方法使用后,要等这个线程死亡了以后才会执行新的线程
!
看起来就是现在都是在执行这个(给了优先级,但和优先级的概念定义是不一样的!!!!要是现在忘记setpriority的掉头看以前的博客!!!!!)
Java 中 setDaemon
作用:标记线程为守护线程(Daemon Thread)。JVM 中若所有运行线程都是守护线程,JVM 会退出。比如垃圾回收线程就是守护线程,辅助程序清理内存,不影响程序核心逻辑,程序结束时无需等它手动收尾。
使用规则:必须在线程 start()
启动前调用 ,否则抛 IllegalThreadStateException
异常。
package PTA_training.Thread_training;public class ThreadSleepDemo {public static void main(String[] args) {ThreadSleep s1 = new ThreadSleep();ThreadSleep s2 = new ThreadSleep();s1.setName("啊");s2.setName("a");//设置主线程为1Thread.currentThread().setName("1");//设置守护线程s1.setDaemon(true);s2.setDaemon(true);s1.start();s2.start();for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+":"+i);}}
}
package PTA_training.Thread_training;public class ThreadSleep extends Thread{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(getName() + ":" + i);}}
}
2 线程的生命周期
线程生命周期描述了线程从创建到销毁的整个过程,Java 线程(以常见的线程模型为例 )生命周期主要包含以下 5 个阶段,对应状态及转换逻辑如下:
1. 新建(New)
- 状态说明:通过
new Thread()
等方式创建线程对象,但还未调用start()
方法。此时线程仅在内存中存在,未与操作系统底层线程关联,不具备 “运行潜力”。 - 典型操作:
Thread thread = new Thread(() -> { /* 任务逻辑 */ });
2. 就绪(Runnable)
- 状态说明:调用
start()
方法后,线程进入就绪态。此时线程已 “激活”,具备执行资格(可参与 CPU 竞争),但没有执行权(需等待 CPU 调度 )。JVM 会把它放到 “就绪队列”,等待操作系统分配 CPU 时间片。 - 状态转换:
- 从 新建 来:
start()
调用后进入就绪。 - 从 阻塞 / 运行 回:若线程因
sleep
时间到、阻塞解除(如锁释放 )、被其他线程抢走 CPU 执行权等,会回到就绪态,重新等 CPU 调度。
- 从 新建 来:
3. 运行(Running)
- 状态说明:就绪线程抢到 CPU 执行权后,进入运行态,开始执行
run()
里的逻辑。此时线程真正在 “干活”,占据 CPU 资源处理任务。 - 状态转换:
- 从 就绪 来:CPU 调度选中线程,切换到运行态。
- 到 就绪 / 阻塞 / 死亡:
- 若 CPU 时间片用完(或被更高优先级线程抢占 ),回到就绪态;
- 若执行
sleep()
、等待锁、IO 阻塞等操作,进入阻塞态; - 若
run()
正常结束或调用stop()
(已被标记为 “过时”,不推荐用,易引发问题 ),进入死亡态。
4. 阻塞(Blocked)
- 状态说明:线程因特定原因暂时失去执行资格(既没执行权,也无法竞争 CPU ),需等 “阻塞条件解除” 才能回归就绪态。
- 触发场景(图中示例 ):
- 调用
sleep(long millis)
:线程休眠指定毫秒,期间不参与 CPU 竞争,时间到后回到就绪态; - 其他阻塞操作:比如等待 synchronized 锁、IO 等待(读文件 / 网络请求 )等,需等锁拿到、IO 完成,才会解除阻塞。
- 调用
- 状态转换:阻塞条件消失(如
sleep
时间到、锁可用 )后,回到就绪态,重新等 CPU 调度。
5. 死亡(Terminated)
- 状态说明:线程执行完毕(
run()
正常结束 )或被强制终止(如调用stop()
,但不推荐 ),进入死亡态。此时线程生命周期结束,无法再回到其他状态,会被 JVM 回收资源。 - 触发方式:
- 自然死亡:
run()
方法逻辑执行完,线程正常退出; - 强制死亡:调用
stop()
(风险高,可能导致资源未释放、数据不一致,现代开发更推荐通过标志位等 “优雅终止” 方式 )。
- 自然死亡:
关键逻辑梳理
- 核心是 “状态流转”:新建→就绪→运行→(就绪 / 阻塞 / 死亡 )→…→死亡。
- 线程能否执行任务,核心看是否拿到 CPU 执行权(就绪态竞争、运行态干活 );阻塞态是 “被迫暂停”,需等条件满足才能重新竞争。
- 实际开发中,要注意线程阻塞可能导致的性能问题(如大量线程因锁阻塞 ),以及合理控制线程生命周期(避免滥用
stop()
,用标志位让线程 “优雅退出” )。
简单说,线程就像 “打工人”:新建是刚入职还没开工,就绪是排队等 “分配工作(CPU)”,运行是正在干活,阻塞是被迫暂停(等审批 / 等资源 ),死亡是干完活下班~ 理解这几个阶段,就能更好把控多线程程序的执行流程啦 。
3 写编程题的感受......(踩坑实录)
初始代码
package PTA_training.Test4_7;import java.util.Scanner;/*
7-7 创建一个倒数计数线程
创建一个倒数计数线程。
要求:
1.该线程使用实现Runnable接口的写法;
2.程序该线程每隔0.5秒打印输出一次倒数数值(数值为上一次数值减1)。输入格式:
N(键盘输入一个整数)输出格式:
每隔0.5秒打印输出一次剩余数输入样例:
6
输出样例:
在这里给出相应的输出。例如:6
5
4
3
2
1
0*/
public class Main {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);int n = scanner.nextInt();Thread t1 = new Thread(new TimeCounter(n));try {Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}scanner.close();t1.start();}
}
package PTA_training.Test4_7;public class TimeCounter implements Runnable{private int n;public TimeCounter(int n) {this.n = n ;}@Overridepublic void run() {for (int i = n ; i < 0 ; i--) {System.out.println(i);}}
}
这段代码存在几个问题,下面来详细分析并给出解决方案:
问题分析
-
for
循环条件错误:在TimeCounter
类的run
方法里,for
循环的条件i < 0
有误。从n
开始倒数,循环条件应当是i >= 0
,这样才能正确输出从n
到 0 的所有数字。(哈哈...哈哈哈......我好蠢啊......) -
时间间隔设置错误:在
Main
类中,Thread.sleep(500)
被放在了启动线程之前,这就意味着主线程会休眠 0.5 秒,而不是让线程每隔 0.5 秒输出一次。!正确的做法是把Thread.sleep(500)
放到TimeCounter
类的run
方法中。 -
异常处理问题:在
Main
类里,Thread.sleep(500)
抛出的InterruptedException
被重新抛出为RuntimeException
,这会导致程序崩溃。应该在TimeCounter
类的run
方法里处理InterruptedException
。
最终代码:
package PTA_training.Test4_7;public class TimeCounter implements Runnable {private int n;public TimeCounter(int n) {this.n = n;}@Overridepublic void run() {for (int i = n; i >= 0; i--) {System.out.println(i);try {// 线程休眠 0.5 秒Thread.sleep(500); } catch (InterruptedException e) {// 处理中断异常Thread.currentThread().interrupt(); }}}
}
package PTA_training.Test4_7;import java.util.Scanner;/*
7-7 创建一个倒数计数线程
创建一个倒数计数线程。
要求:
1.该线程使用实现Runnable接口的写法;
2.程序该线程每隔0.5秒打印输出一次倒数数值(数值为上一次数值减1)。输入格式:
N(键盘输入一个整数)输出格式:
每隔0.5秒打印输出一次剩余数输入样例:
6
输出样例:
在这里给出相应的输出。例如:6
5
4
3
2
1
0*/
public class Main {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);int n = scanner.nextInt();Thread t1 = new Thread(new TimeCounter(n));scanner.close();t1.start();}
}
4 Java 多线程实现的两种方式
Java 提供了两种主要方式来实现多线程编程:通过继承 Thread 类和通过实现 Runnable 接口。这两种方式各有特点,下面我将详细介绍它们的实现方法和区别。
1. 通过继承 Thread 类实现多线程
这是实现多线程的最直接方式,通过创建一个继承自 Thread 类的子类,并重写其 run () 方法:
public class RunnableWithThreadMethods {public static void main(String[] args) {// 创建Runnable任务MyTask task = new MyTask();// 创建多个线程共享同一个任务Thread thread1 = new Thread(task);Thread thread2 = new Thread(task);// 设置线程名称thread1.setName("工作线程-1");thread2.setName("工作线程-2");// 设置线程优先级thread1.setPriority(Thread.MAX_PRIORITY);thread2.setPriority(Thread.MIN_PRIORITY);// 启动线程thread1.start();thread2.start();}
}class MyTask implements Runnable {@Overridepublic void run() {// 获取当前执行该任务的线程Thread currentThread = Thread.currentThread();// 使用线程的各种方法System.out.println("线程名称: " + currentThread.getName());System.out.println("线程优先级: " + currentThread.getPriority());System.out.println("线程ID: " + currentThread.getId());System.out.println("线程状态: " + currentThread.getState());System.out.println("是否为守护线程: " + currentThread.isDaemon());// 在线程中调用sleep方法try {System.out.println(currentThread.getName() + " 开始休眠");Thread.sleep(2000); // 等同于 currentThread.sleep(2000)System.out.println(currentThread.getName() + " 休眠结束");} catch (InterruptedException e) {System.out.println(currentThread.getName() + " 被中断");}}
}
这种方式的核心是继承 Thread 类并重写 run () 方法,然后通过调用 start () 方法启动线程。需要注意的是,直接调用 run () 方法不会创建新线程,只是在当前线程中执行方法体。
2. 通过实现 Runnable 接口实现多线程
这是更常用的实现多线程的方式,通过创建一个实现 Runnable 接口的类,然后将其作为参数传递给 Thread 类的构造函数:
public class RunnableExample {public static void main(String[] args) {// 创建Runnable实例MyRunnable runnable1 = new MyRunnable("任务1");MyRunnable runnable2 = new MyRunnable("任务2");// 创建线程实例,将Runnable对象作为参数传入Thread thread1 = new Thread(runnable1);Thread thread2 = new Thread(runnable2);// 启动线程thread1.start();thread2.start();}
}// 实现Runnable接口创建线程任务
class MyRunnable implements Runnable {private String name;public MyRunnable(String name) {this.name = name;}// 实现run()方法,定义线程执行的任务@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println(name + " 运行: " + i);try {// 线程休眠,模拟耗时操作Thread.sleep((int)(Math.random() * 1000));} catch (InterruptedException e) {e.printStackTrace();}}}
}
这种方式的优点是实现了任务与线程的分离,Runnable 对象代表要执行的任务,而 Thread 对象负责线程的创建和管理。
两种方式的对比
特性 | 继承 Thread 类 | 实现 Runnable 接口 |
---|---|---|
编程复杂度 | 简单,直接继承并重写 run () 方法 | 稍复杂,需要创建 Runnable 对象并传递给 Thread |
资源占用 | 每个线程都需要创建一个独立的 Thread 对象,资源占用较多 | 多个线程可以共享同一个 Runnable 对象,资源占用较少 |
扩展性 | 由于 Java 不支持多重继承,继承 Thread 后不能再继承其他类 | 可以继承其他类并实现 Runnable 接口,扩展性更好 |
代码复用性 | 低,每个线程类只能完成特定任务 | 高,同一个 Runnable 可以被多个线程共享 |
适用场景 | 简单任务,不需要共享资源的场景 | 复杂任务,需要共享资源的场景 |
推荐使用方式
在实际开发中,更推荐使用实现 Runnable 接口的方式,原因如下:
- 避免了 Java 单继承的限制,类可以同时继承其他类并实现多线程
- 更好地实现了任务与线程的分离,提高了代码的可维护性和复用性
- 更适合需要共享资源的场景,多个线程可以共享同一个 Runnable 实例
- 符合面向对象的设计原则,任务逻辑和线程管理职责分明
无论选择哪种方式,都要记住:线程的启动必须通过调用 start () 方法,而不是直接调用 run () 方法。
5 如果在runnable接口里面想要实现thread的方法setName应该怎么做?这种做法又叫什么?
在 Runnable 接口中使用 Thread 的 setName 方法
在 Java 中,Runnable 接口本身并没有提供setName()
方法,因为它只定义了一个run()
方法。要在使用 Runnable 接口的同时设置线程名称,你需要通过 Thread 对象来实现。
这实际上是一种 ** 组合(Composition)** 设计模式的应用。组合是一种比继承更灵活的代码复用方式,它通过将一个类的实例作为另一个类的成员变量来实现功能复用,而不是通过继承。
下面是如何在使用 Runnable 接口时设置线程名称的示例:
public class RunnableWithName {public static void main(String[] args) {// 创建Runnable任务MyRunnable task = new MyRunnable();// 创建Thread并设置名称!!!!!!!!!!Thread thread1 = new Thread(task, "线程A");Thread thread2 = new Thread(task, "线程B");// 或者通过setName方法设置Thread thread3 = new Thread(task);thread3.setName("线程C");// 启动线程thread1.start();thread2.start();thread3.start();}
}// 实现Runnable接口的任务类
class MyRunnable implements Runnable {@Overridepublic void run() {// 获取当前线程名称String threadName = Thread.currentThread().getName();for (int i = 0; i < 5; i++) {System.out.println(threadName + " 运行: " + i);try {Thread.sleep((int)(Math.random() * 1000));} catch (InterruptedException e) {e.printStackTrace();}}}
}
组合与继承的对比
这种做法体现了 "组合优于继承" 的设计原则:
-
组合(Composition):
- 通过将 Thread 对象作为载体,将 Runnable 任务组合到 Thread 中
- 可以在运行时动态改变线程的行为
- 避免了继承的限制(Java 单继承)
- 更符合单一职责原则,Runnable 专注于定义任务,Thread 负责线程管理
-
继承(Inheritance):
- 通过继承 Thread 类直接获取其所有方法
- 关系更紧密,但灵活性较差
- 可能导致类层次结构复杂
- 受到 Java 单继承的限制
实际应用
在实际开发中,组合方式更为常用,特别是在需要共享资源的场景中:
- 多个线程可以共享同一个 Runnable 实例
- 可以在不修改 Runnable 代码的情况下,为线程添加额外功能(如设置优先级、守护线程等)
- 符合面向对象设计中的依赖倒置原则,高层模块(线程)不依赖于低层模块(具体实现)的细节
通过这种方式,你可以在保持代码灵活性和可维护性的同时,充分利用 Thread 类提供的各种功能。
6 意思就是Thread.currentThread()然后点调用对吧?!是的 !
是的,你理解得完全正确!
在 Runnable 接口的实现中,你可以通过Thread.currentThread()
获取当前执行该 Runnable 的线程对象,然后调用线程的所有方法,包括setName()
、sleep()
、interrupt()
等。
这种方式的核心在于:Runnable 接口定义了要执行的任务,而 Thread 类负责线程的创建和管理。当你在 Runnable 的run()
方法中使用Thread.currentThread()
时,你实际上是在访问执行该任务的线程对象。
下面是一个更清晰的示例,展示如何在 Runnable 中使用 Thread 的各种方法:
这种方式的优势
- 解耦设计:将任务逻辑与线程管理分离,使代码更清晰
- 资源共享:多个线程可以共享同一个 Runnable 实例,实现资源共享
- 灵活配置:可以为每个线程单独配置属性(名称、优先级、守护状态等)
- 扩展性强:可以轻松实现更复杂的线程池、并发控制等功能
关于 "这种做法叫什么"
这种做法在设计模式中被称为组合模式(Composition Pattern),它是 "组合优于继承" 设计原则的具体应用。通过组合,你可以在不继承 Thread 类的情况下,利用 Thread 类提供的功能。
另外,从更广泛的角度看,这也是 ** 依赖注入(Dependency Injection)** 的一种形式,因为 Runnable 任务依赖于 Thread 对象来执行。
在实际开发中,这种方式是实现多线程的首选方法,特别是在需要共享资源或实现复杂并发逻辑的场景中。
7 小结一下多线程的实现方式
方式 2:实现 Runnable 接口
- 定义一个类 MyRunnable 实现 Runnable 接口
- 在 MyRunnable 类中重写 run () 方法
- 创建 MyRunnable 类的对象
- 创建 Thread 类的对象,把 MyRunnable 对象作为构造方法的参数
- 启动线程
多线程的实现方案有两种
- 继承 Thread 类
- 实现 Runnable 接口
相比继承 Thread 类,实现 Runnable 接口的好处
- 避免了 Java 单继承的局限性
- 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想