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

这篇具有很好参考价值的文章主要介绍了【JavaEE初阶】多线程进阶(五)常见锁策略 CAS synchronized优化原理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

常见锁策略

乐观锁 & 悲观锁

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

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

轻量级锁 & 重量级锁

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

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

自旋锁 & 挂起等待锁

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

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

互斥锁 & 读写锁

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

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

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

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

公平锁 & 非公平锁

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

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

公平锁:一号沸羊羊先追,当美羊羊分手后,就由等待队列中,最早来的沸羊羊上位。
【JavaEE初阶】多线程进阶(五)常见锁策略 CAS synchronized优化原理
非公平锁:雨露均沾。
【JavaEE初阶】多线程进阶(五)常见锁策略 CAS synchronized优化原理
操作系统和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操作是原子的。**原子的也就可以在一定程度上回避线程安全问题。
【JavaEE初阶】多线程进阶(五)常见锁策略 CAS synchronized优化原理
小结: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;
	}
}

【JavaEE初阶】多线程进阶(五)常见锁策略 CAS synchronized优化原理
寄存器每一个线程都有自己的一份上下文。
【JavaEE初阶】多线程进阶(五)常见锁策略 CAS synchronized优化原理

  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;
		}
}

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

CAS的典型问题:ABA问题

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

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

Synchronized原理

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

1.锁升级/锁膨胀

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

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

synchronized(locker){
}

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

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

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

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

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

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

锁能升级,不能降级。

2.锁消除

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

3.锁粗化

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

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

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

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

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

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

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

相关面试题:

  1. 什么是偏向锁?

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

  1. synchronized 实现原理 是什么?

以上章节全部内容文章来源地址https://www.toymoban.com/news/detail-435891.html

到了这里,关于【JavaEE初阶】多线程进阶(五)常见锁策略 CAS synchronized优化原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识

    List、Set、HashMap作为Java中常用的集合,需要深入认识其原理和特性。 本篇博客介绍常见的关于Java中线程安全的ConcurrentHashMap集合的面试问题,结合源码分析题目背后的知识点。 关于List的博客文章如下: Java进阶(List)——面试时List常见问题解读 结合源码分析 关于的Set的博

    2024年02月06日
    浏览(44)
  • 【JavaEE】多线程之线程安全(synchronized篇),死锁问题

    线程安全问题 观察线程不安全 线程安全问题的原因  从原子性入手解决线程安全问题 ——synchronized synchronized的使用方法  synchronized的互斥性和可重入性 死锁 死锁的三个典型情况  死锁的四个必要条件  破除死锁 在前面的章节中,我们也了解到多线程为我们的程序带来了

    2024年02月01日
    浏览(44)
  • Java多线程(3)---锁策略、CAS和JUC

    目录 前言 一.锁策略 1.1乐观锁和悲观锁 ⭐ 两者的概念 ⭐实现方法 1.2读写锁  ⭐概念 ⭐实现方法 1.3重量级锁和轻量级锁 1.4自旋锁和挂起等待锁 ⭐概念 ⭐代码实现 1.5公平锁和非公平锁 1.6可重入锁和不可重入锁 二.CAS 2.1为什么需要CAS 2.2CAS是什么 ⭐CAS的介绍 ⭐CAS工作原理

    2024年02月13日
    浏览(38)
  • 【Java多线程进阶】CAS机制

    前言 CAS指的是Compare-And-Swap(比较与交换),它是一种多线程同步的技术,常用于实现无锁算法,从而提高多线程程序的性能和扩展性。本篇文章具体讲解如何使用 CAS 的机制以及 CAS 机制带来的问题。 目录 1. 什么是CAS? 2. CAS的应用 2.1 实现原子类 2.2 实现自旋锁 3. CAS的ABA问

    2024年02月10日
    浏览(31)
  • 【多线程进阶】synchronized 原理

    在前面章节中, 提到了多线程中的锁策略, 那么我们 Java 中的锁 synchronized 背后都采取了哪些锁策略呢? 又是如何进行工作的呢? 本节我们就来谈一谈. 关注收藏, 开始学习吧🧐 在 Java 中, synchronized 具有以下特性(这里以 JDK 1.8 为例): 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲

    2024年02月07日
    浏览(26)
  • 【JavaEE初阶】 线程安全

    线程安全是多线程编程是的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且准确的执行,不会出现数据污染等意外情况。上述是百度百科给出的一个概念解释。换言之,线程安全就是某

    2024年02月08日
    浏览(46)
  • javaee初阶———多线程(三)

    T04BF 👋专栏: 算法|JAVA|MySQL|C语言 🫵 小比特 大梦想 此篇文章与大家分享多线程专题第三篇,关于 线程安全 方面的内容 如果有不足的或者错误的请您指出! 我们在前面说过,线程之间是抢占式执行的,这样产生的随机性,使得程序的执行顺序变得不一致,就会使得程序产生不同的结

    2024年04月16日
    浏览(29)
  • JavaEE初阶:多线程 - 编程

    我们在之前认识了什么是多进程,今天我们来了解线程。 一个线程就是一个 \\\"执行流\\\". 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 \\\"同时\\\" 执行 着多份代码. 引入 进程 这个概念,主要是为了解决并发编程这样的问题。因为cpu进入了多核心的时代,要想进一步

    2024年02月12日
    浏览(28)
  • 多线程(JavaEE初阶系列4)

    目录 前言: 1.单例模式 1.1饿汉模式 1.2懒汉模式 1.3结合线程安全下的单例模式 1.4单例模式总结 2.阻塞式队列 2.1什么是阻塞队列 2.2生产者消费者模型 2.2.1 上下游模块之间进行“解耦合” 2.2.2削峰填谷 2.3阻塞队列的实现 结束语: 在上节中小编主要与大家分享了多线程中遇到

    2024年02月15日
    浏览(41)
  • 【JavaEE初阶】 线程安全的集合类

    原来的集合类, 大部分都不是线程安全的. Vector, Stack, HashTable, 是线程安全的(不建议用), 其他的集合类不是线程安全的. 为什么不建议使用呢? 因为我们在使用的时候,这些类就会自动的加锁,虽然编译器会自动优化为没有锁竞争的线程进行锁消除的优化,但是呢万一编译器没

    2024年02月08日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包