线程还没学会,然后查漏补缺。再学一下泛型,下一篇博客写。
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
void join() | 等待这个线程死亡 |
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,Java 虚拟机将退出 |
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{
@Override
public 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);
}
}
}
}
看起来就是现在都是在执行这个(给了优先级,但和优先级的概念定义是不一样的!!!!要是现在忘记setpriority的掉头看以前的博客!!!!!)
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");
//设置主线程为1
Thread.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{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":" + i);
}
}
}
线程生命周期描述了线程从创建到销毁的整个过程,Java 线程(以常见的线程模型为例 )生命周期主要包含以下 5 个阶段,对应状态及转换逻辑如下:
new Thread()
等方式创建线程对象,但还未调用 start()
方法。此时线程仅在内存中存在,未与操作系统底层线程关联,不具备 “运行潜力”。Thread thread = new Thread(() -> { /* 任务逻辑 */ });
start()
方法后,线程进入就绪态。此时线程已 “激活”,具备执行资格(可参与 CPU 竞争),但没有执行权(需等待 CPU 调度 )。JVM 会把它放到 “就绪队列”,等待操作系统分配 CPU 时间片。start()
调用后进入就绪。sleep
时间到、阻塞解除(如锁释放 )、被其他线程抢走 CPU 执行权等,会回到就绪态,重新等 CPU 调度。run()
里的逻辑。此时线程真正在 “干活”,占据 CPU 资源处理任务。sleep()
、等待锁、IO 阻塞等操作,进入阻塞态;run()
正常结束或调用 stop()
(已被标记为 “过时”,不推荐用,易引发问题 ),进入死亡态。sleep(long millis)
:线程休眠指定毫秒,期间不参与 CPU 竞争,时间到后回到就绪态;sleep
时间到、锁可用 )后,回到就绪态,重新等 CPU 调度。run()
正常结束 )或被强制终止(如调用 stop()
,但不推荐 ),进入死亡态。此时线程生命周期结束,无法再回到其他状态,会被 JVM 回收资源。run()
方法逻辑执行完,线程正常退出;stop()
(风险高,可能导致资源未释放、数据不一致,现代开发更推荐通过标志位等 “优雅终止” 方式 )。stop()
,用标志位让线程 “优雅退出” )。简单说,线程就像 “打工人”:新建是刚入职还没开工,就绪是排队等 “分配工作(CPU)”,运行是正在干活,阻塞是被迫暂停(等审批 / 等资源 ),死亡是干完活下班~ 理解这几个阶段,就能更好把控多线程程序的执行流程啦 。
初始代码
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 ;
}
@Override
public 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;
}
@Override
public 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();
}
}
Java 提供了两种主要方式来实现多线程编程:通过继承 Thread 类和通过实现 Runnable 接口。这两种方式各有特点,下面我将详细介绍它们的实现方法和区别。
这是实现多线程的最直接方式,通过创建一个继承自 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 {
@Override
public 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 () 方法不会创建新线程,只是在当前线程中执行方法体。
这是更常用的实现多线程的方式,通过创建一个实现 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()方法,定义线程执行的任务
@Override
public 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 接口的方式,原因如下:
无论选择哪种方式,都要记住:线程的启动必须通过调用 start () 方法,而不是直接调用 run () 方法。
在 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 {
@Override
public 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):
继承(Inheritance):
在实际开发中,组合方式更为常用,特别是在需要共享资源的场景中:
通过这种方式,你可以在保持代码灵活性和可维护性的同时,充分利用 Thread 类提供的各种功能。
在 Runnable 接口的实现中,你可以通过Thread.currentThread()
获取当前执行该 Runnable 的线程对象,然后调用线程的所有方法,包括setName()
、sleep()
、interrupt()
等。
这种方式的核心在于:Runnable 接口定义了要执行的任务,而 Thread 类负责线程的创建和管理。当你在 Runnable 的run()
方法中使用Thread.currentThread()
时,你实际上是在访问执行该任务的线程对象。
下面是一个更清晰的示例,展示如何在 Runnable 中使用 Thread 的各种方法:
这种做法在设计模式中被称为组合模式(Composition Pattern),它是 "组合优于继承" 设计原则的具体应用。通过组合,你可以在不继承 Thread 类的情况下,利用 Thread 类提供的功能。
另外,从更广泛的角度看,这也是 ** 依赖注入(Dependency Injection)** 的一种形式,因为 Runnable 任务依赖于 Thread 对象来执行。
在实际开发中,这种方式是实现多线程的首选方法,特别是在需要共享资源或实现复杂并发逻辑的场景中。
方式 2:实现 Runnable 接口
多线程的实现方案有两种
相比继承 Thread 类,实现 Runnable 接口的好处