多线程基础知识
约 2891 字大约 10 分钟
2025-01-22
1.进程、线程、协程
- 进程:
- 进程是资源分配的基本单位,每个进程都有自己独立的地址空间。
- 举例:电脑启动的一个个应用
- 线程:
- 线程是进程中的独立执行单元。多个线程可以共享同一个进程的资源,如堆、全局变量、静态变量
- 每个线程都有自己独立的寄存器和栈
- 协程:
- 协程是一个用户态的线程,线程的 CPU 信息在内核栈中,线程的切换需要在内核态中完成。
- 协程的切换在用户态,调度由用户完成
如图,进程和线程的关系
2.并发、并行、串行
第一种理解思路:
- 并发:一个线程 同时在 处理多个任务,多个任务通过时间片轮转实现交替执行
- 并行:多个线程 同时在 处理多个任务,多个任务在同一时间真正地同时执行
- 串行:一个程序处理完当前进程,按照顺序接着处理下一个进程,一个接着一个进行
第二种理解思路:
- 并发:一个 CPU 同时 提供给多个线程使用
3.上下文切换
在单核CPU上,如果使用多线程的话,CPU 资源的分配采用了时间片轮转,也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是上下文切换
4.创建线程的方式
共有三种创建线程的方式,分别为继承Thread类、实现Runnable接口、实现Callable接口
4.1 继承Thread类
步骤如下:
- 继承 Thread 类,重写
run()
方法 - 创建线程类对象,调用
start()
方法启动线程
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("MyThread...执行了............."+i);
}
}
}
public static void main(String[] args) {
//创建线程对象
MyThread myThread = new MyThread();
//调用Thread中的start方法,开启线程,jvm自动执行run方法
myThread.start();
myThread.start();//不能多次调用
}
4.2 实现Runnable接口
步骤如下:
- 实现Runnable接口,重写run方法,设置线程任务
- 创建线程类对象,将线程类对象作为参数传递给 Thread 对象
- 调用Thread中的start方法
class RunnableTask implements Runnable {
public void run() {
System.out.println("线程调用了!");
}
public static void main(String[] args) {
RunnableTask task = new RunnableTask();
Thread thread = new Thread(task);
thread.start();
}
}
4.3 实现Callable接口
步骤如下:
- 实现 Callable 接口,重写 call() 方法,设置线程任务
- 创建 FutureTask 对象,参数为 Callable 对象
- 创建 Thread 对象,参数为 FutureTask 对象,调用 start() 方法启动线程
public class CallableTask implements Callable<String> {
public String call() {
return "线程调用了";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableTask task = new CallableTask();
FutureTask<String> futureTask = new FutureTask<>(task);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
5.线程的生命周期
在 Java 中,线程共有 7 种状态:
- 新建:(new):新建一个线程对象,new Thread()
- 就绪(Runnable):当调用线程对象的start(),等待获取 CPU资源 的使用权
- 运行(Running):就绪状态的线程获取了CPU资源,执行程序代码
- 阻塞(Blocked):当线程在试图获取一个锁以进入同步块/方法时,如果锁被其他线程持有,线程将进入阻塞状态,直到它获取到锁
- 等待:①锁对象调用wait()方法;②其他线程调用join()方法,当前线程便进入等待状态,除非其他线程唤醒
- 超时等待:锁对象调用带有超时参数的wait()方法
- 终止:当线程的 run() 方法执行完毕
6.线程的常用调度方法
在 Java 中,创建线程之后,我们需要对这些
等待
- wait()
- wait(long timeout)
- wait(long timeout,int nanos)
- join()
通知:
- notify()
- notifyAll()
让出优先权:yield()
中断:
- interrupt()
- isinterrupted()
- interrupted()
休眠:sleep()
6.1 线程等待与通知
在 Object 类中,提供了一些线程的等待通知方法
等待方法:
wait():当一个线程A调用一个共享变量的wait()方法,线程A会进入阻塞状态,直到出现下面两种情况才会继续执行
- 线程 B 调用了共享对象的唤醒线程方法
notify()
或者notifyAll()
方法; - 其他线程调用了线程 A 的
interrupt()
方法,线程 A 抛出 InterruptedException 异常返回
wait(long timeout):如果线程 A 调用共享对象的
wait(long timeout)
方法后,没有在指定的 timeout 时间内被其它线程唤醒,那么这个方法还是会因为超时而继续执行
除了 Object 类提供了 wait 系列方法之外,Thread 类还提供了一个 join() 方法,用于等待其他线程执行终止后,才继续执行
例子:线程A 执行了 线程B.join(),线程A 会等待 线程B 执行完毕之后,才继续执行
通知方法:
notify()
:一个线程 A 调用共享对象的notify()
方法后,会唤醒 一个 在这个共享变量上调用 wait 系列方法后 处于等待状态的线程
notifyAll()
:notifyAll 方法会唤醒 所有 在该共享变量上调用 wait 系列方法后 处于等待状态的线程。
6.2 线程休眠
sleep(long millis)
:Thread 类中的静态方法,当一个执行中的线程 A 调用了 Thread 的 sleep 方法后,线程 A 会暂时让出指定时间的执行权;
注意:线程A 所拥有的监视器资源,比如锁,是不会释放的
6.3 让出优先权
yield()
:Thread 类中的静态方法,当一个线程调用 yield 方法时,实际是在暗示线程调度器,当前线程请求让出自己的 CPU,但是线程调度器可能会“装看不见”忽略这个暗示
6.4 线程中断
Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行。
void interrupt()
方法:中断线程,例如,当线程 A 运行时,线程B 可以调用线程A的interrupt()
方法来设置 线程A 的中断标志为 true 并立即返回。设置标志仅仅是设置标志,线程A 实际并没有被中断,会继续往下执行boolean isInterrupted()
方法: 检测当前线程是否被中断boolean interrupted()
方法: 检测当前线程是否被中断,与 isInterrupted 不同的是,该方法如果发现当前线程被中断,则会清除中断标志
7.线程安全
线程安全==线程处于安全状态?no,安全指的是数据的正确性
简单来说,如果一段代码块或者一个方法在多线程环境中被多个线程同时执行时,能够正确地处理共享数据,不会出现脏数据,那么这段代码块或者方法就是线程安全的
线程安全的三要素:
- 原子性:确保当某个线程修改共享变量时,没有其他线程可以同时修改这个变量,即这个操作是不可分割的
- 可见性:确保一个线程对共享变量的修改可以立即被其他线程看到
- 活跃性:
8.JMM
JMM,Java内存模型,一种线程之间的通信机制,是一个抽象的规范。Java内存模型定义了共享内存系统中多线程程序读写操作行为的规范
JMM 对内存的划分?
JMM规定了内存主要划分为主内存和工作内存两种,规定所有的变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存了该线程中用到的变量的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存
Java 怎么保证线程并发安全,即原子性、可见性、有序性
- 原子性:JMM保证数据基本读取和赋值,更大范围可使用 synchronized 和 lock
- 可见性:
- volatile 关键字,保证修改的值被立即更新到主内存中,当有其他线程读取该值时,也不会直接读取工作内存中的值,而是直接去主内存中读取(普通共享变量被修改后,写入了工作内存中,什么时候写入主内存其实是不可知的,无法保证可见性)
- synchronized和lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中
- 有序性:synchronized 和 lock
9.ThreadLocal
ThreadLocal
是 Java 中用于创建线程局部变量的类,它允许每个线程都拥有自己的独立副本,这些副本在其他线程中是不可见的,从而避免了线程之间的共享变量竞争问题。
9.1 ThreadLocal 工作原理
每个线程会有自己的一份 ThreadLocal
变量副本。其工作机制是:每个线程都持有一个 ThreadLocalMap
对象,这个 ThreadLocalMap
是线程内部的一个数据结构,用来存储每个线程的局部变量
9.2 ThreadLocalMap
ThreadLocalMap 底层结构:
在 ThreadLocal
的实现中,ThreadLocalMap
是线程内部存储局部变量副本的容器。每个线程中都有一个 ThreadLocalMap
实例,该实例是通过 Thread.currentThread()
获取的。
ThreadLocalMap
实际上是一个类似于哈希表的数据结构,其中存储着键值对,键是 ThreadLocal
对象,值是该线程的局部变量。
ThreadLocalMap 关键特点:
弱引用 作为键(Key):
在
ThreadLocalMap
中,ThreadLocal
对象作为键存储,而ThreadLocal
本身是通过WeakReference
进行持有的。也就是说,ThreadLocal
对象在ThreadLocalMap
中是弱引用。当没有线程再使用某个ThreadLocal
对象时,它可能会被垃圾回收器回收,而ThreadLocalMap
中的弱引用键会被清除,避免内存泄漏强引用 作为值(Value):
对应
ThreadLocal
的值则是通过强引用来保存的每个线程都能独立地存储该
ThreadLocal
对象的值副本,并且每个线程的局部变量副本是隔离的。
为什么使用弱引用?
ThreadLocalMap
中对 ThreadLocal
使用了弱引用,主要是为了防止内存泄漏。假设一个 ThreadLocal
对象不再被任何线程使用时,如果不使用弱引用,ThreadLocalMap
中的键(即 ThreadLocal
对象)将一直存在,无法被垃圾回收,从而导致内存泄漏。
弱引用的使用使得 ThreadLocal
对象在没有线程引用的情况下可以被垃圾回收器回收,从而减少内存泄漏的风险。
9.3 关键方法
get():用于获取当前线程的局部变量副本。如果当前线程没有这个副本,则会创建一个新的副本,并返回它
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
Integer value = threadLocal.get();
set(value):用于将当前线程的局部变量副本设置为指定的值。每个线程都有自己独立的副本
threadLocal.set(100);
remove():移除当前线程的局部变量副本。这是为了避免线程在结束时还持有对某些对象的引用,造成内存泄漏
threadLocal.remove();