【JavaEE初阶】多线程进阶(五)常见锁策略 CAS synchronized优化原理

在这里插入图片描述

常见锁策略

乐观锁 & 悲观锁

乐观锁:预测锁竞争不是很激烈。
悲观锁:预测锁竞争会很激烈。

以上定义并不是绝对的,具体看预测锁竞争激烈程度的结论。

轻量级锁 & 重量级锁

轻量级锁加锁解锁开销比较小,效率更高。
重量级锁加锁解锁开销比较大,效率更低。

多数情况下,乐观锁也是一个轻量级锁。
多数情况下,悲观锁也是一个重量级锁。

自旋锁 & 挂起等待锁

自旋锁:是一种典型的轻量级锁。
挂起等待锁:是一种典型的重量级锁。

举个🌰:
我给男神表白了,然后喜提好人卡一张o(╥﹏╥)o,男神告诉我:你是个好人,但是我有对象了。接下来我可以有两种操作。
自旋锁:每天给男神发早安午安晚安。一旦男神分手,我就可以知道。(一旦锁被释放,就能第一时间感知到,从而有机会获得锁。)自旋锁,占用了大量得系统资源。
挂起等待锁:我说我愿意等,一个人默默的等男神很久。这时候,如果男神分手了,有可能想起我,他分手了。但是也可能(大概率),当男神想起我的时候,已经过了很久很久了。(当真的被唤醒,中间已经是沧海桑田了。)省下了CPU资源,可以做别的事情。

互斥锁 & 读写锁

互斥锁:一个线程加锁了,另一个线程尝试加锁时,就会阻塞等待。(例如synchronized,提供了加锁和解锁的操作。)
读写锁:提供了三种操作

  1. 针对读加锁
  2. 针对写加锁
  3. 解锁

基于一个事实:多线程对同一个变量并发读,这个时候没有线程安全问题,不需要加锁控制。(读写锁就是针对这种情况锁采取的特殊处理。)

读锁和读锁之间没有互斥。
写锁和写锁之间存在互斥。
写锁和读锁之间存在互斥。
(假如当前有一组线程都去读(加读锁),这些线程之间没有锁竞争,也没有线程安全问题。)

公平锁 & 非公平锁

此处将公平定义为先来后道

举个🌰:
在这里插入图片描述

公平锁:一号沸羊羊先追,当美羊羊分手后,就由等待队列中,最早来的沸羊羊上位。
在这里插入图片描述
非公平锁:雨露均沾。
在这里插入图片描述
操作系统和synchronized原生都是“非公平锁”
操作系统这里的针对加锁的控制,本身就以来于线程调度的顺序的。这个调度顺序也是随机的,不会考虑到这个线程等待锁多久。

可重入锁 & 不可重入锁

不可重入锁:一个线程针对一把锁,连续加锁两次,出现死锁。
可重入锁:一个线程针对一把锁,连续加锁多次都不会出现死锁。

synchronized对应以上的锁策略

  1. synchronized既是一个悲观锁,也是一个乐观锁。
    synchronized默认是乐观锁,但是如果发现当前锁竞争比较激烈,就会变成悲观锁。
  2. synchronized既是轻量级锁,也是一个重量级锁。
    synchronized默认是轻量级锁,但是如果发现当前锁竞争比较激烈,就会转化成重量级锁。
  3. synchronized这里的轻量级锁,是基于自旋锁的方式实现的。
    synchronized这里的重量级锁,是基于挂起等待锁的方式实现的。
  4. synchronized不是读写锁
  5. synchronized是非公平锁。
  6. synchronized是可重入锁。

总结:上述谈到的六种锁策略,可以理解为“锁的形容词”。

锁策略中的面试题:

  1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?

悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁。
乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数
据. 在访问的同时识别当前的数据是否出现访问冲突。
悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就
等待.
乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突.

  1. 介绍下读写锁?

