NipGeihou's blog NipGeihou's blog
  • Java

    • 开发规范
    • 进阶笔记
    • 微服务
    • 快速开始
    • 设计模式
  • 其他

    • Golang
    • Python
    • Drat
  • Redis
  • MongoDB
  • 数据结构与算法
  • 计算机网络
  • 应用

    • Grafana
    • Prometheus
  • 容器与编排

    • KubeSphere
    • Kubernetes
    • Docker Compose
    • Docker
  • 组网

    • TailScale
    • WireGuard
  • 密码生成器
  • 英文单词生成器
🍳烹饪
🧑‍💻关于
  • 分类
  • 标签
  • 归档

NipGeihou

我见青山多妩媚,料青山见我应如是
  • Java

    • 开发规范
    • 进阶笔记
    • 微服务
    • 快速开始
    • 设计模式
  • 其他

    • Golang
    • Python
    • Drat
  • Redis
  • MongoDB
  • 数据结构与算法
  • 计算机网络
  • 应用

    • Grafana
    • Prometheus
  • 容器与编排

    • KubeSphere
    • Kubernetes
    • Docker Compose
    • Docker
  • 组网

    • TailScale
    • WireGuard
  • 密码生成器
  • 英文单词生成器
🍳烹饪
🧑‍💻关于
  • 分类
  • 标签
  • 归档
  • 设计模式

  • 开发规范

  • 经验分享

  • 记录

  • 快速开始

  • 笔记

    • 多线程与并发

      • 前言
      • 理论基础
      • Java中各种锁的概念
        • 悲观锁 VS 乐观锁
        • 自旋锁
        • 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
        • 公平锁 VS 非公平锁
        • 可重入锁 VS 非可重入锁
        • 独享锁(互斥锁) VS 共享锁
      • 关键字:synchronized
      • 关键字:volatile
      • 关键字:final
      • Java多线程 - 线程池
      • Java多线程 - Block Queue阻塞队列
      • Java多线程 - 辅助类
      • Java多线程 - CompletableFuture
      • Java多线程 - 线程变量传递ThreadLocal
    • JDK

    • Java集合

    • Spring

    • JVM

    • Other

  • 面试题

  • 微服务

  • 踩过的坑

  • Java
  • 笔记
  • 多线程与并发
NipGeihou
2022-10-18
目录

Java中各种锁的概念

# 悲观锁 VS 乐观锁

对于同一个数据的并发操作:

悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。(Java 中, synchronized关键字 和 Lock的实现类 都是悲观锁。)

乐观锁在 Java 中是通过使用无锁编程来实现,最常采用的是 CAS 算法。(Java 原子类 中的递增操作就通过 CAS 自旋实现的。)

img

  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

为什么不所有场景都使用乐观锁?

在实际开发中,数据操作大部分时候都是在和数据库打交道,报错意味着事务回滚,频繁回滚会导致数据库性能降低。

# 自旋锁

在 JDK1.4 中就引入了,当时默认关闭的。在 JDK 1.6 后默认为开启状态。

img

没有自旋锁的场景

当下计算机处理器核心普通都有双核或以上,因此可以同时执行两个或以上的线程任务。

现有线程 1、2 同时抢夺同一个锁,线程 1 抢到了,由于线程 2 没抢到,因此需要阻塞线程,把处理器资源让出来。而阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

image-20221018205639152

在 “没有自旋锁的场景” 中,如果线程 2 “稍等一下”,那么线程 2 反而能够很快获得锁,从而提高了程序的效率。而为了让当前线程 “稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

自旋锁的缺点:不适合锁被占用的时间长的场景,会白浪费处理器资源。

所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是 10 次,可以使用 -XX:PreBlockSpin 来更改)没有成功获得锁,就应当挂起线程。

自旋锁的实现原理同样也是 CAS,AtomicInteger 中调用 unsafe 进行自增操作的源码中的 do-while 循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

笔记

自旋锁可以这么理解,在挂起线程前,先尝试 count 遍获得锁。

for(int i = 0;i < 10;i++){
    if(getLock()){
       do();
       break;
    }
}

# 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

todo

# 公平锁 VS 非公平锁

img

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。

优点:等待锁的线程不会饿死。

缺点:整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大。

img

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。

优点:可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。

缺点:处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

笔记

公平锁,老老实实排队,先到先得。

非公平锁,先试试插队,不行再排队。

为什么需要非公平锁?

跟自旋锁一样,为了尽可能的提升程序执行效率。

# 可重入锁 VS 非可重入锁

可重入锁,又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者 class),不会因为之前已经获取过还没释放而阻塞。

Java 中 ReentrantLock 和 synchronized 都是可重入锁,

优点:可一定程度避免死锁。

笔记

可重入锁允许同一个线程,反复(递归)获取同一把锁。

非可重入锁只允许同一个线程,获取一次锁,再次获取就会阻塞,死锁。

# 独享锁 (互斥锁) VS 共享锁

独享锁,也叫排他锁、互斥锁,是指该锁一次只能被一个线程所持有。如果线程 T 对数据 A 加上排它锁后,则其他线程不能再对 A 加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。

JDK 中的 synchronized 和 JUC 中 Lock 的实现类就是互斥锁。

共享锁是指该锁可被多个线程所持有。如果线程 T 对数据 A 加上共享锁后,则其他线程只能对 A 再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

ReentrantReadWriteLock 中的 WriteLock 为共享锁。

笔记

  • 在上述描述中共享锁只读,只是一个约定,如果在代码中非要在获取读锁(共享锁)后修改数据是不会报错的,但我们约定应在修改数据时获取写锁,只读数据时获取读锁。

  • 共享锁不会单独使用,通常作为读写锁中的读锁出现。

  • 读(锁)写(锁)、写读、写写的过程互斥,即读锁上锁时,写锁上锁阻塞...

上次更新: 2024/03/11, 22:37:05
理论基础
关键字:synchronized

← 理论基础 关键字:synchronized→

最近更新
01
Docker Swarm
04-18
02
安全隧道 - gost
04-17
03
Solana最佳实践
04-16
更多文章>
Theme by Vdoing | Copyright © 2018-2025 NipGeihou | 友情链接
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式