🎇个人主页:Ice_Sugar_7
🎇所属专栏:JavaEE
🎇欢迎点赞收藏加关注哦!
🍉简介
加锁的过程中可能会出现冲突,这就会涉及到不同的处理方式,这些方式就称为锁策略
🍌乐观锁&悲观锁
这是锁的两种不同实现方式
对于乐观锁,它在加锁前预估出现锁冲突的概率不大,因此在加锁时不会做太多的工作。由于加锁过程做的事比较少,所以加锁速度可能会更快。当然,快就更容易引入一些其他的问题,比如消耗更多的 cpu 资源
而对于悲观锁,它在加锁之前预估出现冲突的概率比较大,在加锁时就会做更多的工作,因此加锁的速度可能更慢,但是整个过程就不容易出现其他问题
🍌轻量级锁&重量级锁
轻量级锁加锁的开销更小,加锁速度更快
重量级锁加锁的开销更大,加锁速度更慢
这两种锁和乐观锁、悲观锁只是所站角度不同,称呼才不一样。轻量重量是站在结果的角度,是对加锁结果的评价;而悲观乐观则是站在预测的角度,在加锁之前,对锁冲突的概率进行预估
本质上说的是同一件事,所以我们一般认为轻量级锁就是乐观锁,重量级锁就是悲观锁
🍌自旋锁&挂起等待锁
自旋锁是轻量级锁的一种典型实现
就是在加锁时搭配一个 while 循环
。如果加锁成功,循环自然就会结束;反之就进行下一次循环,再次尝试获取锁。如果加锁一直没有成功,就会反复循环,而且循环速度很快,这个过程就称为“自旋”。这样,一旦其他线程释放锁,就能第一时间拿到锁
自旋锁也是乐观锁,使用自旋的前提就是预期锁冲突的概率不大,因为如果有多个线程参与竞争,那么自旋就很可能拿不到锁,就会白白浪费 cpu 的资源
而挂起等待锁是重量级锁的一种典型实现,因为在挂起等待的时候需要内核调度器
介入,所以这一块要完成的操作就比较多,获取到锁要花费的时间也就更多一些
🍌普通互斥锁&读写锁
普通互斥锁就类似 synchronized,操作只涉及到加锁和解锁
使用 synchronized 有一个问题,当两个线程都进行读操作也会产生阻塞,这就导致性能会有一定的损失
所以引入了读写锁,它对加锁操作进行了细分,把加锁分成两种情况:
①加读锁:一个线程加读锁时,另一个线程只能读不能写
②加写锁:一个线程加写锁时,另一个线程既不能读,也不能写
引入读写锁之后,就可以省去并发读产生的锁冲突的开销,显著提升性能
注意:
- 读锁和读锁之间不会出现锁冲突(因为都只是读操作)
- 写锁和写锁之间、读锁和写锁之间都会出现锁冲突(就是会阻塞)
🍌公平锁&非公平锁
这里的“公平”,是只要遵循“先来后到”
这一规则,就叫“公平”,这和我们常识上的“公平”不是一回事
系统原生的锁属于非公平锁
,因为系统是随机调度线程的;synchronized 也是非公平的
想要实现公平锁,就需要引入额外的数据结构,比如引入队列记录每个线程的先后顺序
🍌synchronized 内部优化
Java 的 synchronized 具有自适应能力
。也就是说它在某些情况下是乐观锁,某些情况下是悲观锁。至于是哪种锁,这取决于当前锁冲突的激烈程度
,它内部会自动评估激烈程度。如果锁冲突激烈程度不大,那就是乐观锁;反之则是悲观锁
下面来了解一下 synchronized 的加锁过程,尤其是“自适应”是怎么回事
🥝锁升级
线程执行到 synchronized 时,如果这个对象当前处于未加锁的状态,那么就会经历以下的过程
-
偏向锁阶段
这个阶段的加锁思想就是非必要不加锁,能不加锁就不加锁,能晚一点加锁,就尽可能晚一点”(这有点像懒汉模式)
所谓的偏向锁,并不是真的加锁了,而是只做了一个非常轻量的标记。如果全程都没有其他线程来竞争锁,那就会完全忽略加锁的过程,所以在没有竞争的情况下,偏向锁的效率比加锁高很多
而一旦有其他线程来竞争这个锁,那这个线程就能在另一个线程加锁之前捷足先登,获取到锁,这样就会从偏向锁升级到轻量级锁 -
轻量级锁阶段
一旦进入这个阶段,那就是真的加锁了,这个阶段是通过自旋锁
的方式实现的。它的优势在于:其他线程释放锁之后,当前线程就可以第一时间拿到锁;不过劣势也很明显,就是比较消耗 cpu
而与此同时,synchronized 内部也会当前这个锁对象上有多少个线程在参与竞争。如果发现竞争的线程比较多,那就会进一步升级为重量级锁(升级的意义在于降低 cpu 的消耗
,因为对于自旋锁来说,如果同一个锁的竞争者很多的话,那大量的线程都在自旋,整体 cpu 的消耗就很大) -
重量级锁阶段
在这个阶段,拿不到锁的线程不会继续自旋,而是进入阻塞等待的状态,这样就可以让出 cpu,使 cpu 占用率不会太高。等到当前线程释放锁时,系统会随机唤醒一个线程来获取锁
注意:在当前 jvm 版本中,锁只能升级,而不能降级,就是说不能从轻量级锁降为偏向锁,不过可能未来某个版本就可以降级了
🥝锁消除
这也是 synchronized 内置的优化策略。编译器编译代码时,如果发现这个代码不需要加锁
,就会自动把锁给干掉
举些例子,比如只有一个线程,然后在这个线程里面加锁,单线程的情况下没必要加锁,就优化掉
再比如,加锁代码中没有涉及到成员变量的修改,或者只是一些局部变量,这也无需加锁
不过锁消除还是比较保守
的,只有当这个代码一眼看上去就知道完全不涉及线程安全问题时,才会把锁消除掉。而像其他一些比较复杂、模棱两可的情况,编译器不知道是否需要加锁,就不会去消除了
🥝锁粗化
synchronized {} 大括号里面包含的代码越少,就认为锁的粒度越细;反之则认为锁的粒度越粗
通常情况下,我们会倾向于让锁的粒度细一些,这样更有利于多个线程并发执行,但有时候是希望锁的粒度粗一点,因为每次加锁都可能阻塞,把多个细粒度的锁合并为一个粗粒度的锁,这样可以降低阻塞的可能性,从而提高效率