【Java】多线程篇 —— 多线程的基本使用
目录
- 多线程的概念
- 多线程的实现方式
-
- 继承 Thread 类的方式进行实现
- 实现 Runnable 接口的方式进行实现
- 利用 Callable 接口和 FutureTask 类
- 三种方式对比
- 常见的成员方法
-
- 线程的命名和休眠
- 线程的优先级
- 守护线程
- 礼让线程
- 插入线程
- 线程的生命周期
- 线程的安全问题
-
- 同步代码块
- 同步方法
- Lock 锁
- 死锁
-
- 死锁的四个必要条件
- 死锁示例代码
- 解决死锁的常用方法
-
- 破坏循环等待条件
- 使用 `tryLock` 实现超时机制
- 减少锁的持有时间
- 使用更高层次的并发工具
- 预防死锁的最佳实践
- 生产者和消费者(等待唤醒机制)
-
- 原理
-
- 消费者代码思路:
- 生产者代码思路:
- 生产者和消费者常见方法
- 代码示例
- 阻塞队列方式实现
-
- 原理
- 代码示例
- 线程的状态
多线程的概念
线程:是操作系统能够进行运输调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。可以简单理解为,应用软件中互相独立,可以同时运行的功能
进程:是程序的基本执行实体
多线程:可以将线程简单理解为,应用软件中互相独立,可以同时运行的功能。而多线程就是同时运行多个这样的功能,可以让程序同时做多件事
多线程作用:提高效率(压榨 CPU)
多线程应用场景:
-
服务端领域
- Web 服务:处理大量并发请求,像电商网站高峰期多用户同时访问,多线程让服务器并行响应,提升并发处理力和响应速度。
- 数据库服务:多个客户端同时进行读写操作时,多线程并行处理,提高数据库吞吐量和性能。
-
界面相关应用
-
桌面软件:进行图形渲染、文件读写等耗时操作时,多线程避免界面卡顿,保证流畅交互。
-
游戏开发:不同线程分别负责逻辑计算、图形渲染、网络通信等,实现流畅游戏体验。
-
-
数据计算场景
-
科学计算:气象预报、基因测序等需大量数值计算,多线程分解任务并行处理,缩短计算时间。
-
大数据处理:对海量数据进行挖掘、分析,多线程同时处理不同部分数据,加快处理进程。
-
-
异步 I/O 操作
-
文件读写:读写大文件时,多线程让读写与其他任务并行,提高整体效率。
-
网络通信:即时通讯等应用中,不同线程分别负责收、发消息,保证消息及时处理。
-
并发:在同一时刻,有多条指令在单个 CPU 上交替执行
并行:在同一时刻,有多条指令在多个 CPU(多核) 上同时执行
多线程的实现方式
继承 Thread 类的方式进行实现
操作步骤:
- 创建一个子类继承 Thread 类
- 重写 Thread 类中的 run()
- 创建子类对象并启动线程
代码示例:
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "helloworld");
}
}
public static void main(String[] args) {
MyThread mt1 = new MyThread();
MyThread mt2 = new MyThread();
mt1.setName("线程1:");
mt2.setName("线程2:");
// 开启线程
mt1.start();
mt2.start();
}
}
实现 Runnable 接口的方式进行实现
操作步骤:
- 创建一个类实现 Runnable 接口
- 重写 Runnable 接口中的 run()
- 创建实现类对象
- 创建一个 Thread 类对象并启动线程
代码示例:
public class MyRun implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
Thread t = Thread.currentThread();
System.out.println(t.getName() + "HelloWorld");
//System.out.println(Thread.currentThread().getName() + "HelloWorld");
}
}
public static void main(String[] args) {
MyRun mr = new MyRun();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.setName("线程1:");
t2.setName("线程2:");
t1.start();
t2.start();
}
}
利用 Callable 接口和 FutureTask 类
特点:可以获取到多线程运行的结果
操作步骤:
- 创建一个类实现 Callable 接口
- 重写 call()(是有返回值的,表示多线程运行的结果)
- 创建实现类对象(表示多线程要执行的任务)
- 创建 FutureTask 类的对象(作用:管理多线程运行的结果)
- 创建 Thread 类的对象并启动线程
代码示例:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 1;i <= 100;i++) {
sum = sum + i;
}
return sum;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<Integer>(mc);
Thread t1 = new Thread(ft);
t1.start();
Integer result = ft.get();
System.out.println(result);
}
}
三种方式对比
对比维度 | 继承 Thread 类 |
实现 Runnable 接口 |
实现 Callable 接口并结合 FutureTask |
---|---|---|---|
代码复杂度 | 简单直观,只需继承 Thread 类并重写 run() 方法,但受单继承限制 |
稍高,需额外创建 Thread 对象并传入 Runnable 实例,可避免单继承限制 |
最高,要创建 Callable 类、FutureTask 包装对象和 Thread 对象 |
线程执行结果获取 | 无法直接获取,run() 方法返回值为 void ,需借助共享变量、回调函数等 |
无法直接获取,run() 方法返回值为 void ,需借助额外手段 |
可直接获取,通过 FutureTask 的 get() 方法阻塞等待获取 call() 方法返回值 |
资源共享和线程安全性 | 不同线程实例有独立实例变量,共享需用静态变量,需额外同步机制保证安全 | 多个线程可共享同一 Runnable 实例成员变量,需同步机制保证数据一致性 |
与 Runnable 类似,多个线程可共享同一 Callable 实例成员变量,需同步机制 |
扩展性和灵活性 | 受单继承限制,扩展性差 | 不影响类继承关系,扩展性和灵活性好,可分离任务和线程创建 | 不影响类继承关系,扩展性和灵活性好,可分离任务和线程创建 |
常见的成员方法
线程的命名和休眠
String getName()
:方法名,返回此线程的名称,返回值类型为String
。void setName(String name)
:方法名,用于设置线程的名字(构造方法也可设置),无返回值(void
) 。
代码示例:
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(getName() + "@" + i);
}
}
public static void main(String[] args) {
// 构造方法命名
MyThread mt1 = new MyThread("线程1");
MyThread mt2 = new MyThread();
MyThread mt3 = new MyThread();
// setName()命名
mt2.setName("线程2");
mt1.start();
mt2.start();
// 默认命名
mt3.start();
}
}
程序运行结果如下:
线程1@1
线程1@2
Thread-1@1
Thread-1@2
Thread-1@3
线程2@1
线程2@2
线程2@3
线程1@3
注意事项:
- 如果没有给线程命名,线程会有默认名,格式:Thread-X(X 是序号,从 0 开始)
- 线程命名可以使用 setName(),也可使用构造方法,由于子类不会继承父类的构造方法,所以不要忘了在继承类中写构造方法
static Thread currentThread()
:静态方法,获取当前线程的对象,返回值类型为Thread
。
在前面实现 Runnable 接口中使用过该方法,就不写代码示例了,不过需要思考一个问题,在 main 方法中不开启线程,用 currentThread 方法获取的对象是什么?
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(getName() + "@" + i);
}
}
public static void main(String[] args) {
Thread t = Thread.currentThread();
System.out.println(t.getName()); // 输出 main
}
}
当 JVM 虚拟机启动之后,会自动开启多条线程,其中有一条就是 main 线程,作用是调用 main 方法并执行其中的代码
static void sleep(long time)
:静态方法,让线程休眠指定的毫秒数,无返回值(void
)。
代码示例:
public class Test {
public static void main(String[] args) throws InterruptedException {
System.out.println("123");
long start = System.currentTimeMillis();
Thread.sleep(5000);
long end = System.currentTimeMillis();
long time = (end - start) / 1000;
System.out.println("线程休眠了"+ time + "秒");
System.out.println("456");
}
}
注意事项:
- 执行到这个方法的线程会在这里停留对应的时间
- 当时间到了后,线程会自动继续执行后面的代码
线程的优先级
调度分为抢占式调度(随机)和非抢占式调度(交替),Java 采取的就是抢占式调度,优先级越大,在随即中抢到 CPU 的概率就越大,优先级为 1-10,默认是 5。
setPriority(int newPriority)
:方法名,设置线程的优先级,无返回值(void
)。final int getPriority()
:方法名,获取线程的优先级,返回值类型为int
。