本文纲要
一、多线程基础概念
1.1 初步认识多线程
1.2 并发与并行
1.3 进程与线程
二、多线程的实现方式
2.1 项目结构概览
2.2 方式一:继承 Thread 类
2.3 run() 与 start() 的区别
2.4 方式二:实现 Runnable 接口
2.5 方式三:实现 Callable 接口与 FutureTask
2.6 三种实现方式对比
三、Thread 类常用方法
3.1 获取和设置线程名称
3.2 获取当前线程对象 currentThread()
3.3 线程休眠 sleep()
3.4 线程优先级
3.5 守护线程
四、总结
多线程基础概念
1 ) 初步认识多线程
多线程是指从软件或硬件上实现多个线程并发执行的技术,具有多线程能力的计算机必须有硬件支持,从而在同一时间执行多个线程,提升性能。
以一个生活场景为例:你一边玩电脑,一边抽烟、喝可乐。你的右手一会儿点鼠标,一会儿拿烟,一会儿拿可乐。右手动作非常快,在外人看来你似乎在同时做三件事,但在某一瞬间,你其实只做了其中一件事(比如拿可乐喝)。CPU 就像这只右手,三个软件就像鼠标、烟、可乐。CPU 在多个软件之间高速切换,让我们感觉它们是同时执行的。
所以,对多线程的初步理解可以总结为:
- 多线程技术就是让多个应用程序“同时”执行
- 它需要硬件的支持(CPU 高速切换)
2 )并发与并行
并行:在同一时刻,有多个指令在多个 CPU 上同时执行。
并发:在同一时刻,有多个指令在单个 CPU 上交替执行。
| 概念 | 执行方式 | 比喻 |
|---|---|---|
| 并行 | 多个 CPU 同时执行多个任务 | 三个厨师同时炒三个菜(真·同时) |
| 并发 | 单 CPU 交替执行多个任务 | 一个厨师快速切换炒三个菜(宏观同时,微观交替) |
重点:Java 多线程主要研究的是并发场景,多个线程在单个 CPU 上交替执行。
3 ) 进程与线程
进程:正在运行的软件。打开任务管理器可以看到许多正在运行的进程(如 PPT、IDEA)。
进程的特点:
- 独立性:进程是能独立运行的基本单位,也是系统分配资源和调度的独立单位,进程之间不能直接传递数据。
- 动态性:进程的实质是程序的一次执行过程,动态产生、动态消亡。
- 并发性:任何进程都可以同其他进程一起并发执行。
线程:进程中的单个顺序控制流,是一条执行路径。
简单理解,线程属于进程,是进程中真正干活的部分。例如,360安全卫士同时进行“电脑体检”、“木马查杀”、“电脑清理”、“系统修复”、“优化加速”,这五项任务就是五个线程,它们都属于360这个进程。
- 一个进程中如果只有一条执行路径,则称为单线程程序(如之前写的顺序执行代码)。
- 一个进程中如果有多条执行路径,则称为多线程程序。
多线程的实现方式
Java 中多线程的实现主要有三种方式:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口配合 FutureTask。
1 ) 项目结构概览
以下示例代码的组织结构(基于文本与代码整理):
threadmodule/src/com/wb/
├─ threaddemo1 // 继承 Thread 类
│ ├─ MyThread.java
│ └─ Demo.java
├─ threaddemo2 // 实现 Runnable 接口
│ ├─ MyRunnable.java
│ └─ Demo.java
├─ threaddemo3 // 实现 Callable 接口
│ ├─ MyCallable.java
│ └─ Demo.java
├─ threaddemo4 // 设置/获取线程名称
│ ├─ MyThread.java
│ └─ Demo.java
├─ threaddemo5 // currentThread()
│ └─ Demo.java
├─ threaddemo6 // sleep()
│ ├─ MyRunnable.java
│ └─ Demo.java
├─ threaddemo7 // 线程优先级
│ ├─ MyCallable.java
│ └─ Demo.java
└─ threaddemo8 // 守护线程
├─ MyThread1.java
├─ MyThread2.java
└─ Demo.java
2 ) 方式一:继承 Thread 类
步骤:
- 定义一个类继承
Thread - 在这个类中重写
run()方法 - 创建该类的对象
- 调用
start()方法启动线程
代码示例(threaddemo1):
// MyThread.java
public class MyThread extends Thread {
@Override
public void run() {
// 线程开启后执行的代码
for (int i = 0; i < 100; i++) {
System.out.println("线程开启了" + i);
}
}
}
// Demo.java
public class Demo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start(); // 开启第一条线程
t2.start(); // 开启第二条线程
}
}
运行效果:两个线程交替打印,每次运行顺序可能不同,体现了多线程的随机性。
3 ) run() 与 start() 的区别
问:为什么要重写 run() 方法?
答:多线程开启后,执行的就是 run() 方法里的代码,因此必须重写它来封装任务。
问:run() 和 start() 有什么区别?
如果直接调用 run() 方法:
t1.run(); // 只是普通的方法调用,没有开启新线程,main 线程顺序执行
t2.run();
输出会严格按照顺序:t1 的循环全部执行完,再执行 t2 的循环,没有多线程效果。
而调用 start() 方法:
t1.start(); // 与操作系统交互,真正开启一条新线程,由新线程执行 run()
t2.start();
此时两个线程交替执行,具有多线程效果。
小结
run():封装线程执行的代码,直接调用仅相当于普通方法调用,没有开启新线程。
start():开启新线程,JVM 会在新线程中调用 run() 方法。
4 ) 方式二:实现 Runnable 接口
步骤:
- 定义一个类实现
Runnable接口 - 重写
run()方法 - 创建该实现类的对象(作为参数对象)
- 创建
Thread对象,将Runnable对象传入 - 调用
Thread对象的start()方法启动线程
代码示例(threaddemo2):
// MyRunnable.java
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 使用 currentThread() 获取当前线程对象,再获取名称
System.out.println(Thread.currentThread().getName() + " 第二种方式实现多线程" + i);
}
}
}
// Demo.java
public class Demo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable(); // 参数对象
Thread t1 = new Thread(mr); // 创建线程对象并绑定任务
t1.start(); // 开启线程
MyRunnable mr2 = new MyRunnable();
Thread t2 = new Thread(mr2);
t2.start();
}
}
注意:Runnable 接口没有 start() 方法,必须借助 Thread 对象启动。在线程启动后,执行的是参数对象中的 run() 方法。
5 ) 方式三:实现 Callable 接口与 FutureTask
特点:与前两种方式不同,Callable 允许线程执行完成后返回结果,返回值的类型可通过泛型指定。
步骤:
- 定义一个类实现
Callable<V>接口(V为返回值类型) - 重写
call()方法(相当于run(),但有返回值) - 在测试类中创建
Callable实现类对象 - 创建
FutureTask<V>对象,将Callable对象传入 - 创建
Thread对象,将FutureTask对象传入(FutureTask实现了Runnable,因此可以传递) - 启动线程
- 在
start()之后,通过FutureTask的get()方法获取结果
代码示例(threaddemo3):
// MyCallable.java
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("跟女孩表白" + i);
}
// 返回值表示线程运行完毕之后的结果
return "答应";
}
}
// Demo.java
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable(); // 创建 Callable 对象
FutureTask<String> ft = new FutureTask<>(mc); // 包装成 FutureTask
Thread t1 = new Thread(ft); // 传入 Thread
t1.start(); // 启动线程
String result = ft.get(); // 获取结果(会阻塞直到线程完成)
System.out.println(result); // 输出:答应
}
}
特别注意:get() 方法必须放在 start() 之后调用。如果放在 start() 之前,主线程会因等待一个尚未开始执行的结果而无限阻塞。
6 ) 三种实现方式对比
| 对比维度 | 继承 Thread 类 | 实现 Runnable 接口 | 实现 Callable 接口 |
|---|---|---|---|
| 是否有返回值 | 无 | 无 | 有(泛型指定) |
| 能否继承其他类 | 不能(单继承) | 可以(实现接口同时还能继承) | 可以 |
| 编程复杂度 | 较简单 | 稍复杂(不能直接使用 Thread 方法) | 较复杂(需要 FutureTask) |
能否直接使用 Thread 中的方法 | 能 | 不能(需 Thread.currentThread()) | 不能 |
| 适用场景 | 简单任务、无需共享资源 | 推荐使用,可继承其他类、可共享资源 | 需要线程执行结果时 |
Thread 类常用方法
1 ) 获取和设置线程名称
获取名称:getName()
设置名称:
方式一:setName(String name)
方式二:通过构造方法为线程传递名称(需在子类中定义对应构造方法调用 super(name))
即使不手动命名,线程也有默认名称,格式为 Thread-编号(编号从 0 开始自增)。
代码示例(threaddemo4):
// MyThread.java
public class MyThread extends Thread {
public MyThread() {}
public MyThread(String name) {
super(name); // 将名称传递给 Thread 类
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "@@@" + i);
}
}
}
// Demo.java
public class Demo {
public static void main(String[] args) {
// 方式一:构造方法命名
MyThread t1 = new MyThread("小蔡");
MyThread t2 = new MyThread("小强");
// 方式二:setName() 命名(注释演示)
// t1.setName("小蔡");
// t2.setName("小强");
t1.start();
t2.start();
}
}
2 ) 获取当前线程对象 currentThread()
Thread.currentThread() 是一个静态方法,返回当前正在执行的线程对象。
当需要在 Runnable 实现类中获取线程名称时非常有用(因为 Runnable 没有继承 Thread)。
代码示例(threaddemo5):
public class Demo {
public static void main(String[] args) {
// 当前线程为 main 线程
String name = Thread.currentThread().getName();
System.out.println(name); // 输出:main
}
}
在 Runnable 的 run() 方法中同样可以使用:
public void run() {
System.out.println(Thread.currentThread().getName() + " 正在执行");
}
3 ) 线程休眠 sleep()
Thread.sleep(long millis) 是一个静态方法,让当前执行的线程休眠指定的毫秒数(1秒 = 1000毫秒)。
休眠期间该线程不会释放锁,休眠结束后将继续运行。
注意:如果 sleep() 被放在没有声明异常的方法中(如 run()),必须使用 try-catch 捕获 InterruptedException,不能抛出。
代码示例(threaddemo6):
// MyRunnable.java
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100); // 休眠 100 毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
// Demo.java
public class Demo {
public static void main(String[] args) throws InterruptedException {
// 主线程休眠演示
// System.out.println("睡觉前");
// Thread.sleep(3000);
// System.out.println("睡醒了");
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.start();
t2.start();
}
}
4 ) 线程优先级
Java 采用抢占式调度模型:优先级高的线程获取 CPU 时间片的概率相对更高,但不保证一定优先执行。
Thread 类中定义了优先级常量:
| 常量 | 数值 |
|---|---|
MIN_PRIORITY | 1 |
NORM_PRIORITY | 5 (默认) |
MAX_PRIORITY | 10 |
相关方法:
setPriority(int priority):设置优先级(1~10,范围外会抛出IllegalArgumentException)getPriority():获取优先级
代码示例(threaddemo7):
// MyCallable.java
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
return "线程执行完毕了";
}
}
// Demo.java
public class Demo {
public static void main(String[] args) {
MyCallable mc = new MyCallable();
FutureTask<String> ft = new FutureTask<>(mc);
Thread t1 = new Thread(ft);
t1.setName("飞机");
t1.setPriority(10); // 最高优先级
// System.out.println(t1.getPriority()); // 默认 5
t1.start();
MyCallable mc2 = new MyCallable();
FutureTask<String> ft2 = new FutureTask<>(mc2);
Thread t2 = new Thread(ft2);
t2.setName("坦克");
t2.setPriority(1); // 最低优先级
t2.start();
}
}
多次运行会发现,“飞机”虽然优先级高,但并不总是先执行完,只是抢占到 CPU 的概率更大。
5 ) 守护线程
守护线程(Daemon Thread)也叫后台线程,主要服务于普通线程。当所有普通线程结束时,守护线程会自动终止,不会继续执行完毕。
通过 setDaemon(true) 可将一个线程设置为守护线程(必须在 start() 之前设置)。
典型应用场景:垃圾回收线程、自动保存线程等。
代码示例(threaddemo8):
// MyThread1.java(女神:普通线程,执行 10 次)
public class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "---" + i);
}
}
}
// MyThread2.java(备胎:守护线程,执行 100 次)
public class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "---" + i);
}
}
}
// Demo.java
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("备胎");
t2.setDaemon(true); // 设置为守护线程
t1.start();
t2.start();
}
}
运行现象:“女神”线程(普通线程)打印 0~9 结束后,“备胎”线程(守护线程)挣扎打印一段时间后终止,不会打印完 100 次。这说明守护线程在失去最后的普通线程后将结束运行。
总结
本文从多线程的基本概念出发,介绍了:
- 并发与并行的区别
- 进程与线程的关系
- 多线程的三种实现方式(继承 Thread、实现 Runnable、使用 Callable+FutureTask)及其对比
- Thread 类的常用方法:名称设置、获取当前线程、sleep、优先级、守护线程
掌握这些知识就为 Java 多线程编程打下了坚实的基础。在后续学习中,还会涉及线程同步、线程池、并发工具等高级内容。
1033

被折叠的 条评论
为什么被折叠?



