终于会了,java锁的底层原理
共 12268字,需浏览 25分钟
·
2021-05-19 02:37
点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
知识整理
Synchronized 内置锁,JVM级别
代码优化:同步代码块、减少锁粒度、读锁并发
JDK自带 偏置锁、轻量级锁(CAS操作)、自适应自旋、锁粗化、锁消除
使用
底层 锁升级过程、CAS操作的缺点【替换线程和copy mw】
优化
Volatile
概念:非阻塞可见性、禁止指令重排序*
与syn区别: 无法实现原子操作、使用场景--单线程、不依赖当前值
Reentrantlock 显示锁:基于AQS实现,API级别
数据结构:state、waitstate【signal-1、传播-3】、
独占、共享 tryAcquireShared
AQS原理:
非公平锁
特性锁 可重入、轮询、定时、可中断
优点、使用场景
与Syn区别、Syn优点
死锁
检测算法:资源分配表、遍历锁关系图
撤销进程、设置线程随机优先级
概念:多个线程因竞争资源而互相等待的僵局;4个必要条件:资源互斥、不可剥夺、保持与请求、循环等待
死锁避免:锁顺序、锁时限、死锁检测与恢复
死锁检测与恢复:分配资源时不加条件;检测时机:进程等待、定时、利用率下降
锁模式
读锁、写锁
乐观锁:用户解决---数据版本id、时间戳;CAS;适合写操作少的场景;MVCC实现
悲观锁:数据库行锁、页锁...
synchronized的4种应用方式 jvm内部实现 称为:内置锁
修饰类,作用范围:synchronized括号内, 作用对象:类的所有对象;synchronized(Service.class){ }
修改静态方法,作用范围:整个静态方法, 作用对象:类的所有对象;
修饰方法,被修饰的同步方法,作用范围:整个方法, 作用对象:调用这个方法的对象;
缺点:A线程执行一个长时间任务,B线程必须等待
修饰代码块,被修饰的代码块同步语句块,作用范围:大括号内的代码, 作用对象:调用这个代码块的对象;
优点:减少锁范围,耗时的代码放外面,可以异步调用
notifyAll方法实现
锁住(lock)
主->从 read load 将需要的数据从主内存拷贝到自己的工作内存(read and load)
修改 use assign 根据程序流程读取或者修改相应变量值(use and assign)
从->主 store write将自己工作内存中修改了值的变量拷贝回主内存(store and write)
释放对象锁(unlock)
当多个线程访问某个类,其始终能表现出正确的行为
采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,限制其他线程访问,直到锁释放
Java中的锁优化 代码方式、JDK自带方式
减少锁持有时间
使用同步代码块,而非同步方法;
减小锁粒度
JDK1.6中 ConcurrentHashMap采取对segment加锁而不是整个map加锁,提高并发性;
锁分离 读锁之间不互斥;读写分离
根据同步操作的性质,把锁划分为的读锁和写锁,读锁之间不互斥,提高了并发性
锁主要存在四中状态,依次是:无锁状态01、偏向锁状态01、轻量级锁状态00、重量级锁状态10,
会随着竞争的激烈而逐渐升级,锁可以升级不可降级,提高 获得锁和释放锁 效率
“轻量级锁”和“偏向锁”作用:减少 获得锁和释放锁 的性能消耗
访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
如果为可偏向状态,则判断偏向线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;
如果竞争失败,执行4,偏向锁升级为轻量级锁。
如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
执行同步代码
虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝【因为栈帧为线程私有,对象大家都有】
拷贝对象头中的Mark Word复制到锁记录(Lock Record)中;
拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word中的,更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。
如果一系列的连续操作都对同一个对象反复加锁和解锁,如循环体内,很耗性能
加锁同步的范围扩展到整个操作序列的外部:第一个append到最后一个append;不对每个append加锁
CAS底层实现原理 用于更新数据
适合资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;
CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,可以获得更高的性能
synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS
原子类中都使用到了CAS
volatile详解
volatile保证新值立即同步到主存,
线程对变量读取的时候,要从主内存中读,而不是缓存
变量的赋值一旦变化就会通知到其他线程,如果其他线程的工作内存中存在这个同一个变量拷贝副本,那么其他线程会放弃这个副本中变量的值,重新去主内存中获取
为了减少CPU空闲时间,java不能保证程序执行的顺序与代码中一致,
volatile修饰的变量相当于生成内存屏障,重排序时不能把后面的指令排到屏障之前;指令屏障
作用:为了保证happen-before原则:写、锁lock、传递性、线程启动、中断、终结、对象创建的先后关系
定义了线程、锁、volatile变量、对象创建的先后关系
若满足,则保证一个操作执行的结果需要对另一个操作可见
判断数据是否存在竞争、线程是否安全的依据
可重入锁 Re entrantlock
无阻塞的同步机制(非公平锁实现)
可实现轮询锁、定时锁、可中断锁特性;
提供了一个Condition(条件)类,对锁进行更精确的控制
默认使用非公平锁,可插队跳过对线程队列的处理(因此被称为可重入)
ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。
公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;唤醒锁的时间CPU浪费;是否是AQS队列中的头结点
非公平锁:线程获取锁的顺序和调用lock的顺序无关,先执行lock方法的锁不一定先获得锁
加锁和解锁都需要显式写出,实现了Lock接口,注意一定要在适当时候unlock
总结:公平锁与非公平锁对比
FairSync:lock()少了插队部分(即少了CAS尝试将state从0设为1,进而获得锁的过程)
FairSync:tryAcquire(int acquires)多了需要判断当前线程是否在等待队列首部的逻辑(实际上就是少了再次插队的过程,但是CAS获取还是有的)。
公平锁的核心
获取一次锁数量,state值
如果锁数量为0,如果当前线程是等待队列中的头节点,基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;
如果锁数量不为0或者当前线程不是等待队列中的头节点或者上边的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒。
非公平锁 两者都是非公平锁
非公平锁,可以直接插队获取锁,跳过了对队列的处理,速度会更快
公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销;
AQS底层原理:在lock获取锁时首先判断当前锁是否可以用(AQS的state状态值是否为0),如果是 直接“插队”获取锁,否则进入排队队列,并阻塞当前线程;充分利用了唤醒线程的时间【Singel标志唤醒,需要前驱节点唤醒】
非公平锁加锁的简单步骤
如果设置成功,设置当前线程为独占锁的线程;
如果设置失败,还会再获取一次锁数量,---第二次插队
如果锁数量为0,再基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;
如果锁数量不为0或者上边的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒
入队后,无限循环tryAcquire(1)方法 ---第三次插队
非公平锁源码
首先会使用addWaiter(Node.EXCLUSIVE)将当前线程封装进Node节点node,然后将该节点加入等待队列
先快速入队【存在尾节点,将使用CAS尝试将尾节点设置为node】
如果快速入队不成功【尾节点为空】,使用正常入队方法enq,无限循环=第一次阻塞,直到Node节点入队为止【创建一个dummy节点,并将该节点通过CAS设置到头节点,若头结点不为null,cas继续快速入队】
如果p不是头节点,或者tryAcquire(1)请求不成功,执行shouldParkAfterFailedAcquire(Node pred, Node node)来检测当前节点是不是可以安全的被挂起:判断p的等待状态waitStatus
SIGNAL(即可以唤醒下一个节点的线程),则node节点的线程可以安全挂起,返回true
CANCELLED,则p的线程被取消了,我们会将p之前的连续几个被取消的前驱节点从队列中剔除
等待状态是除了上述两种的其他状态,CAS尝试将前驱节点的等待状态设为SIGNAL【p与node竞争】
take()和offer()都是lock了重入锁,按照synchronized的公平锁,两个方法是互斥
take()方法需要等待1个小时才能返回,offer()需要马上提交一个10秒后运行的任务,此时offer()可以插队获取锁
原理:A执行时,B lock()锁,并休眠;当锁被A释放处于可用状态时,B线程却还处于被唤醒的过程中,此时C线程请求锁,可以优先C得到锁
显示锁可中断,防止死锁,内置锁不可中断,会产生死锁
实现其他特性的锁
对锁更精细的控制
显示锁易忘记 finally 块释放锁,对程序有害
显示锁只能用在代码块,强制更细粒度的加锁;syn可以用在方法上
synchronized 管理锁定和释放时,能标识死锁或者其他异常行为的来源,利于调试
Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多
Condition类对锁进行更精确的控制,指定唤醒、分组唤醒
防止死锁
轮询锁:用tryLock(long timeout, TimeUnit unit)和tryLock() 这两个方法实现,即没有获取到锁,可以使用while循环 隔一段时间再次获取,直到获取到为止
定时锁:指的是在指定时间内没有获取到锁,就取消阻塞并返回获取锁失败;tryLock(long timeout, TimeUnit unit)
可中断锁:lockInterruptibly,防止死锁
区别
如果该锁没有被另一个线程持有,则获取该锁并立即返回,将锁计数设置为 1;对应AQS中的state
如果当前线程已经持有该锁,将锁计数加 1,并立即返回方法---重入锁
如果该锁被另一个线程持有,则禁用当前线程,在获得锁之前,一直休眠,此时锁保持计数设置为 1
tryLock能获得锁就返回true,不能就立即返回false,可以增加时间限制,如果超过该时间段还没获得锁,返回false;tryLock(long timeout,TimeUnit unit),
lock能获得锁就返回true,不能的话一直等待获得锁
lockInterruptibly,中断会抛出异常
锁的Condition类
Condition中的await()方法相当于Object的wait()方法
Condition中的signal()方法相当于Object的notify()方法
Condition中的signalAll()相当于Object的notifyAll()方法
ReentrantLock类可以唤醒指定条件的线程,而object的唤醒是随机的
造成当前线程在接到信号或被中断之前一直处于等待状态 void await()
唤醒一个等待线程 void signal()
唤醒所有等待线程 void signalAll()
造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态 boolean await(long time, TimeUnit unit)
造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态 long awaitNanos(long nanosTimeout)
造成当前线程在接到信号之前一直处于等待状态 void awaitUninterruptibly()
造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态 boolean awaitUntil(Date deadline)
共享锁实现 并发读 ReentrantReadWriteLock、计数器
在AQS队列中,将线程包装为Node.SHARED节点,即标志为共享锁
当头节点获得共享锁后,唤醒下一个共享类型结点的操作
头节点node1调用unparkSuccessor()方法唤醒了Node2,并且调用tryAcquireShared方法检查下一个节点是共享节点
如果是,更改头结点,重复以上步骤,以实现节点自身获取共享锁成功后,唤醒下一个共享类型结点的操作
死锁
资源互斥条件:资源互斥,即某资源仅为一个进程占有
资源不可剥夺条件:进程所获得的资源在未使用完毕之前,只能是主动释放,不能被其他进程强行夺走
保持和请求条件:进程已经保持了一个资源,又提出了新的资源请求,而该资源已被其他进程占有
循环等待条件:进程资源循环等待
如何避免死锁
加锁顺序(线程按照一定的顺序加锁)
按照顺序加锁是一种死锁预防机制,需要事先知道所有会用到的锁
加锁时限(超时则放弃)
获取锁时加上时限,超过时限则放弃请求,并释放锁,等待一段随机的时间再重试
死锁检测与恢复
操作系统中:系统为进程分配资源,不采取任何限制性措施,提供检测和恢复的手段
死锁恢复
撤消进程,剥夺资源
线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁
死锁发生的时候设置随机的优先级;如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。
锁模式包括:
共享锁:(读取)用户可以并发读取数据,但不能获取写锁,直到释放所有读锁。
排他锁(写锁):加上写锁后,其他线程无法加任何锁;写锁可以读和写
更新锁: 防止死锁而设立,转换读锁为写锁之前的准备,仅一个线程可获得更新锁
行锁: 粒度最小,并发性最高
页锁:锁定一页。25个行锁可升级为一个页锁。
表锁:粒度大,并发性低
数据库锁:控制整个数据库操作
Happen-Before原则 八大原则:
定义了线程、锁、volatile变量、对象创建的先后关系
若满足,则保证一个操作执行的结果需要对另一个操作可见
判断数据是否存在竞争、线程是否安全的依据
单线程:在同一个线程中,书写在前面的操作happen-before后面的操作。
锁:解锁先于锁定;同一个锁的unlock操作happen-before此锁的lock操作。
volatile:先写;对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
线程启动:start方法优先;同一个线程的start方法happen-before此线程的其它方法。
线程中断:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
线程终结:线程中的所有操作都happen-before线程的终止检测。
对象创建:先初始化,后finalize;一个对象的初始化完成先于他的finalize方法调用。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:
https://blog.csdn.net/zzpueye/article/details/90047506
锋哥最新SpringCloud分布式电商秒杀课程发布
👇👇👇
👆长按上方微信二维码 2 秒
感谢点赞支持下哈