决战圣地玛丽乔亚Day16 --- 算法两道+ 独占锁/共享锁学习
算法部分:
复习一下二叉树的题目:
简单的前中后序遍历:
解二叉树的题目的逻辑:
1.确定入参和返参
2.确定终止条件
3.确认每层的逻辑
例如简单的后续遍历。 后续遍历是左右中。
那么入参和返参根据题目来做。
终止条件是: 遍历下去,遍历到的根节点为null
每层的逻辑是:左右中
代码如下,前中序列类似:
安排周末把二叉树重新过一遍。刷过的题忘了一大半。
今天时间有限,先继续往下刷,刷回溯算法。
回溯算法的本质是穷举,效率一般,如果想要提高效率,就需要进行剪纸操作。
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
1.返回值以及参数
void backtracking(val1,val2.....)
2.回溯函数终止条件
if (终止条件) {
存放结果;
return;
}
3.回溯搜索遍历过程
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
for循环相当于在横向遍历,backtracking相当于调用自己,挖掘深度的纵向遍历。
回溯算法都可以抽象成N叉树的问题。把回溯看做树的遍历
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
表锁:共享锁、排它锁、意向锁、MDL锁
行锁:共享锁、排它锁
区间锁:间隙锁、临键锁
共享锁和排它锁:
共享锁:
共享锁可以被多个线程共同持有。获取共享锁只能去读数据,不能写数据。
排它锁:
排它锁也叫互斥锁、独占锁。只能被一个线程独享,独享期间可以读写数据。synchronized和JUC的lock实现类用的是独占锁。
从ReentrantReadWriteLock的源代码来了解一下共享锁和互斥锁:
我们可以看出来,这个可冲入读写锁的类,有读锁和写锁的分别实现,通过传入是否公平fair来实现公平版本和非公平版本的锁机制。
接下来看一下读锁和写锁分别的实现过程。
读写锁都是依靠sync来实现,sync是AQS的一个子类。


1.首先我们通过getState取到锁的个数。然后通过exclusiveCount方法获取到写锁的个数(低16位)。
2.判断是否已经有线程持有写锁? 写锁数目为0(由于c!=0,但是w=0,所以此时存在读锁)或者持有锁的线程不是当前线程(写锁可能被其他线程获取但不是当前线程) --->返回false
3.如果写锁个数超出最大值,抛出异常。
排除以上的情况,写锁重入成功
c=0的情况(没有任何锁):
这里牵扯到公平锁和非公平锁,需要判断一下是否要阻塞:
非公平锁直接返回false
公平锁 :需要判断当前同步队列中是否有阻塞的线程,如果有,那么当前线程也需要去同步队列阻塞
如果不需要阻塞,就通过CAS去修改state
属性值,然后将当前线程设置为独占线程
写锁的释放:
由于可重入,释放一次写锁后需要判断是否需要再次释放。如果释放之后扣完state值为0,说明没有写锁被占用,设置独占线程为null,返回true。在release()
中就会去唤醒同步队列中阻塞的线程
读锁的获取(共享锁):
读锁的获取调用的是AQS
的acquireShared()
,ReentrantReadWriteLock.Sync
中实现了tryAcquireShared()
方法
excluscie:独占 share:共享
1.如果当前写锁个数不为0,且获取写锁的线程不是当前线程,那么不能获取读锁,并加入读写锁的同步队列。
2.获取当前读锁的个数,判断读锁是否需要阻塞:
1)如果不需要阻塞读锁,并且读锁数量合法,并且视图通过CAS去修改获取读锁次数成功,那么就把当前线程读锁次数+1.
读锁通过firstReader
和firstReaderHoldCount
的属性值来记录第一个获取读锁的线程。
如果当前线程就是第一个获取锁的线程,且是重入获取读锁,直接将firstReaderHoldCount
+1
如果不是第一个获取,那么就修改cachedHoldCounter的值,这个cachedHoldCounter用来缓存最近一个获取读锁的线程.
如果缓存cachedHoldCounter
中的线程不是当前线程,则从readHolds
中获取当前线程获取读锁的情况,readHolds.get()
返回的是一个HoldCounter
对象,最后将HoldCounter
对象中的count
属性值+1。
3.如果获取读锁失败:最后调用fullTryAcquireShared()
去自旋尝试获取读锁,该方法的源码基本与tryAcquireShared()
是一样的,就不再赘述
读锁的释放:
读锁释放调用的是AQS
的releaseShared()
,核心逻辑在tryReleaseShared()
中
顺序是:第一个获取读锁的线程释放锁,然后其他线程释放
如果该线程释放读锁后读锁个数为0,应该同步清除ThreadLocal中的HoldCounter值。
读锁的释放是可以并发进行的,所以通过CAS来修改state属性值
总结:
共享锁和独占锁互斥,但是共享锁和共享锁是不互斥的。
多个事务同时更新一行数据,都会加锁,然后排队等待,必须一个事务执行完毕提交了,释放锁,才能唤醒下一个事务继续执行,这个时候加的是独占锁。
当有人在更新数据时,其他事务可以读,因为读写两个操作不互斥MVCC机制,但是读写锁是互斥的。
------------恢复内容开始------------
算法部分:
复习一下二叉树的题目:
简单的前中后序遍历:
解二叉树的题目的逻辑:
1.确定入参和返参
2.确定终止条件
3.确认每层的逻辑
例如简单的后续遍历。 后续遍历是左右中。
那么入参和返参根据题目来做。
终止条件是: 遍历下去,遍历到的根节点为null
每层的逻辑是:左右中
代码如下,前中序列类似:
安排周末把二叉树重新过一遍。刷过的题忘了一大半。
今天时间有限,先继续往下刷,刷回溯算法。
回溯算法的本质是穷举,效率一般,如果想要提高效率,就需要进行剪纸操作。
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
1.返回值以及参数
void backtracking(val1,val2.....)
2.回溯函数终止条件
if (终止条件) {
存放结果;
return;
}
3.回溯搜索遍历过程
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
for循环相当于在横向遍历,backtracking相当于调用自己,挖掘深度的纵向遍历。
回溯算法都可以抽象成N叉树的问题。把回溯看做树的遍历
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
表锁:共享锁、排它锁、意向锁、MDL锁
行锁:共享锁、排它锁
区间锁:间隙锁、临键锁
共享锁和排它锁:
共享锁:
共享锁可以被多个线程共同持有。获取共享锁只能去读数据,不能写数据。
排它锁:
排它锁也叫互斥锁、独占锁。只能被一个线程独享,独享期间可以读写数据。synchronized和JUC的lock实现类用的是独占锁。
从ReentrantReadWriteLock的源代码来了解一下共享锁和互斥锁:
我们可以看出来,这个可冲入读写锁的类,有读锁和写锁的分别实现,通过传入是否公平fair来实现公平版本和非公平版本的锁机制。
接下来看一下读锁和写锁分别的实现过程。
读写锁都是依靠sync来实现,sync是AQS的一个子类。


