工作时,时常会遇到,线程相关的问题与解法,本人会持续对开发过程中遇到的关于线程相关的问题及解决记录更新记录在此篇博客中。
一、线程基本知识
1. 线程与进程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。例如,任务管理器显示的就是进程:
线程是比进程更小的执行单位。一个进程在其执行过程中可以产生多个线程。与进程不同的是同类的多个线程共享堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。
JDK 1.2之前,Java线程是基于绿色线程实现的,这是一种用户级线程。即JVM自己模拟了多线程的运行,而不依赖于操作系统。由于绿色线程和原生线程比起来的在使用时有一些限制(比如,绿色线程不能直接使用操作系统提供的功能如异步 I/O、只能在一个内核线程上运行无法利用多核);在JDK 1.2之后,Java线程改为使用原生线程来实现。也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。
- 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。
- 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。
顺便简单总结一下用户线程和内核线程的区别和特点:用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。现在的 Java 线程的本质其实就是操作系统的线程。
线程模型是用户线程和内核线程之间的关联方式,常见的线程模型有这三种:
- 一对一(一个用户线程对应一个内核线程)
- 多对一(多个用户线程映射到一个内核线程)
- 多对多(多个用户线程映射到多个内核线程)
在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个 Java 线程对应一个系统内核线程。
一个进程中可以有多个线程,多个线程共享进程的堆和方法区(JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
- 程序计数器为什么私有:程序计数器是为了线程切换后,能够回到正确的执行位置。
- 虚拟机栈为什么私有:每个Java栈帧在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至完成的过程,对应着一个栈帧在Java虚拟机栈中入栈和出栈的过程。
- 本地方法栈为什么私有:和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象(几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
2. volatile关键字
用于修饰变量,确保变量的读写操作在多线程环境中具有可见性和禁止指令重排序的特性,使用场景:
- 状态标志:用于标记线程的状态,例如线程是否正在运行,是否需要停止等。
- 线程间通信、单例模式。
volatile关键字主要有两个作用:
- 保证变量的可见性:确保一个线程修改了变量后,其他线程能够立刻看到这个修改。
- 禁止指令重排序:防止JVM对volatile变量的读写操作进行指令重排序。
1)可见性原理
多线程环境下,现成对于共享变量的操作可能会被缓存在线程的本地内存(如CPU缓存)中,而不是直接操作主内存。这可能导致修改了一个变量之后,其他线程无法立刻看到这个修改,从而引发数据不一致的问题。
volatile关键字通过 内存屏障机制(Memory Barrier)确保变量可见性。即,volatile
变量的读写操作会插入内存屏障,确保变量的读写操作直接作用于主内存,而不是线程的本地内存。
- 读屏障(Load Barrier):当读取
volatile
变量时,会插入读屏障,确保读取操作直接从主内存中获取最新值。 - 写屏障(Store Barrier):当写入
volatile
变量时,会插入写屏障,确保写入操作立即刷新到主内存中,其他线程可以立即看到这个修改。
2)禁止指令重排序
JVM和CPU为了优化性能,可能会对代码中的指令进行重排序,以提高执行效率然而,在多线程环境下,这种重排序可能会导致程序行为不符合预期。
volatile关键字通过内存屏障禁止对volatile变量的读写操作进行指令重排序。具体来说:
- 写屏障:在写入
volatile
变量之前,所有之前的写操作必须先完成。 - 读屏障:在读取
volatile
变量之后,所有后续的读操作必须在读取操作之后执行。
volatile关键可以解决以上部分线程安全问题,但并不能替代
synchronized
或Lock
机制:
- 只能保证单个变量的读写操作的可见性和禁止指令重排序,不能保证复合操作(如
i++
)的原子性。如果需要原子操作,可以使用AtomicInteger
等原子类,或者使用synchronized
块。- 复杂场景:在复杂的线程同步场景中,
volatile
可能不足以解决问题,需要结合锁机制(如synchronized
或ReentrantLock
)来实现更复杂的同步逻辑。
3. 线程池
线程池是池化思想的一种实例。池化技术的思想主要为了减少每次获取资源的消耗,提高对资源的利用率。
线程池能够:降低资源消耗、提高相应技术、提高线程可管理性。
1. Executor框架介绍
Executors
是一个工具类,提供了一系列静态方法来创建不同类型的线程池。它简化了线程池的创建过程,但隐藏了线程池的一些底层细节。简单易用、功能丰富。
主要方法
-
Executors.newFixedThreadPool(int nThreads)
:- 创建一个固定大小的线程池,线程池的大小由参数
nThreads
指定。 - 适用于任务数量较多且任务执行时间较短的场景。
- 创建一个固定大小的线程池,线程池的大小由参数
-
Executors.newSingleThreadExecutor()
:- 创建一个单线程的线程池,所有任务都在同一个线程中按顺序执行。
- 适用于需要保证任务顺序执行的场景。
-
Executors.newCachedThreadPool()
:- 创建一个可缓存的线程池,线程池的大小会根据任务数量动态调整。
- 适用于任务数量不确定且任务执行时间较短的场景。
-
Executors.newScheduledThreadPool(int corePoolSize)
:- 创建一个支持定时任务和周期性任务的线程池。
- 适用于需要执行延迟任务或周期性任务的场景。
2. ThreadPoolExecutor
ThreadPoolExecutor
是 Java 并发包中提供的一个更底层的线程池实现类,它提供了更丰富的配置选项,允许开发者根据具体需求进行精细的调整。灵活性高、可控性强、性能优化。需要手动配置多个参数,对开发者的要求较高。开发者需要理解线程池的内部机制,才能合理配置参数。
主要参数:
corePoolSize
:核心线程数。线程池中始终保持的线程数量。maximumPoolSize
:最大线程数。线程池中允许的最大线程数量。keepAliveTime
:非核心线程的空闲存活时间。