Java基础学习笔记:并发编程
Java并发编程
1. 基础问题
1.1 进程和线程
1.1.1 为什么有多线程
场景:当一个程序有多个核心功能
单进程问题 | 多进程问题 |
---|---|
执行不连贯,可能阻塞 | 进程间通信、共享数据麻烦 |
不是并发,影响资源使用效率 | 维护进程成本大,需维护PCB |
因此需要一个新实体:线程
- 可以并发运行
- 可以共享内存空间
1.1.2 定义
进程:程序的一次执行过程,系统运行程序的基本单位,资源分配的单位
线程:进程中的一条执行流程,CPU调度的单位
1.1.3 关系
一个进程中可以有多个线程
进程 | 线程 |
---|---|
相互独立 | 可能相互影响 |
拥有完整的资源平台(堆、方法区、元空间内存) | 独享虚拟机栈、本地方法栈、程序计数器 |
利于资源的管理和保护 | 执行开销小 |
提示
- 单核CPU
- 支持多线程并发:线程IO阻塞时无需占用CPU资源
- 支持多进程,但无法并行
- 多核CPU
- 支持多进程并行、多线程并发
1.1.4 进程状态
无挂起:创建、就绪、运行、阻塞、结束
有挂起:创建、就绪、就绪挂起、运行、阻塞、阻塞挂起、结束
1.1.5 线程状态(生命周期)
初始状态NEW、运行状态RUNNABLE、终止状态TERMINATED
阻塞状态BLOCKED、等待状态WAITING、超时等待状态TIME_WAITING
- 对应操作系统进程状态中的阻塞状态(WAITING)
- 阻塞状态:等待争夺锁
- 等待状态:
wait()
- 必须在同步代码块中调用,需要先持有锁
- 释放锁后,不参与争夺,直到被唤醒
- 超时等待:
sleep()
- 不必须先持有锁
- 固定时间后,自动唤醒
1.1.6 进程控制块
定义:进程存在的唯一标识
组成:
描述 | 详情 |
---|---|
进程描述信息 | 进程标识符 用户标识符 |
进程控制和管理信息 | 进程当前状态 进程优先级 |
资源分配信息 | 虚拟内存地址 打开的文件列表 IO设备信息 |
CPU信息 | 各个寄存器的值 |
组织方式:链表形式
- 好处:便于调度时的插入和删除
- 队列种类:就绪队列、阻塞队列、运行队列
1.1.7 多线程的问题:ThreadLocal内存泄露、死锁、线程不安全
1.1.8 JVM内存结构中的体现
进程(线程共享) | 线程私有 |
---|---|
堆 | 程序计数器 |
方法区 | 虚拟机栈 |
内存 | 本地方法栈 |
1.2 并发与并行
并发:两个及以上的事件在同一时间段内执行,同一实体,针对线程
并行:两个及以上的事件在同一时刻执行,不同实体,针对进程
1.3 上下文切换
CPU上下文切换
- 先把前一个任务的CPU上下文(寄存器和程序计数器)保存
- 再加载新任务到CPU
- 最后跳转到程序计数器所指新位置,运行新任务
进程上下文内容
-
用户空间资源:虚拟内存、栈、全局变量
-
内核空间资源:内核堆栈、寄存器
线程上下文内容
- 虚拟机栈、本地方法栈、程序计数器
1.4 sleep和wait
sleep | wait |
---|---|
不释放锁 | 释放锁 |
用于暂停执行 | 用于线程间交互/通信 |
自动苏醒 | 需要notify唤醒 |
1.5 start和run
start:底层调用native方法,启动一个线程,自动执行run
run:直接调用run,只是普通的方法
1.6 如何保障线程安全
- 阻塞同步(使用锁)
- 非阻塞同步(CAS自旋)
- 不同步(ThreadLocal)
1.7 线程间通信方式
- volatile:保证变量线程可见性
- wait/notify:完成等待方和通知方的交互
- join
- 场景:主线程创建并启动子线程后,如果子线程要进行很耗时的计算,那么主线程将比子线程先结束,但是主线程需要子线程的计算的结果来进行自己下一步的计算,这时主线程就需要等待子线程
- 作用:在当前线程A调用线程B的join()方法后,会让当前线程A阻塞,直到线程B的逻辑执行完成,A线程才会解除阻塞,然后继续执行自己的业务逻辑,这样做可以节省计算机中资源。
2. 锁
2.1 目的
保证共享资源在任意时间内,只有一个线程访问
2.2 设计锁的概念
2.2.1 独享锁和共享锁
定义:
- 独享锁:该锁一次只能被一个线程持有
- 共享锁:该锁可以被多个线程持有
实现:AbstractQueuedSynchronizer
应用:
- 可重入锁:
synchronize
、ReentrantLock
是独享锁 - 读写锁:
ReadLock
是共享锁,WriteLock
是独享锁
2.2.2 乐观锁与悲观锁
悲观锁 | 乐观锁 |
---|---|
互斥锁、自旋锁、读写锁 | 无锁编程、CAS |
访问资源前先上锁 | 不加锁,先修改共享资源,再验证 |
多线程同时修改共享资源的概率较高,容易冲突 | 如果有冲突,则放弃本次操作,失败重试 |
CAS与自旋:
CAS本质是用新值来替换旧值的操作,成功或失败都是一次操作。
但通常都会在失败后自旋,轮询空转,直到成功
lock互斥锁
spin_lock自旋锁
2.3 锁的实现
2.3.1 互斥锁与自旋锁
互斥锁
-
特点
-
独享锁、悲观锁
-
获取锁时,失败则堵塞,进入等待队列
-
-
优点:在线程阻塞期间,不消耗CPU资源
-
问题:需要消耗大量资源来建立锁、消耗上下文切换成本
自旋锁
-
特点
- 独享锁、乐观锁
- 加锁失败后,线程忙等待,直到获得锁
-
实现
- 通过CPU提供的CAS函数实现,在用户态完成加锁和解锁
-
优点
- 对于持有锁较短的程序,使用自旋锁代替互斥锁,能够提高程序的性能
- 只需要消耗很少资源来建立锁
- 适合异步
-
问题
- 持有自旋锁的线程需尽快释放,否则浪费CPU资源
- 持有自旋锁的线程在sleep前需释放,否则可能导致整个系统挂起
-
应用
-
单CPU非抢占内核:自旋锁在编译时被忽略,相当于空操作
-
单CPU抢占内核:自旋锁仅仅作为一个设置抢占的开关,禁止内核抢占,但不自旋
-
多CPU:使用自旋锁即都为禁止抢占,且自旋
-
自旋锁用来防止多处理器中并发访问临界区(共享数据),防止内核抢占
-
可能死锁
抢占式调度:只运行某段时间,在时间段末尾发生时钟中断,挂起
非抢占式调度:让进程运行,直到阻塞会退出,不理会时钟中断
-
-
2.3.2 可重入与不可重入锁
可重入锁
-
定义:对于同一个线程在外层方法获取锁时,进入内层方法时也会自动获取该锁
-
优点:避免死锁,节省资源
不可重入锁,当同一线程重复调用同一成员方法时,会重复加锁,造成死锁
若要避免死锁,需要先释放原来的锁,再重新加锁,耗费资源
-
应用:
ReentrantLock
、synchronized
-
注意:使用必须记得开锁和关锁
-
原理
- 每一个锁关联一个线程持有者和计数器
- 计数器为0时,表示该锁未被任何线程持有,任何线程都可能获得该锁
- 当某一线程获取该锁成功,JVM记下该线程,并将计数器设为1
- 其他线程请求该锁,则必须等待
- 持有锁的线程再次获取锁,则可以拿到,计数器递增
- 当线程退出同步代码块时,计数器递减。如果计数器为0,则释放该锁
2.3.3 公平锁与不公平锁
- 公平锁:按照线程在CLH队列中的排队顺序,先到先得锁
- 非公平锁:调用lock时进行CAS抢锁,在等待队列中时,锁被释放后再进行CAS抢锁
2.3.4 分段锁
主要应用:ConcurrentHashMap
- JDK 1.8之前,对Segment加锁
- JDK 1.8之后,对Node加锁,只要哈希不冲突,就不产生并发
优点:多线程访问时,线程间不存在锁竞争,提高并发访问效率
缺点:维护多个锁,比维护独占锁更困难,且开销更大
2.3.5 偏向锁、轻量级锁、重量级锁
轻量级锁
- 应用场景:多个线程,交替执行,竞争锁
- 原理:利用CAS操作,无锁情况下实现线程安全
- 虚拟机栈中创建锁记录的空间(Lock Record):用于存储锁对象当前Mark Word的拷贝(Displaced Mark Word)
- 将对象头中的Mark Word复制到Lock Record中
- 拷贝成功,利用CAS操作尝试将对象的Mark Word更新为Lock Record指针,并将Lock Record中的owner指针指向对象的Mark Word
- 更新成功:线程就拥有了该对象的锁
- 更新失败:需要升级为重量级锁
偏向锁
- 背景:轻量级锁在没有竞争时,每次重入仍需要CAS操作
- 目的:优化轻量级锁,只有第一次需要CAS操作
- 原理:
- 第一次CAS操作,将线程ID设置到对象的Mark Word头
- 之后每次使用该对象,线程只需要检查Mark Word中的线程ID是否是自己
- 如果是自己,则说明没有竞争,不用重新CAS
重量级锁
- 应用场景:多个线程,同时,竞争锁
- 原理:OS层面的Mutex Lock,对象的Mark Word记录的是monitor对象的起始地址
2.4 死锁
2.4.1 死锁条件
- 互斥条件:资源任意时间只能由一个线程占用
- 持有并等待条件:线程因请求资源而阻塞时,对持有资源保持不释放
- 不可剥夺条件:线程已获得资源在为使用完前不可被其他线程剥夺
- 环路等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
2.4.2 避免死锁
- 破坏持有并等待条件:一次性申请所有资源
- 破坏不可剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放其占有资源
- 破坏环路等待条件:按序申请资源,释放资源反序
3. volatile
作用:保证修饰的变量对线程是可见的
变量存放位置:内存
实现原理:MESI缓存一致性协议
效果:保证有序性、可见性,但是不保证原子性
4. synchronized
作用:保证被修饰的方法或代码块,只能有一个线程执行
使用:
- 修饰实例方法:给对象加锁
- 修饰静态方法:给类加锁
- 修饰代码块:对给定对象加锁
实现原理:
- 同步方法:常量池设置
ACC_SYNCHRONIZED
,加监视器锁 - 同步代码块:使用
monitorenter
、monitorexit
指令,维护信号量计数器,相当于轻量级锁
效果:保证可见性、有序性、原子性
5. 线程池
5.1 定义
一种限制和管理资源的方式
5.2 作用
降低资源消耗:重复利用已创建的线程,减少线程创建和销毁的损耗
提高响应速度:任务到达时,不用等待线程创建,立即执行
提高线程的可管理性:统一分配线程资源,调优,监控
5.3 Executor
任务(Runnable/Callable接口)
- Runnable不会返回结果或抛出异常
- Callable可以返回结果或异常
执行(Executor接口)
- execute提交不需要返回值的任务
- submit提交需要返回值的任务,线程池返回Future类型对象
异步计算结果(Future接口)
5.4 ThreadPoolExecutor(推荐使用)
好处:明确线程池的运行规则,规避资源耗尽的风险
5.4.1 重要参数
corePoolSize
:核心线程数,定义最小可以同时运行的线程数maximumPoolSize
:任务队列达到最大容量时,当前可以同时运行的线程数变为此最大值workQueue
:任务队列,新任务到来时判断当前运行的线程数量是否达到核心线程数
5.4.2 其他参数
keepAliveTime
:等待时间,线程池中线程超过核心线程数时,经过此等待时间后再销毁unit
:keepAliveTime
的时间单位threadFactory
:创建新线程时用到handler
:饱和策略AbortPolicy
:直接抛出RejectedExecutionException
异常,ThreadPoolExecutor
默认CallerRunsPolicy
:如果线程池未关闭,则当前线程自己调用自己的run(),注意不是启动新线程DiscardPolicy
:无操作,相当于丢弃此任务DiscardOldestPolicy
:弹出线程池等待队列的队首任务,再次提交新加入的任务
5.5 常见线程池
FixedThreadPool
、SingleThreadExecutor
:允许请求的队列长度为Integet.MAX_VALUE
,可能堆积大量请求,导致OOMCachedThreadPool
、ScheduledThreadPool
:允许创建的线程数量为Inthger.MAX_VALUE
,可能创建大量线程,导致OOM
5.6 线程池大小确定
- CPU密集型任务(N+1)
- IO密集型任务(2 N)
5.7 线程池使用
- 构造函数声明线程池
- 监测线程池运行状态
- 不同业务使用不同的线程池
- 线程池命名
- 正确配置线程池参数
5.8 线程池实现原理
6. AQS
6.1 定义
AbstractQueueSynchronizer
:抽象队列同步器- 一个实现锁的框架
- 内部结构:先进先出队列、state状态、内部类
ConditionObject
- 线程模式:独享、共享
6.2 实现原理
同步状态
- volatile修饰state,实现线程可见性
- CAS修改state,保证原子性
CLH先进先出队列
CLH:带头节点的双向队列
- 每个节点存放线程和state
acquire方法:获取独占锁
tryAcquire
尝试获取资源,由子类实现- 获取失败,加入等待队列
- 根据前置节点状态判断是否应该继续获取资源(前置为头节点,继续尝试)
release方法:释放独占锁
tryRelease
释放锁,由子类实现- 唤醒合适节点
公平锁与非公平锁
- 公平锁:按照线程在队列中的排队顺序,先到先得锁
- 非公平锁:调用lock时进行CAS抢锁,在等待队列中时,锁被释放后再进行CAS抢锁
6.3 应用
- Semaphore信号量
- CountDownLatch倒计时器
- CycliBarrier循环栅栏
- ReentrantLock
7. ThreadLocal
7.1 定义
提供线程的局部变量,对其他线程而言是隔离的
7.2 应用
- 管理Connection
- 避免参数传递(Cookie和Session)
7.3 数据结构
Thread -->ThreadLoaclMap(类似HashMap)-->Entry数组
- key: ThreadLocal
- value: Object
7.4 实现原理
hash、set、get、扩容
7.5 内存泄露问题
8. Atomic
8.1 基本类型
AtomicInteger
AtomicLong
AtomicBoolean
8.2 数组类型
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
8.3 引用类型
AtomicReference
AtomicMarkableReference
AtomicStampedReference
8.4 对象属性修改更新器
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater
8.5 ABA问题
问题
解决
8.6 实现原理
利用CAS、volatile、native方法保证原子操作
- CAS:拿期望值和原本的一个值做比较,相同则更新
- native: Unsafe类的
objectFieldOffset()
是本地方法,拿到原来值的内存地址 - volatile: value用volatile修饰,确保线程可见性