1.首先我们通过getState取到锁的个数。然后通过exclusiveCount方法获取到写锁的个数(低16位)。
2.判断是否已经有线程持有写锁? 写锁数目为0(由于c!=0,但是w=0,所以此时存在读锁)或者持有锁的线程不是当前线程(写锁可能被其他线程获取但不是当前线程) --->返回false
3.如果写锁个数超出最大值,抛出异常。
排除以上的情况,写锁重入成功
c=0的情况(没有任何锁):
这里牵扯到公平锁和非公平锁,需要判断一下是否要阻塞:
非公平锁直接返回false
公平锁 :需要判断当前同步队列中是否有阻塞的线程,如果有,那么当前线程也需要去同步队列阻塞
如果不需要阻塞,就通过CAS去修改state
属性值,然后将当前线程设置为独占线程
写锁的释放:
由于可重入,释放一次写锁后需要判断是否需要再次释放。如果释放之后扣完state值为0,说明没有写锁被占用,设置独占线程为null,返回true。在release()
中就会去唤醒同步队列中阻塞的线程
读锁的获取(共享锁):
读锁的获取调用的是AQS
的acquireShared()
,ReentrantReadWriteLock.Sync
中实现了tryAcquireShared()
方法
excluscie:独占 share:共享
1.如果当前写锁个数不为0,且获取写锁的线程不是当前线程,那么不能获取读锁,并加入读写锁的同步队列。
2.获取当前读锁的个数,判断读锁是否需要阻塞:
1)如果不需要阻塞读锁,并且读锁数量合法,并且视图通过CAS去修改获取读锁次数成功,那么就把当前线程读锁次数+1.
读锁通过firstReader
和firstReaderHoldCount
的属性值来记录第一个获取读锁的线程。
如果当前线程就是第一个获取锁的线程,且是重入获取读锁,直接将firstReaderHoldCount
+1
如果不是第一个获取,那么就修改cachedHoldCounter的值,这个cachedHoldCounter用来缓存最近一个获取读锁的线程.
如果缓存cachedHoldCounter
中的线程不是当前线程,则从readHolds
中获取当前线程获取读锁的情况,readHolds.get()
返回的是一个HoldCounter
对象,最后将HoldCounter
对象中的count
属性值+1。
3.如果获取读锁失败:最后调用fullTryAcquireShared()
去自旋尝试获取读锁,该方法的源码基本与tryAcquireShared()
是一样的,就不再赘述
读锁的释放:
读锁释放调用的是AQS
的releaseShared()
,核心逻辑在tryReleaseShared()
中
顺序是:第一个获取读锁的线程释放锁,然后其他线程释放
如果该线程释放读锁后读锁个数为0,应该同步清除ThreadLocal中的HoldCounter值。
读锁的释放是可以并发进行的,所以通过CAS来修改state属性值
总结:
共享锁和独占锁互斥,但是共享锁和共享锁是不互斥的。
多个事务同时更新一行数据,都会加锁,然后排队等待,必须一个事务执行完毕提交了,释放锁,才能唤醒下一个事务继续执行,这个时候加的是独占锁。
当有人在更新数据时,其他事务可以读,因为读写两个操作不互斥MVCC机制,但是读写锁是互斥的。
在普遍情况下,都是利用MVCC进行一致性读,不加锁的形式。
共享锁:
select lock in share mode
独占锁:
select for update
误区:
1.加独占锁和共享锁的大前提是在事务中。
2.共享锁和排他锁的添加,不会影响正常的select去读,影响的是加锁。
像超卖问题等安全性高,并发会出问题的,可以用独占锁。
在mysql,如果不去修改隔离级别,是基本用不到共享锁的。可能解决不可重复读的场景,共享锁加完后,其他线程对数据update等写操作是无法进行的。
但是实际上没有必要退后到更低的隔离级别去加共享锁解决这种问题,默认的隔离级别MVCC已经帮我们做到了
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!