前言
在实际开发中,我们要根据不同的情况来处理锁冲突,所策略就是针对冲突来处理的方法~常见的锁策略有 乐观锁 悲观锁 轻量级锁 重量级锁 自旋锁 挂起等待锁 公平锁 非公平锁 普通互斥锁 读写锁 可重入锁 不可重入锁 下面将一一讲述他们的区别和联系.
一.乐观锁与悲观锁.
①.乐观锁:在加锁之前认为出现锁冲突的概率不大.因此在进行加锁的时候就不会做太多工作,加锁的过程中做的事比较少,加锁的速度比较快.
②.悲观锁:在加锁之前认为出现的概率很大,因此在进行加锁的时候,会做很多工作.在每次拿到数据的时候都会加锁,这个加锁的过程会比较缓慢,但不容易出现问题.
举个例子:
在上课时老师说一会要找人讲一个题:一些同学就认为班里那么多人应该找不到我回答问题,就在那啥也不干(此时认为是乐观锁)…有的同学认为老师叫道我的概率比较大,所以我要赶紧看看这个题怎么写的,不会了赶紧问问(此时就认为是悲观锁)
二.轻量级锁与重量级锁.
①.轻量级锁:加锁的开销少,加锁的速度快====>一般也认为是乐观锁
②.重量级锁: 加锁的开销比较大,加锁的速度慢====>一般也认为是悲观锁.
举个例子:
到一个陌生的地方去餐馆吃饭,点饭的时候不知道哪个好吃,吃过饭之后如果认为这顿饭钱花的比较值,饭够好吃钱花的少(此时就可以认为是轻量级锁) ,如果认为很亏,饭不够吃,钱还化的多(此时就可以认为是重量级锁)
注:乐观锁和悲观锁是在加锁之前,对未发生的事情进行评估.轻量级锁和重量级锁是加锁之后,对整个加锁过程进行的评估…但从整体的角度来看,两者描述的是同一种事务.
三.自旋锁与挂起等待锁.
①.自旋锁:是一种轻量级锁(乐观锁) 的典型实现…相当于在加锁的时候搭配一个while循环,如果加锁成功,while循环自然结束;如果加锁没有成功,就继续进入循环,再次加锁,直到加上锁为止.这个反复快速加锁的过程,就称为自旋 . 此时就能解释为什么说自旋锁,是轻量级锁/乐观锁的一种典型实现(使用自旋的前提是预估锁冲突的概率不大,如果线程比较多,此时的自旋相当于白白浪费cpu的资源)
②.挂起等待锁:是一种重量级锁(悲观锁的典型实现)…如果在加锁的过程中遇到锁冲突,会放弃竞争,转而进入阻塞状态,等待所资源被释放,调度到时再去获得锁.这种情况适用于锁竞争比较激烈的情况.
举个例子:
他呢跟他女神表白,但是被女神发了好人卡(加锁失败),但是失败之后怎么办呢?自旋锁:每天锲而不舍,向女神发早安晚安,终于有一天,女神和她男朋友分手了,这是机会就来了;第一时间就有机会枪到锁,然后加锁。此时就可以认为是自旋锁.
一旦被拒绝,那么就不理女神了,潜心敲代码,学习;终于有一天,女神分手了,想起了他来,主动要求加锁;此时就可以认为是挂起等待锁.
四.互斥锁与读写锁
①.互斥锁:Java中的互斥锁策略是一种用于控制多线程访问共享资源的机制,以确保在同一时间只有一个线程能够访问该资源。互斥锁可以防止多个线程同时修改共享数据,从而避免数据不一致和竞争条件等问题。
②.读写锁:把加锁分成了两种情况 加读锁 加写锁 此时,读锁和读锁之间不会产生锁冲突(不会阻塞). 写锁和写锁之间会产生锁冲突(会阻塞) 读锁和写锁之间会出现锁冲突(会阻塞).进一步来说就是~~读的时候,不能写,只能读;写的时候,不能读,也不能写…也可以这样说,一个线程加读锁的时候,另一个线程只能读不能写;;;;一个现场加写锁的时候,另一个线程不能读也不能写
为什么要引入读写锁?
如果两个线程都是读,本身就是就是线程安全的,不需要进行互斥~~如果使用synchronized这种方式加锁,两个线程读也会产生阻塞,这样就会降低性能.
五.公平锁与非公平锁
此处的公平是指是否遵循先来后到
公平锁:公平锁是一种设计思想,多线程在进行数据请求的过程中,先去队列中申请锁,按照FIFO先进先出的原则拿到线程,然后占有锁。要想实现公平锁,就需要引入额外的数据结构(引入队列,记录每个线程先后顺序),才能实现公平锁~~使用公平锁,天然就可以避免线程饿死的问题.
非公平锁:也是一种设计思想。线程尝试获取锁,如果获取不到,这时候采用公平锁的方式进行,与此同时,多个线程获取锁的顺序有一定的随机性,并非按照先到先得的方式进行。
举个例子:
非公平锁:
六.可重入锁与不可重入锁
可重入锁:可重⼊锁的字⾯意思是“可以重新进⼊的锁”,即允许同⼀个线程多次获取同⼀把锁。
不可重入锁:不可重⼊锁的字⾯意思是“不可以重新进⼊的锁”,即不允许同⼀个线程多次获取同⼀把锁。
如何理解把自己锁死
按照之前对于锁的设定, 第⼆次加锁的时候, 就会阻塞等待. 直到第⼀次的锁被释放, 才能获取到第⼆个锁. 但是释放第⼀个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想⼲了, 也就⽆法进⾏解锁操作. 这时候就会 死锁.
举个例子:
七.Java中synchronized属于哪种情况?
synchronized具有自适应能力
synchronized在某些情况下是"乐观锁/轻量级锁/自旋锁" 在某些情况下是"悲观锁/重量级锁/等待挂起锁" synchronized会自动评估当前锁冲突的激烈程度 如果当前锁冲突的激烈程度不大就处于"乐观锁/轻量级锁/自旋锁"~~如果当前锁冲突的激烈程度很大,就处于"悲观锁/重量级锁/等待挂起锁"…
对于synchronized来说:1.乐观锁/悲观锁自适应 2.轻量级锁/重量级锁自适应 3.自旋锁/挂起等待锁自适应 4.不是读写锁. 5.非公平锁 6.可重入锁.
八.synchronized加锁过程中的优化
优化一:锁升级
synchronized的加锁过程,当线程执行到加锁这一步的时候会经历以下三个阶段:
阶段一.偏向锁阶段.
偏向锁阶段的核心思想就是"懒汉模式"(能不加锁,就不加锁);能晚加锁,就晚加锁.
所谓的偏向锁,并非是真正的加锁,而是做了一个非常轻量的标记
换句话说就是"搞暧昧".假设你是一个渣女,有个老哥在追你.此时,你还想看看有没有更合适的(鸡肋–>食之无味,弃之可惜),这时候最好的方法就是搞暧昧.要是没人来竞争(没有别的妹子追老哥),你就可以继续寻找更合适的(此时就相当于没有给老哥加锁).要是有别的妹子来找老哥了,此时我能够第一时间给老哥确定关系(相当于有别的线程来和我竞争这个锁了,此时就会进入加锁的第二个阶段 ----->轻量级锁阶段(真正的加锁了))
阶段二:轻量级锁阶段.
此处的轻量级锁就是通过自旋锁来实现的
与此同时,如果锁的竞争更加激励,加锁就会进入第三个阶段---->重量级锁阶段.
阶段三.重量级锁阶段.
在这个阶段,如果线程拿不到锁就会进入阻塞等待,而不会白白浪费cpu的资源了
优化二:锁消除.
锁消除是编译器优化的一种方式.编译器编译代码的时候,如果发现这个代码,不需要加锁,就会自动把锁给干掉~~;锁消除是针对一眼看上去就完全不涉及线程安全的代码才把锁消除掉;偏向锁,运行起来才知道有没有锁冲突.
优化三:锁粗化
会把多个细粒度的锁,合并成一个粗粒度的锁~~
synchronized{}大括号里包含的代码越少,就认为锁的粒度越细,包含的代码越多,就认为锁的粒度越粗.
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
小结
synchronized背后涉及到了很多优化手段:
①.锁升级:偏向锁–>轻量级锁–>重量级锁
②.锁消除:自动干掉不必要的锁.
③.锁粗化:把多个细粒度的锁合并为一个新粒度的锁,减少锁竞争的开销.
九.与锁策略相关的面试题
- 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
悲观锁认为多个线程访问同⼀个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁.
乐观锁认为多个线程访问同⼀个共享变量冲突的概率不大. 并不会真的加锁, ⽽是直接尝试访问数据.
在访问的同时识别当前的数据是否出现访问冲突.
悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.
乐观锁的实现可以引⼊⼀个版本号. 借助版本号识别出当前的数据访问是否冲突. - 介绍下读写锁?
读写锁就是把读操作和写操作分别进⾏加锁.
读锁和读锁之间不互斥.
写锁和写锁之间互斥.
写锁和读锁之间互斥.
读写锁最主要⽤在 “频繁读, 不频繁写” 的场景中. - 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
如果获取锁失败, ⽴即再尝试获取锁, ⽆限循环, 直到获取到锁为止. 第⼀次获取锁失败, 第⼆次的尝试
会在极短的时间内到来. ⼀旦锁被其他线程释放, 就能第⼀时间获取到锁.
相比于挂起等待锁,
优点: 没有放弃 CPU 资源, ⼀旦锁被释放就能第⼀时间获取到锁, 更⾼效. 在锁持有时间比较短的场景下非常有用.
缺点: 如果锁的持有时间较⻓, 就会浪费 CPU 资源.