并发专题阶段性总结1
2023-03-20 22:06:57 0 举报
AI智能生成
并发专题
作者其他创作
大纲/内容
进程就是用来加载指令、管理内存、管理 IO 的
进程就可以视为程序的一个实例
操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源)
进程是资源分配的最小单位
进程
线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程。
线程是操作系统调度(CPU调度)执行的最小单位
线程
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
进程拥有共享的资源,如内存空间等,供其内部的线程共享
管道(pipe)
信号(signal)
消息队列(message queue)
共享内存(shared memory)
套接字(socket)
进程间通信的方式
进程间通信较为复杂,线程通信相对简单
区别
线程和进程
线程之间所具有的一种制约关系
一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒
同步
对于共享的进程系统资源,在各单个线程访问时的排它性
当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
互斥
临界区
互斥量
信号量
事件
同步互斥的控制方法
线程的同步互斥
是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换
是CPU内部的一小部分非常快的内存
通过提供对常用值的快速访问来加快计算机程序的执行
寄存器
专门的寄存器,它指示CPU在其指令序列中的位置,并保存着正在执行的指令的地址或下一条要执行的指令的地址
程序计数器
上下文是CPU寄存器和程序计数器在任何时间点的内容。
暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中的某个地方
从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程
过程
上下文切换只能在内核模式下发生
上下文切换
初始状态
可运行状态
运行状态
休眠状态
终止状态
操作系统层面的线程生命周
线程生命周期
线程基础知识
基于Monitor机制实现,依赖底层操作系统的互斥原语Mutex(互斥量),它是一个重量级锁,性能较低
jdk1.5之前
锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自适应自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平
jdk1.5之后
方法中的access_flags中设置ACC_SYNCHRONIZED
0x0029
同步方式
通过monitorenter和monitorexit来实现
UnsafeFactory.getUnsafe().monitorEnter(lock);//加锁 try{ counter--; }finally { UnsafeFactory.getUnsafe().monitorExit(lock);//解锁 }
UnsafeFactory.getUnsafe().monitorEnter(lock);//加锁
UnsafeFactory.getUnsafe().monitorExit(lock);//解锁
方式会被淘汰,不推荐使用
小知识点
同步代码块
synchronized的字节码指令序列
while(条件不满足) { wait();}
编程范式
共享变量
多个线程试图进入时排队,只允许一个线程进入其他线程等待
引入入口等待队列
为了解决互斥问题
条件变量
引入条变量等待队列
为了解决同步问题
满足以下三个条件时,可以使用notify(),其余情况尽量使用notifyAll():
所有等待线程拥有相同的等待条件;所有等待线程被唤醒后,执行相同的操作;只需要唤醒一个线程。
notify()和notifyAll()分别何时使用
MESA模型
Java 语言内置的管程里只有一个条件变量
表明支持重入
_recursions = 0; // 锁的重入次数
_WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
先进后出,说明是非公平锁
_cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
_EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
_object = NULL; //存储锁对象
_owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
ObjectMonitor数据结构
在Java中的实现
Monitor(管程/监视器)
hash码
GC分代年龄
对象锁
锁状态标志
线程持有的锁
偏向线程ID
Mark Word
Klass Pointer
数组长度(只有数组对象有)
对象头
存放类的属性数据信息,包括父类的属性信息
实例数据
对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐
对齐填充
对象内存分部
biased_lock_pattern = 5 //101 偏向锁
锁标记枚举
一种针对加锁操作的优化手段
新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态
jdk6默认开启
JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等
在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向
为了减少初始化时间,JVM默认延时加载偏向锁
虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁模式
偏向锁延迟偏向
调用锁对象的obj.hashCode()或System.identityHashCode(obj)方法会导致该对象的偏向锁被撤销
HashCode只会生成一次并保存,偏向锁是没有地方保存hashcode的
当对象可偏向时,MarkWord将变成未锁定状态,并只能升级成轻量锁
轻量级锁会在锁记录中记录 hashCode
当对象正处于偏向锁时,调用HashCode将使偏向锁强制升级成重量锁
重量级锁会在 Monitor 中记录 hashCode
调用对象HashCode
notify() 会升级为轻量级锁
wait(timeout) 会升级为重量级锁
调用wait/notify
偏向锁撤销
偏向锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构
轻量级锁所适应的场景是线程交替执行同步块的场合
轻量级锁释放回先变成无锁状态,然后其他线程才能加锁,否则CAS无法操作
轻量级锁
重量级锁释放之后变为无锁
重量级锁
锁对象记录锁的状态
底层原理
synchronized的使用
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区,其共享资源为临界资源
private static int counter = 0;//临界资源public static void increment() { //临界区 counter++;}
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
阻塞式的解决方案:synchronized,Lock
非阻塞式的解决方案:原子变量
避免临界区的竞态条件发生
竞态条件
基本概念
JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁
锁消除的依据是逃逸分析的数据支持
锁消除
对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的
将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部
锁粗化
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势
对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次
反之,就少自旋甚至不自旋
在 Java 6 之后自旋是自适应的
Java 7 之后不能控制是否开启自旋功能
挂起操作涉及系统调用,存在用户态和内核态切换
自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程
自旋优化
背景:在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降
class维护一个偏向锁撤销计数
class的对象发生偏向撤销操作时,该计数器+1
这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。
原理
一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作
批量重偏向(bulk rebias)
在明显多线程竞争剧烈的场景下使用偏向锁是不合适的
批量撤销(bulk revoke)
批量重偏向和批量撤销是针对类的优化,和对象无关
偏向锁重偏向一次之后不可再次重偏向
当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利
偏向锁批量重偏向&批量撤销
synchronized锁优化
synchronized
当一个线程修改了共享变量的值,其他线程能够看到修改的值
通过 volatile 关键字
通过 内存屏障
通过 synchronized
通过 Lock
通过 final
保证可见性
可见性
即程序执行的顺序按照代码的先后顺序执行
有序性
一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行
原子性
并发编程的可见性,原子性与有序性问题
JMM是围绕原子性,有序性、可见性展开
是共享数据区域
所有线程创建的实例对象都存放在主内存中
实例对象是成员变量还是方法中的本地变量(也称局部变量)
类信息
常量
态变量
信息包括
多条线程对同一个变量进行访问可能会发生线程安全问题
主内存
主要存储当前方法的所有本地变量信息
还有主内存中的变量副本拷贝
线程中的本地变量对其它线程是不可见的
线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。
工作内存
缓存
硬件内存
JMM只是一种抽象的概念,是一组规则,并不实际存在
JMM
内存模型与硬件内存架构的关系
lock(锁定)
unlock(解锁)
read(读取)
load(载入)
use(使用)
assign(赋值)
store(存储)
write(写入)
八大原子操作
主内存与工作内存之间的具体交互协议
JMM模型
控制器(Control)
运算器(Datapath)
存储器(Memory)
输入(Input system)
输出(Output system)
计算机五大核心组成部分
控制单元是整个CPU的指挥控制中心,由指令寄存器IR(Instruction Register)、指令译码器ID(Instruction Decoder)和 操作控制器OC(Operation Controller) 等组成
控制单元
运算单元是运算器的核心。可以执行算术运算(包括加减乘数等基本运算及其附加运算)和逻辑运算(包括移位、逻辑测试或两个值比较
运算单元
存储单元包括 CPU 片内缓存Cache和寄存器组,是 CPU 中暂时存放数据的地方
数据单元
CPU指令结构
L1 Cache,分为数据缓存和指令缓存,逻辑核独占
L2 Cache,物理核独占,逻辑核共享
L3 Cache,所有物理核共享
三级缓存结构
存储器存储空间大小:内存>L3>L2>L1>寄存器
存储器速度快慢排序:寄存器>L1>L2>L3>内存;
特点
高速缓存以解决I\\O速度和CPU运算速度之间的不匹配问题
CPU缓存结构
操作系统内部内部程序指令通常运行在ring0级别
Linux与Windows只用到了2个级别:ring0、ring3
,操作系统以外的第三方程序运行在ring3级别
所以线程阻塞唤醒是重型操作了
ring0ring1ring2ring3
4个运行级别
CPU运行安全等级
可由用户代码 和 内核代码进行引用
用户空间
只能由内核代码进行访问
内核空间
因为有两种空间,进程与线程只能运行在用户方式(usermode)或内核方式(kernelmode)下
内核线程(KLT)
用户线程(ULT)
内存管理
计算机基础
在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就是局部性原理
时间局部性(Temporal Locality)
如果一个存储器的位置被引用,那么将来他附近的位置也会被引用
比如顺序执行的代码、连续创建的两个对象、数组等
例如一个二维数据求和,你横向相加的速度,必定快于纵向相加的速度
空间局部性(Spatial Locality)
局部性原理
CPU高速缓存
JMM与三大特性
本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程
使用 Thread类或继承Thread类
实现 Runnable 接口配合Thread
使用有返回值的 Callable
使用 lambda
线程的实现方式
Java线程实现原理
线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上
好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题
坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里
优缺点
协同式线程调度
每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定
抢占式线程调度
调度机制
NEW(初始化状态)
RUNNABLE(可运行状态+运行状态)
BLOCKED(阻塞状态)
WAITING(无时限等待)
TIMED_WAITING(有时限等待)
TERMINATED(终止状态)
Java线程的生命周期
调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁
其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志
睡眠结束后的线程未必会立刻得到执行
sleep当传入参数为0时,和yield相同
sleep方法
yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁
假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程
具体的实现依赖于操作系统的任务调度器
yield方法
等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景
join方法
stop()方法已经被jdk废弃,原因就是stop()方法太过于暴力,强行把执行到一半的线程终止
stop会释放对象锁,可能会造成数据不一致。
stop方法
Thread常用方法
Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制
被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。
interrupt(): 将线程的中断标志位设置为true,不会停止线程
isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle
API的使用
while (!Thread.currentThread().isInterrupted() && more work to do)
利用中断机制优雅的停止线程
使用中断机制时一定要注意是否存在中断标志位被清除的情况
Java线程的中断机制
volatile
等待唤醒机制可以基于wait和notify方法来实现
LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待“许可”,调用unpark则为指定线程提供“许可”。
等待唤醒(等待通知)机制
Java线程间通信
Java线程详解
深入理解Java线程
并发专题阶段性总结
0 条评论
回复 删除
下一页