线程
什么是程序
程序=算法+数据结构+编程语言
静态的没有运行的。
什么是进程
进程是程序执行的实例,在一个操作系统中,正在运行的程序就是进程
什么是线程
在一个进程中,正在运行的子程序流就是线程。
是程序执行的最小单元,CPU调度的最小单位,指的是一个进程内的一个独立的执行路径。
进程是线程的集合,一个进程除了包含多个进程之外还包含其余的共享变量空间。
线程和CPU之间的关系
-
线程执行:线程的指令由 CPU 执行。CPU 是实际运行线程代码的硬件。
-
调度与上下文切换:操作系统负责调度线程在 CPU 上执行,通过上下文切换在不同线程之间切换 CPU 时间。
-
多核并行:多核 CPU 可以同时运行多个线程,实现真正的并行处理。单核 CPU 通过快速切换线程实现并发。
-
资源共享:线程在同一进程内共享内存等资源,但每个线程需要自己的寄存器和栈。
-
线程管理:线程的创建、调度和销毁都是由操作系统的调度器和 CPU 协同管理的。
注意:同一时刻一个CPU只会执行一个线程的任务
什么是上下文切换
上下文切换(Context Switch)是指当操作系统在不同的线程或进程之间切换执行时,保存当前线程或进程的状态(即上下文),并恢复下一个要运行的线程或进程的状态的过程。上下文切换使得操作系统能够在多个进程或线程之间分配 CPU 资源,实现并发和多任务处理。
上下文(Context)是什么?
上下文指的是当前进程或线程的运行状态,主要包括以下信息:
- 程序计数器(PC):当前正在执行的指令地址。
- CPU 寄存器:包括通用寄存器、栈指针、基址指针等,存储着当前计算的临时数据。
- 内存状态:虚拟内存映射、页面表、堆栈等。
- 程序状态字(PSW):包含了当前的 CPU 状态、优先级、中断标志等。
-
进程上下文切换:
- 在进程之间切换时,操作系统不仅要保存和恢复 CPU 寄存器和程序计数器,还要切换内存映射、页面表等。这是因为不同进程有各自独立的地址空间。
- 进程的上下文切换开销较大,通常涉及到内核态的复杂操作,比如更新虚拟内存地址映射。
-
线程上下文切换:
- 在线程之间切换时,线程共享同一个进程的地址空间,因此上下文切换只需要保存和恢复 CPU 寄存器和程序计数器,无需切换内存映射等资源。
- 线程的上下文切换相对开销较小。
在高并发或多任务场景中,频繁的上下文切换可能会导致过多的开销,从而降低系统性能。因此,减少不必要的上下文切换是系统性能优化的重要方面。
用户态和内核态
- 内核态是操作系统的核心运行模式,具有最高权限,用于执行系统级功能和管理硬件资源。用户程序通过系统调用进入内核态来请求系统服务。
- 用户态是应用程序的运行模式,权限较低,不能直接访问系统资源。用户态代码通过系统调用与内核交互,实现对硬件的间接访问。
这两种模式的分离有助于提高系统的稳定性和安全性,同时使操作系统能够有效管理和隔离不同程序的运行。
创建线程的方法
- 继承
Thread
类:重写run()
,通过start()
启动线程。 - 实现
Runnable
接口:适用于多继承场景,任务执行放在run()
中。 - 匿名内部类或 Lambda 表达式:简化线程的创建和启动。
- 实现
Callable
接口:用于返回结果的线程,适合需要返回值或处理异常的任务。
线程的生命周期
- 新建(new):线程被创建,但未开始运行。
- 就绪(Runnable):线程已准备好运行,等待 CPU 调度。
- 运行(Running):线程正在执行任务。
- 阻塞(Blocked):线程等待资源或事件,无法继续执行。
- 等待(Waiting):线程主动让出 CPU,等待其他线程唤醒。
- 超时等待(Timed Waiting):线程等待指定时间,超时后进入就绪状态。
- 终止(Dead):线程完成任务或遇到异常,生命周期结束。
锁机制
锁的优化机制
从JDK 1.6 版本之后,synchronized本身也在不断优化锁的机制,有些情况下他并不是一个很重量级的的锁了。
优化机制包括自适应锁、自旋锁、锁消除、锁粗轻量级锁和偏向锁。锁的状态从低到高,依次为无锁 - 偏向所 - 轻量级锁 - 重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。
自旋锁: 由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置。
自适应锁: 自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。
锁消除: 锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。
锁粗化: 锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。
偏向锁: 当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁。
轻量级锁: JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。
syncronized
synchronized
是 Java 中最基础的锁机制,提供内置的互斥锁,保证同一时间只有一个线程可以访问被锁住的代码块或方法。
用法
- 同步方法:在方法上加
synchronized
,表示该方法是同步方法,同一时刻只能有一个线程执行。 - 同步代码块:使用
synchronized
块来锁定某个对象,确保只有一个线程能执行块内的代码。
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
特点
- 阻塞机制:其他线程试图访问同步代码时会被阻塞,直到锁被释放。
- 可重入锁:一个线程可以多次进入被同一个
synchronized
锁住的代码块,不会发生死锁。
ReentrantLock(可重入锁)
ReentrantLock
是 Java java.util.concurrent.locks
包中的类,提供了比 synchronized
更灵活的锁机制。它是显式锁,可以手动获取和释放锁。
用法:
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 确保释放锁
}
}
public int getCount() {
return count;
}
}
特点:
- 显式锁控制:需要手动获取和释放锁,灵活性更高。
- 可重入:与
synchronized
一样,一个线程可以多次获得同一把锁。 - 公平性:
ReentrantLock
可以设置为公平锁(公平性参数true
),保证锁按请求的顺序被获取。 - 提供条件变量:通过
newCondition()
可以创建多个条件变量,精确控制线程的等待与通知机制。
悲观锁和乐观锁
悲观锁和乐观锁是两种不同的并发控制策略,用于解决多线程或多进程环境下对共享资源的访问冲突。它们的区别在于对数据冲突发生的假设和处理方式。
悲观锁
悲观锁假设数据在并发操作时每次都会发生冲突,因此在对获取资源操作之前,直接锁定资源,保证在锁释放之前,其他线程或进程无法访问该资源。想法非常悲观所以叫悲观锁
实现方法
在 Java 并发编程中,synchronized
和 ReentrantLock
都是悲观锁的实现。它们在访问共享资源时,会加锁,其他线程在锁释放前无法访问。
乐观锁
与悲观锁相反,乐观锁假设每次执行都不会发生冲突,不需要加锁,每次获取资源之前,只需要用版本号机制或CAS算法去检测对应资源有没有被其他资源修改即可。
实现方法
在 Java 中,AtomicInteger
、AtomicReference
等类采用了乐观锁的思想,内部使用了CAS(Compare And Swap,比较并交换) 操作。CAS 是一种无锁操作,通过比较当前值和期望值是否一致来决定是否更新数据。
CAS(Compare And Swap,比较并交换)
- 读取变量的当前值。
- 比较当前值是否等于期望值。
- 如果相等,更新为新值;否则,不做修改并重试。
区别
自旋锁4
线程池
是提前创建好的、可以重复使用的线程集合。当有任务需要执行时,线程池会提供一个线程执行任务,任务完成后,线程不会被销毁,而是返回线程池等待执行下一个任务。
核心优势
- 减少线程创建和销毁的开销:线程的创建和销毁代价较大,线程池通过复用线程来降低开销。
- 控制并发线程数:通过设置线程池的大小,限制同时运行的线程数,避免系统过载。
- 任务管理:可以对任务进行排队、调度和管理,避免大量任务同时执行导致资源耗尽。
如何创建线程池
Java 提供了 java.util.concurrent
包来支持线程池,推荐通过ThreadPoolExecutor
构造函数来创建。
ThreadPoolExecutor
是 Java 中高度可配置的线程池实现类,提供了灵活的参数配置来控制线程池的行为。通过 ThreadPoolExecutor
的构造函数,可以精细化地创建线程池,设置核心线程数、最大线程数、任务队列以及拒绝策略等。
核心参数
-
corePoolSize
(核心线程数):- 定义了线程池中最小的线程数量,线程池会始终保持至少有
corePoolSize
个线程存在,即使这些线程是空闲的。 - 当提交任务时,如果当前线程数少于核心线程数,则会创建新的线程执行任务,而不将任务放入队列中。
- 定义了线程池中最小的线程数量,线程池会始终保持至少有
-
maximumPoolSize
(最大线程数):- 定义了线程池中允许存在的最大线程数。当任务队列已满时,线程池会继续创建新线程,直到达到
maximumPoolSize
。
- 定义了线程池中允许存在的最大线程数。当任务队列已满时,线程池会继续创建新线程,直到达到
-
keepAliveTime
(线程空闲存活时间):- 当线程池中的线程数量超过
corePoolSize
时,多余的线程在空闲超过keepAliveTime
时间后会被回收。 - 如果线程池中的线程数不超过核心线程数,则这些线程不会被回收。
- 当线程池中的线程数量超过
-
unit
(时间单位):- 定义了
keepAliveTime
的时间单位,如TimeUnit.SECONDS
、TimeUnit.MILLISECONDS
等。
- 定义了
-
workQueue
(任务队列):- 当线程池中的线程数达到
corePoolSize
时,新提交的任务会被放入任务队列等待执行。 - 常见的任务队列实现:
LinkedBlockingQueue
:基于链表的无界队列。ArrayBlockingQueue
:基于数组的有界队列。SynchronousQueue
:不存储任务的队列,任务直接提交给线程执行。
- 当线程池中的线程数达到
-
threadFactory
(线程工厂):- 用于创建线程的工厂,通常用于为线程命名或设置守护线程。
ThreadFactory
接口允许自定义线程的创建逻辑,Java 提供了默认实现Executors.defaultThreadFactory()
。
-
handler
(拒绝策略):- 当线程池的线程数达到
maximumPoolSize
,且任务队列也已满时,新提交的任务将被拒绝执行,线程池会根据指定的RejectedExecutionHandler
拒绝策略来处理这些任务。 - 常见的拒绝策略:
AbortPolicy
(默认):抛出RejectedExecutionException
异常。DiscardPolicy
:直接丢弃任务,不抛异常。DiscardOldestPolicy
:丢弃队列中等待最久的任务,然后尝试重新提交当前任务。CallerRunsPolicy
:由调用线程(提交任务的线程)来执行任务。
- 当线程池的线程数达到
线程池的工作流程
- 任务提交:任务被提交到线程池后,首先判断线程池中运行的线程数是否小于核心线程数(
corePoolSize
),如果小于则创建新线程执行任务。 - 任务排队:如果线程数达到核心线程数,任务被放入等待队列中排队。
- 扩展线程池:如果任务排队已满,并且线程数还未达到最大线程数(
maximumPoolSize
),则创建新的线程执行任务。 - 拒绝任务:如果线程池中的线程数达到最大线程数,并且任务队列也已满,线程池会根据设置的拒绝策略来处理新提交的任务。
- 回收空闲线程:如果线程池中的线程数大于核心线程数,且某些线程长时间没有任务执行,这些空闲线程会在超过
keepAliveTime
后被终止。
总结
- 线程池是一种用于管理线程资源的机制,能够提升性能,减少系统开销。
- Java 提供了多种线程池类型,如
FixedThreadPool
、CachedThreadPool
、ScheduledThreadPool
等,以适应不同的应用场景。 ThreadPoolExecutor
是 Java 线程池的核心实现类,具有高度的可配置性,可以通过核心线程数、最大线程数、任务队列和拒绝策略等参数进行灵活配置。