读写锁就是把读操作和写操作分别进行加锁.
读锁和读锁之间不互斥.
写锁和写锁之间互斥.
写锁和读锁之间互斥.
读写锁最主要用在 “频繁读, 不频繁写” 的场景中

  1. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝
试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.
相比于挂起等待锁,
优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场
景下非常有用.
缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源.

  1. synchronized 是可重入锁么?

是可重入锁.
可重入锁指的就是连续两次加锁不会导致死锁.
实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁
的线程就是持有锁的线程, 则直接计数自增.

CAS

CAS的介绍

CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

  1. 比较 A 与 V 是否相等。(比较)
  2. 如果比较相等,将 B 写入 V。(交换)
  3. 返回操作是否成功。

此处最特别的地方,上述这个CAS的过程,并非是通过一段代码实现的。而是通过一条CPU指令完成的。也就是说**CAS操作是原子的。**原子的也就可以在一定程度上回避线程安全问题。
在这里插入图片描述
小结:CAS可以理解为CPU给我们提供的一个特殊指令,通过这个指令,就可以一定程度的处理线程安全问题。
CAS的伪代码(辅助理解,并不是真的代码):

boolean CAS(V, A, B) {
	if (V == A) {
		V = B;
		return true;
	}
	return false;
}

CAS如何实现

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:

  • java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
  • unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
  • Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子 性。

CAS的应用场景

  1. 实现原子类
    标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.
    典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadDemo28 {
    public static void main(String[] args) {
        AtomicInteger count = new AtomicInteger(0);
        System.out.println(count.getAndIncrement());
        System.out.println(count.getAndDecrement());
    }
}

伪代码实现:

class AtomicInteger {
	private int value;
	public int getAndIncrement() {
		int oldValue = value;
		while ( CAS(value, oldValue, oldValue+1) != true) {
		oldValue = value;
	}
	return oldValue;
	}
}

在这里插入图片描述
寄存器每一个线程都有自己的一份上下文。
在这里插入图片描述

  1. 实现自旋锁
    基于CAS 实现更灵活的锁, 获取到更多的控制权.
    自旋锁伪代码:
public class SpinLock {
	private Thread owner = null;
	public void lock(){
	// 通过 CAS 看当前锁是否被某个线程持有.
	// 如果这个锁已经被别的线程持有, 那么就自旋等待.
	// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
		while(!CAS(this.owner, null, Thread.currentThread())){
		}
	}
	public void unlock (){
		this.owner = null;
		}
}

在这里插入图片描述

CAS的典型问题:ABA问题

CAS在运行中的核心,是检查value和oldValue是否一致,如果一致,就视为value中途没有被修改过,所以进行下一次交换操作。但是在判断value和oldValue是否一致时,这里的值可能改过,但是还原回来了。
也就是:把value的值设为A的话
CAS判定value为A,此时可能确实是A。但是也可能本来是A,被改成了B,但是又还原为A。
ABA这个情况,大部分情况下,不会对代码/逻辑产生影响的。但是不排除极端情况。

举个🌰:
当前滑稽老铁要去ATM上取钱给老婆买情人节礼物:假设滑稽的账户余额1000元,滑稽准备取500元。当按下取款这一瞬间,机器卡了,滑稽就多按了几下,可能就会产生bug,可能就会产生重复扣款的操作。此时可以考虑使用CAS的方式来扣款。
在这里插入图片描述
此时正确扣款。
但是如果当t2线程在执行CAS之前,有人给滑稽老铁转账500,导致之前扣除的500又变为了1000。此时CAS条件满足,执行扣款操作,导致扣款成功。这就出现了bug。在这里插入图片描述
针对当前问题,采取的方案,就是加入一个版本号。假设初识版本号为1,每次修改版本号都+1.然后进行CAS的时候,不是以金额为准,而是以版本号为准。
在这里插入图片描述

Synchronized原理

两个对象,针对同一个对象加锁,就会产生阻塞等待。synchronized内部还有一些优化机制,存在的目的就是为了是synchronized锁更加高效。

