知识点扫盲-多线程
2021-09-01 14:39:38 78 举报
AI智能生成
小白一枚,多学多记!
作者其他创作
大纲/内容
进程/线程
进程
一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程
线程
进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
区别
知乎上有这么个比喻:进程-->火车,线程-->车厢;线程在进程下行进(单纯的车厢无法运行)<br>
1.一个进程可以包含多个线程(一辆火车可以有多个车厢)<br>
2.不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
3.同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
4.进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
5.进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
6.进程可以拓展到多机,线程最多扩展到多核CPU,而不能扩展到多机(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
7.进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(如火车上的洗手间)-互斥锁
8.进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-信号量
Lock
ReentrantLock
一般的锁,对于同一个线程,如果连续两次对同一把锁进行lock,这个线程就会被永远卡死在那边<br>
重入锁使得同一个线程可以对同一把锁,在不释放的前提下,反复加锁,而不会导致线程卡死
保证lock()、unlock()次数一样多<br>
void lock()
加锁,如果锁已经被别人占用了,就<font color="#000000">无限等待</font><br>
大规模得在复杂场景中使用,是有可能因此死锁的
boolean tryLock(long timeout, TimeUnit unit)
尝试获取锁,等待timeout时间。同时,可以响应中断
相较于lock()好处
可以不用进行无限等待<br>
可以在应用程序这层进行进行自旋,你可以自己决定尝试几次,或者是放弃<br>
等待锁的过程中可以响应中断
boolean tryLock()
这个不带任何参数的tryLock()不会进行任何等待,如果能够获得锁,<br>直接返回true,如果获取失败,就返回false,特别适合在应用层自<br>己对锁进行管理,在应用层进行自旋等待<br>
ReentrantLock里面有一个内部类Sync,Sync继承AQS,添加锁和释放锁的大部分操作实际上都是在Sync中实现的
reentrantLock.lock()
重入锁的核心功能委托给内部类Sync实现,并且根据是否是公平锁有FairSync和NonfairSync两种实现
ReentrantLock(boolean fair)---sync = fair ? new FairSync() : new NonfairSync()
默认不公平,随机<br>
ReentrantLock()---sync = new NonfairSync()
公平/不公平区别:<br>
公平锁是有代价的:维持公平竞争是以牺牲系统性能为代价的
非公平锁会调用compareAndSetState(),试图抢占,<font color="#f44336">如果第一次争抢失败,<br>后面的处理和公平锁是一样的,都是进入等待队列慢慢等</font><br>
acquire()
进入队列等待
实现重入锁的方法很简单,就是基于一个状态变量state。这个变量保存在AQS对象中;当这个state==0时,表示锁是空闲的,大于零表示锁已经被占用, 它的数值表示当前线程重复占用这个锁的次数
重入锁的伴生对象Condition提供了await()和singal()的功能,可以用于线程间消息通信
乐观锁
CAS
含义
compare and swap,比较并替换,是乐观锁的一种实现方式,是一种轻量级锁;<br>JUC中很多工具类的实现都是基于CAS的<br>
怎么实现线程安全<br>
线程在读取数据时不进行加锁,在回写数据时,先去查询原值,操作的时候比较原值是否修改,<br>若未被其他线程修改则写回,否则重新执行读取程序<br>
比较+更新,整体是一个原子操作<br>
举例:一个线程修改订单的状态(待支付-->已支付),修改前判断状态是不是待支付,<br>如果没有被其他线程修改状态则更新,如果修改了则重新查询<br>
存在的问题
ABA
cas比较过程中,数据被其他线程更改又改回来了<br>
解决
加标志位,如版本号、时间戳、自增长字段
svn提交代码,版本号不一致会冲突
循环时间长,开销大
cas比较一直失败,会一直自旋,CPU压力大
自适应自旋
只能保证一个变量的原子操作<br>
JUC工具类
AtomicInteger<br>
自增函数incrementAndGet(),判断偏移量是否一致do,里面用的do-while循环,不成功一直循环<br>
AtomicReference
可以用来保证对象之间的原子性,就可以把多个对象放入CAS中操作
项目实践
在很多订单表,流水表,为了防止并发问题,就会加入CAS的校验过程,保证了线程的安全,但是看场景使用,并不是适用所有场景,他的优点缺点都很明显<br>
LongAdder替换AtomicLong
内部的实现有点类似ConcurrentHashMap的分段锁,最好的情况下,每个线程都有独立的计数器,这样可以大量减少并发操作
悲观锁
Synchronized<br>
对象
对对象进行加锁,在 JVM 中,<br>对象在内存中分为三块区域:<br>
对象头(Header)
Mark Word(标记字段):用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 <br>ID、偏向时间戳等等;它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化<br>
锁状态
无锁
001
偏向锁
101
轻量级锁
010
重量级锁
011
Klass Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
实例数据(InstanceData)
对齐填充(Padding)
修饰<br>
静态方法
锁当前类的实例
实例方法
锁对象实例
代码块
synchronized对象实例<br>
方法
flags: ACC_SYNCHRONIZED<br>
代码块
通过 monitorenter 和 monitorexit 实现的
每个monitor维护着一个记录着拥有次数的计数器。未被拥有的monitor的该计数器为0,<br>当一个线程获得monitor(执行monitorenter)后,该计数器自增变为 1<br>
当同一个线程再次获得该monitor的时候,计数器再次自增
当不同线程想要获得该monitor的时候,就会被阻塞
当同一个线程释放 monitor(执行monitorexit指令)的时候,计数器再自减。<br>当计数器为0的时候,monitor将被释放,其他线程便可以获得monitor
以前一直说是重量级,<br>现在锁升级<br>
重锁
内核态和用户态的切换(线程的等待唤起过程),大量系统资源消耗<br>
依赖底层的Mutex Lock实现
被优化了,1.6为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁<br>
针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,<br>就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁<br>
锁只能升级,不能降级
特性保证
原子性
单一线程持有,确保同一时间只有一个线程能拿到锁<br>
可见性
内存强制刷新
有序性
as-if-serial
happens-before
可重入性<br>
计数器
synchronized锁对象的时候有个计数器,他会记录下线程获取锁的次数,在执行完对应的代码块之后,<br>计数器就会-1,直到计数器清零,就释放锁了<br>
不可中断性
一个线程获取锁之后,另外一个线程处于阻塞或者等待状态,前一个不释放,后一个也一直会阻塞或者等待,不可以被中断
PS:Lock的tryLock方法是可以被中断的<br>
sync和lock的区别
synchronized是关键字,是JVM层面的;而Lock是一个接口,是JDK层面的有丰富的API<br>
synchronized会自动释放锁,而Lock必须手动释放锁
synchronized是不可中断的,Lock可以中断也可以不中断<br>
通过Lock可以知道线程有没有拿到锁,而synchronized不能<br>
synchronized能锁住方法和代码块,而Lock只能锁住代码块<br>
Lock可以使用读锁提高多线程读效率<br>
synchronized是非公平锁,ReentrantLock可以控制是否是公平锁
ReentrantLock
AQS(AbstractQueuedSynchronizer)
也就是队列同步器,这是实现 ReentrantLock 的基础
AQS有一个state标记位,值为1时表示有线程占用,其他线程需要进入到同步队列等待,同步队列是一个双向链表<br>
当获得锁的线程需要等待某个条件时,会进入<font color="#ff0000">condition的等待队列</font>,等待队列可以有多个
当condition条件满足时,线程会从等待队列重新进入同步队列进行获取锁的竞争
对AQS理解
AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配
private volatile int state<span class="" style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; white-space: pre; color: rgb(166, 38, 164); line-height: 26px; box-sizing: border-box !important; overflow-wrap: break-word !important;"></span><span style="color: rgb(56, 58, 66); font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace; font-size: 12px; white-space: pre; background-color: rgb(250, 250, 250);"></span>
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作
AQS使用CAS对该同步状态进行原子操作实现对其值的修改
状态信息通过protected类型的getState,setState,compareAndSetState进行操作
CAS
共享方式
Exclusive(独占)<br>
ReentrantLock
share(共享)
CountDownLatch
Semaphore(信号量)
维护了一个先进先出的队列和一个volatile修饰的state状态变量<br>
双向队列(Node)
4种状态
CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)和0<br>
state变量
加锁
以非公平锁为了,我们在外界调用lock方法的时候,源码是这样实现的<br>1:CAS尝试获取锁,获取成功则可以执行同步代码<br>2:CAS获取失败,则调用acquire方法,acquire方法实际上就是AQS的模板方法<br>3:acquire首先会调用子类的tryAcquire方法(又回到了ReentrantLock中)<br>4:tryAcquire方法实际上会判断当前的state是否等于0,等于0说明没有线程持有锁,则又尝试CAS直接获取锁<br>5:如果CAS获取成功,则可以执行同步代码<br>6:如果CAS获取失败,那判断当前线程是否就持有锁,如果是持有的锁,那更新state的值,获取得到锁(这里其实就是处理可重入的逻辑)<br>7:CAS失败&&非重入的情况,则回到tryAcquire方法执行「入队列」的操作<br><font color="#f44336">8:将节点入队列之后,会判断「前驱节点」是不是头节点,如果是头结点又会用CAS尝试获取锁<br>9:如果是「前驱节点」是头节点并获取得到锁,则把当前节点设置为头结点,并且将前驱节点置空(实际上就是原有的头节点已经释放锁了)<br>10:没获取得到锁,则判断前驱节点的状态是否为SIGNAL,如果不是,则找到合法的前驱节点,并使用CAS将状态设置为SIGNAL<br>11:最后调用park将当前线程挂起</font>
解锁
1:外界调用unlock方法时,实际上会调用AQS的release方法,而release方法会调用子类tryRelease方法(又回到了ReentrantLock中)<br>2:tryRelease会把state一直减(锁重入可使state>1),直至到0,当前线程说明已经把锁释放了<br>3:随后从队尾往前找节点状态需要 < 0,并离头节点最近的节点进行唤醒<br>唤醒之后,被唤醒的线程则尝试使用CAS获取锁,假设获取锁得到则把头节点给干掉,把自己设置为头节点<br>解锁的逻辑非常简单哈,把state置0,唤醒头结点下一个合法的节点,被唤醒的节点线程自然就会去获取锁<br>
为什么设置前置节点Signal
表示后继节点需要被唤醒
其实归终结底就是为了判断节点的状态,去做些处理。<br>
volatile<br>
JMM
JavaMemoryModel--Java内存模型
用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果
JMM并不是实际存在的,而是一套规范,这个规范描述了很多java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,Java内存模型是对共享数据的可见性、有序性、和原子性的规则和保障
由于计算机的存储设备与处理器的运算速度有几个数量级的差距,引入高速缓存解决速度不匹配的问题<br>
将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就<br>无须等待缓慢的内存读写了;<br>基于高速缓存的存储交互,为计算机系统带来更高的复杂度,引入了新问题:<font color="#ff0000">缓存一致性(CacheCoherence)</font><br>
在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存
为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,<br>这类协议有MSI、<font color="#ff0000">MESI</font>(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol等<br>
MESI
缓存一致性协议
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取
特性
原子性
JMM只能保证基本的原子性,如果要保证一个代码块的原子性,提供了monitorenter 和 moniterexit <br>两个字节码指令,也就是 synchronized 关键字。因此在 synchronized 块之间的操作都是原子性的<br>
可见性<br>
可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了
volatile<br>
volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去
synchronized
不管怎么重排序,单线程下的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义
final
被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去,<br>那么在其他线程就能看见final字段的值(无须同步)<br>
有序性
重排序
为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。
分类
编译器重排序,指令级并行的重排序,内存系统重排序
as-if-serial<br>
可以使用synchronized或者volatile保证多线程之间操作的有序性
volatile
volatile关键字是使用<font color="#f44336">内存屏障</font>达到禁止指令重排序,以保证有序性
volatile写是在前面和后面分别插入内存屏障<br>
前:StoreStore屏障
后:StoreLoad屏障
而volatile读操作是在后面插入两个内存屏障
后1:LoadLoad屏障
后2:LoadStore屏障
第一个操作是volatile读,无论第二个是什么操作,都禁止重排
第二个操作是volatile写,无论第一个是什么操作,都进制重排
第一个操作是volatile写,第二个是volatile读,禁止重排
happens-before
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系<br>
synchronized
一个线程lock之后,必须unlock后,其他线程才可以重新lock,使得被synchronized包<br>住的代码块在多线程之间是串行执行的<br>
8种内存交互操作<br>
见流程图
volatile与synchronized的区别<br>
volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块
volatile保证数据的可见性,但是不保证原子性;而synchronized是一种排他(互斥)的机制<br>
volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了
总结
volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如booleanflag;或者作为触发器,实现轻量级同步<br>
volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁_上,所以说它是低成本的<br>
volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序<br>
volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓存,始终从主 存中读取
volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作<br>
volatile可以使得long和double的赋值是原子的<br>
volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性
ThreadLocal<br>
是什么?
ThreadLocal提供了线程的局部变量,每个线程都可以通过set()和get()来对这个局部变量进行操作,<br>但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离<br>
private static
这种变量在线程的生命周期内起作用,可以减少同一个线程内多个函数或者组件之间一些公共变量传递的复杂度
实现原理
set()
1.获取当前线程 2.获取ThreadLocalMap对象 3.校验对象是否为空,不为空set,为空初始化一个map对象
ThreadLocalMap
当前线程Thread一个叫threadLocals的变量中获取的
每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据<br>是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离<br>
ThreadLocalMap其实就是ThreadLocal的一个静态内部类,每个Thread维护一个ThreadLocalMap映射表,<br>这个映射表的key是ThreadLocal实例本身,value是真正要存储的Object<br>
数组,非链表,怎么解决hash冲突?
ThreadLocal.ThreadLocalMap是一个比较特殊的Map,<br>它的每个Entry的key都是一个弱引用<br>
Entry继承WeakReference<br>
这样设计的好处是,如果这个变量不再被其他对象使用时,可以自动回收这个ThreadLocal对象,避免可能的内存泄露<br>
ThreadLocal为了避免内存泄露,不仅使用了弱引用维护key,<br>还会在每个操作上检查key是否被回收,进而再回收value<br>
remove()/set()<br>
get()方法总是访问固定几个一直存在的ThreadLocal(如使用线程池,核心线程一直运行的不会被回收)<br>那么清理动作就不会执行,如果你没有机会调用set()和remove(),那么这个内存泄漏依然会发生<br>
当你不需要这个ThreadLocal变量时,主动调用remove(),这样对整个系统是有好处的
内存泄漏
InheritableThreadLocal
父子类
作用?
主线程开了一个子线程,希望在子线程中可以访问主线程中的ThreadLocal对象
应用
数据库连接<br>
ThreadLocal能够实现当前线程的操作都是用同一个Connection,保证了事务!
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复
线程池
优势
降低资源消耗<br>
通过重复利用已创建的线程降低线程创建和销毁造成的消耗
提高响应速度
当任务到达时,任务可以不需要等到线程创建就能立即执行
提高线程的可管理性
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
ThreadPoolExecutor
参数
corePoolSize-核心线程数量<br>
默认情况下,核心线程会一直存活,但是当将allowCoreThreadTimeout设置为true时,核心线程也会超时回收
maximumPoolSize-最大线程数
线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞
keepAliveTime-线程闲置超时时长<br>
如果超过该时长,非核心线程就会被回收。如果将allowCoreThreadTimeout设置为true时,核心线程也会超时回收
unit<br>
指定keepAliveTime参数的时间单位。常用:TimeUnit.MILLISECONDS(毫秒)/TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)
workQueue-任务队列
通过线程池的execute()方法提交的Runnable对象将存储在该参数中。其采用阻塞队列实现<br>
threadFactory-线程工厂<br>
用于指定为线程池创建新线程的方式
handler-拒绝策略<br>
当达到最大线程数时需要执行的饱和策略
ThreadPoolExecutor.AbortPolicy
默认,抛异常
ThreadPoolExecutor.CallerRunsPolicy
ThreadPoolExecutor.DiscardPolicy
ThreadPoolExecutor.DiscardOldestPolicy
丢弃最早的未处理的任务请求
自定义
记录,做事后补偿
工具类
newCachedThreadPool<br>
没有核心线程,线程数无限大,SynchronousQueue
newFixedThreadPool<br>
只有核心线程,定义核心线程数,LinkedBlockingQueue,队列无限大
newSingleThreadExecutor
只有1个核心线程
....
自定义线程池<br>
提交优先级
核心线程-->队列-->非核心线程<br>
执行优先级
核心线程-->非核心线程-->队列 addWorker
ctl是什么
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
RUNNING = -1 << COUNT_BITS<br>
COUNT_BITS = Integer.SIZE - 3
ctl 是一个涵盖了两个概念的原子整数类,它将工作线程数和线程池状态结合在一起维护,低 29 位存放 workerCount,高 3 位存放 runState
其实并发包中有很多实现都是一个字段存多个值的,比如读写锁的高 16 位存放读锁,低 16 位存放写锁,<br>这种一个字段存放多个值可以更容易的维护多个值之间的一致性<br>
0 条评论
下一页