1.锁升级/锁膨胀

  1. 无锁
  2. 偏向锁
  3. 轻量级锁
  4. 重量级锁

在这里插入图片描述

synchronized(locker){
}

以上代码块就可以经历前面说的几个阶段。
进行加锁的时候,首先会进入到偏向锁的状态。偏向锁并不是真正加锁,只是标记一下。有需要再加锁。
举个🌰:
有一只老虎,他的捕食能力很强。他捕到了很多猎物,但是他一次性吃不完。所以就吃一部分,留一部分。但是留着的一部分有别的动物想要来抢。所以留下来的部分他要看着(标记),当别的动物来抢的时候,老虎就立即扑上去保护食物。对留下来的食物进行加锁。

上述例子,就是偏向锁的过程。
synchronized的时候,并不是真正加锁,先偏向锁状态,做个标记。(这个过程是非常轻量的)如果整个使用锁的过程中,都没有出现锁竞争,在synchronized执行完之后,取消偏向锁即可。
但是,如果使用过程中,另一个线程也尝试加锁,在它加锁之前,迅速的把偏向锁升级成真正的加锁状态。另一个线程也就只能阻塞等待。

当synchronized发生锁竞争的时候,就会从偏向锁升级成轻量级锁。此时,synchronized相当于是通过自旋的方式,来进行加锁的。
要是别人很快就释放锁,自旋是划算的。但是如果迟迟拿不到锁,就不划算。synchronized自旋不是无休止的自旋,自旋到一定程度之后,就会再次升级成为重量级锁。(挂起等待锁)

重量级锁(挂起等待锁):基于操作系统原生的API来进行加锁。Linux原生提供了mutex一组API,操作系统内核提供的加锁功能,这个锁会影响到线程的调度。

此时,如果线程进行了重量级锁的加锁,并且发生锁竞争,此时线程就会被放到阻塞队列中。暂时不参与CPU调度了。直到锁被释放,这个线程才可能会被调用到。

值得注意的是:一旦当前线程被切出CPU,就比较低效。

锁能升级,不能降级。

2.锁消除

编译器智能的判断,看当前代码是否真的需要加锁。如果这个场景不需要加锁,但是程序员加了,就自动将锁去掉了。
例如:StringBuffer 是线程安全的,关键方法中都带有synchronized。但是如果在单线程中使用StringBuffer,synchronized加锁操作是没有意义的。所以就会将锁优化掉。

3.锁粗化

锁的粒度:synchronized包含的代码越多,粒度就越粗,包含的代码越少,粒度就越细。

通常情况下,认为锁的粒度细一点比较好。加锁的部分的代码,并不能并发执行的。锁粒度越细,能并发的代码就越多。反之则越少。

但是有些情况下,锁的粒度反而粗一点更好。

比如:两次加锁解锁之间,间隙非常小,此时,就用一把大锁来解决。

在这里插入图片描述

举个🌰: 麻麻👩要我去买菜菜。要包玉米猪肉饺子。
我买了猪肉,给麻麻打电话汇报买了猪肉。
我买了玉米,给麻麻打电话汇报买了玉米。
我买了饺子皮,给麻麻打电话汇报买了饺子皮。
当我的第三个电话汇报完,就挨骂了。理由是一根筋,为什么不全部买完再汇报?
当我第二次要我买菜菜的时候,我就一次买完给麻麻汇报。

显然第二次的方法更为高效。

相关面试题:

  1. 什么是偏向锁?

偏向锁不是真的加锁, 而只是在锁的对象头中记录一个标记(记录该锁所属的线程). 如果没有其他线程参与竞争锁, 那么就不会真正执行加锁操作, 从而降低程序开销. 一旦真的涉及到其他的线程竞
争, 再取消偏向锁状态, 进入轻量级锁状态.

  1. synchronized 实现原理 是什么?

以上章节全部内容

  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xxxflower.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值