先赞后看,养成习惯!!! ^ _ ^ ❤️ ❤️ ❤️
码字不易,大家的支持就是我坚持下去的动力,点赞后不要忘了 关注我哦!

如有错误,还请您批评改正^ _ ^

1. 常见的锁策略

1.1 乐观锁和悲观锁

1.1.1 悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别⼈会修改,所以每次在拿数据的时候都会上锁,这样别⼈想拿这个数据就会阻塞直到它拿到锁(锁竞争激烈)

1.1.2 乐观锁

假设数据⼀般情况下不会产⽣并发冲突,所以在数据进⾏提交更新的时候,才会正式对数据是否产⽣并发冲突进⾏检测,如果发现并发冲突了,则让返回⽤⼾错误的信息,让⽤⼾决定如何去做。
加锁的时候锁竞争不激烈,不需要做额外的工作
Synchronized 初始使⽤乐观锁策略. 当发现锁竞争⽐较频繁的时候, 就会⾃动切换成悲观锁策略

1.2 重量级锁 VS 轻量级锁

锁的核⼼特性 “原⼦性”, 这样的机制追根溯源是 CPU 这样的硬件设备提供的。

  • CPU 提供了 “原⼦操作指令”.
  • 操作系统基于 CPU 的原⼦指令, 实现了 mutex 互斥锁.
  • JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类
    ![[Pasted image 20250712104219.png]]

1.2.1 重量级锁

加锁机制重度依赖了操作系统 OS 提供了 mutex
⼤量的内核态⽤⼾态切换
很容易引发线程的调度
synchronized是自适应
JVM内部会统计每个锁竞争的激烈程度
如果竞争不激烈,此时synchronized就会按照轻量级锁(自旋)
如果竞争激烈,此时synchronized就会按照重量级锁(挂起等待)
悲观锁-重量级锁-挂起等待锁
乐观锁-轻量级锁-自旋锁

1.2.2 轻量级锁

加锁机制尽可能不使⽤ mutex, ⽽是尽量在⽤⼾态代码完成. 实在搞不定了, 再使⽤ mutex
少量的内核态⽤⼾态切换.
不太容易引发线程调度
synchronized 开始是⼀个轻量级锁. 如果锁冲突⽐较严重, 就会变成重量级锁

1.3 ⾃旋锁

按之前的⽅式,线程在抢锁失败后进⼊阻塞状态,放弃 CPU,需要过很久才能再次被调度.
但实际上, ⼤部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使⽤⾃旋锁来处理这样的问题
⾃旋锁是⼀种典型的 轻量级锁 的实现⽅式

  • 优点: 没有放弃 CPU, 不涉及线程阻塞和调度, ⼀旦锁被释放, 就能第⼀时间获取到锁.
  • 缺点: 如果锁被其他线程持有的时间⽐较久, 那么就会持续的消耗 CPU 资源. (⽽挂起等待的时候是不消耗 CPU 的)
    synchronized 中的轻量级锁策略⼤概率就是通过⾃旋锁的⽅式实现的.

1.4 挂起等待锁

挂起等待锁:当线程获取锁是失败后会无限的阻塞等待,直到锁被释放,再与其他线程竞争锁

  • 优点:不会持续消耗cpu的资源
  • 缺点:当锁释放后还要与其他线程竞争,并不会第一时间获取锁

1.5 读写锁

多线程之间,数据的读取⽅之间不会产⽣线程安全问题,但数据的写⼊⽅互相之间以及和读者之间都

需要进⾏互斥。如果两种场景下都⽤同⼀个锁,就会产⽣极⼤的性能损耗。所以读写锁因此⽽产⽣。
⼀个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.
两个线程都只是读⼀个数据, 此时并没有线程安全问题. 直接并发的读取即可.
两个线程都要写⼀个数据, 有线程安全问题.
⼀个线程读另外⼀个线程写, 也有线程安全问
读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现
了读写锁
读写锁特别适合于 “频繁读, 不频繁写” 的场景中. (这样的场景其实也是⾮常⼴泛存在的).
读加锁和读加锁之间, 不互斥.
写加锁和写加锁之间, 互斥.
读加锁和写加锁之间, 互斥

1.6 可重入锁和不可重入锁

一个线程,一把锁,把连续加锁多次,是否会死锁
核心要点:

  1. 锁要记录当前是哪个线程拿到这把锁
  2. 使用计数器,记录当前加锁多少次,在合适的时候解锁
    Java⾥只要以Reentrant开头命名的锁都是可重⼊锁,⽽且JDK提供的所有现成的Lock实现类,包括

synchronized关键字锁都是可重⼊的。

1.7公平锁和非公平锁

公平锁: 遵守 “先来后到”. B ⽐ C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
⾮公平锁: 不遵守 “先来后到”. B 和 C 都有可能获取到锁

1.8 加锁过程

JVM 将 synchronized 锁分为 ⽆锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进⾏依次升
级。

  1. 偏向锁: 第⼀个尝试加锁的线程, 优先进⼊偏向锁状态,做标记,并非真加锁
  2. 轻量级锁:随着其他线程进⼊竞争, 偏向锁状态被消除, 进⼊轻量级锁状态(⾃适应的⾃旋锁)
  3. 重量级锁:如果竞争进⼀步激烈, ⾃旋不能快速获取到锁状态, 就会膨胀为重量级锁
    此处的重量级锁就是指⽤到内核提供的 mutex

2. 其他优化操作

2.1 锁消除

编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除
什么是 “锁消除”
有些应⽤程序的代码中, ⽤到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer)

2.2锁粗化

段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会⾃动进⾏锁的粗化.
实际开发过程中, 使⽤细粒度锁, 是期望释放锁的时候其他线程能使⽤锁.
但是实际上可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会⾃动把锁粗化, 避免频繁申请释放
锁.

3.CAS

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

  1. ⽐较 A 与 V 是否相等。(⽐较)

  2. 如果⽐较相等,将 B 写⼊ V。(交换)

  3. 返回操作是否成功。
    当多个线程同时对某个资源进⾏CAS操作,只能有⼀个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。
    CAS 可以视为是⼀种乐观锁

3.1 CAS 有哪些应⽤

  1. 实现原⼦类:
    `
boolean CAS(address, expectValue, swapValue) {`

`if (&address == expectedValue) {`

`&address = swapValue;`

`return true;`

`}`

`return false;`

`}`
  1. 实现⾃旋锁
    **
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;**

**}**

**}**

3.2 CAS 的 ABA 问题

ABA 的问题:

假设存在两个线程 t1 和 t2. 有⼀个共享变量 num, 初始值为 A.
接下来, 线程 t1 想使⽤ CAS 把 num 值改成 Z, 那么就需要

  • 先读取 num 的值, 记录到 oldNum 变量中.
  • 使⽤ CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.
    但是, 在 t1 执⾏这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, ⼜从 B 改成了 A
    线程 t1 的 CAS 是期望 num 不变就修改. 但是 num 的值已经被 t2 给改了. 只不过⼜改成 A 了. 这个时候 t1 究竟是否要更新 num 的值为 Z 呢?
    到这⼀步, t1 线程⽆法区分当前这个变量始终是 A, 还是经历了⼀个变化过程.

3.2.1 解决⽅案

给要修改的值, 引⼊版本号. 在 CAS ⽐较数据当前值和旧值的同时, 也要⽐较版本号是否符合预期.
CAS 操作在读取旧值的同时, 也要读取版本号.
真正修改的时候,
如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
如果当前版本号⾼于读到的版本号. 就操作失败(认为数据已经被修改过了).

4. Callable 接⼝

Callable 是⼀个 interface . 相当于把线程封装了⼀个 “返回值”. ⽅便程序猿借助多线程的⽅式计算结
果
`

public class Demo29 {`  
    `public static void main(String[] args) throws ExecutionException, InterruptedException {`  
        `//此时callable不能执行,需要借助Thread`  
        `Callable<Integer> callable=new Callable<Integer>() {`  
            `@Override`  
            `public Integer call() throws Exception {`  
                `int result=0;`  
                `for (int i=1;i<=100;i++){`  
                    `result+=i;`  
                `}`  
                `return result;`  
            `}`  
        `};`  
        `FutureTask<Integer> futureTask=new FutureTask<>(callable);`  
        `Thread t=new Thread(futureTask);`  
        `t.start();`  
        `//get操作就是获取FutureTask的返回值,这个返回值就是来自于Callable的call方法`  
        `//get可能会阻塞,如果当前线程执行完毕,get会拿到结果`  
        `//如果当前线程还没执行完毕,get会一直阻塞`  
        `System.out.println(futureTask.get());`  
    `}`  
`}`

创建⼀个匿名内部类, 实现 Callable 接⼝. Callable 带有泛型参数. 泛型参数表⽰返回值的类型.
重写 Callable 的 call ⽅法, 完成累加的过程. 直接通过返回值返回计算结果.
把 callable 实例使⽤ FutureTask 包装⼀下.
创建线程, 线程的构造⽅法传⼊ FutureTask . 此时新线程就会执⾏ FutureTask 内部的 Callable 的
call ⽅法, 完成计算. 计算结果就放到了 FutureTask 对象中.
在主线程中调⽤ futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的
结果

4.1 理解 Callable

Callable 和 Runnable 相对, 都是描述⼀个 “任务”. Callable 描述的是带有返回值的任务, Runnable
描述的是不带返回值的任务.
Callable 通常需要搭配 FutureTask 来使⽤. FutureTask ⽤来保存 Callable 的返回结果. 因为

Callable 往往是在另⼀个线程中执⾏的, 啥时候执⾏完并不确定.

FutureTask 就可以负责这个等待结果出来的⼯作

4.2 理解 FutureTask

想象去吃⿇辣烫. 当餐点好后, 后厨就开始做了. 同时前台会给你⼀张 “⼩票” . 这个⼩票就是

FutureTask. 后⾯我们可以随时凭这张⼩票去查看⾃⼰的这份⿇辣烫做出来了没.

5. ReentrantLock

可重⼊互斥锁. 和 synchronized 定位类似, 都是⽤来实现互斥效果, 保证线程安全
ReentrantLock 的⽤法:

lock(): 加锁, 如果获取不到锁就死等.

trylock(超时时间): 加锁, 如果获取不到锁, 等待⼀定的时间之后就放弃加锁.

unlock(): 解锁
`

ReentrantLock lock = new ReentrantLock();`

`-----------------------------------------`

`lock.lock();`

`try {`

`// working`

`} finally {`

`lock.unlock()`

`}`

6. ReentrantLock 和 synchronized 的区别:

  • synchronized 是⼀个关键字, 是 JVM 内部实现的(⼤概率是基于 C++ 实现). ReentrantLock 是标准
    库的⼀个类, 在 JVM 外实现的(基于 Java 实现).
  • synchronized 使⽤时不需要⼿动释放锁. ReentrantLock 使⽤时需要⼿动释放. 使⽤起来更灵活, 但
    是也容易遗漏 unlock.
  • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的⽅式等待⼀段时间就放
    弃.
  • synchronized 是⾮公平锁, ReentrantLock 默认是⾮公平锁. 可以通过构造⽅法传⼊⼀个 true 开启
    公平锁模式
  • 更强⼤的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是⼀个
  • 随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定
    的线程.

6.1 如何选择使⽤哪个锁?

锁竞争不激烈的时候, 使⽤ synchronized, 效率更⾼, ⾃动释放更⽅便.
锁竞争激烈的时候, 使⽤ ReentrantLock, 搭配 trylock 更灵活控制加锁的⾏为, ⽽不是死等.
如果需要使⽤公平锁, 使⽤ ReentrantLock

7. 原⼦类

原⼦类内部⽤的是 CAS 实现,所以性能要⽐加锁实现 i++ ⾼很多。原⼦类有以下⼏个

  • AtomicBoolean

  • AtomicInteger

  • AtomicIntegerArray

  • AtomicLong

  • AtomicReference

  • AtomicStampedReference
    addAndGet(int delta); i += delta;

decrementAndGet(); --i;

getAndDecrement(); i–;

incrementAndGet(); ++i;

getAndIncrement(); i++;

8. 信号量 Semaphore

信号量, ⽤来表⽰ “可⽤资源的个数”. 本质上就是⼀个计数器
理解信号量

可以把信号量想象成是停⻋场的展⽰牌: 当前有⻋位 100 个. 表⽰有 100 个可⽤资源.

当有⻋开进去的时候, 就相当于申请⼀个可⽤资源, 可⽤⻋位就 -1 (这个称为信号量的 P 操作)

当有⻋开出来的时候, 就相当于释放⼀个可⽤资源, 可⽤⻋位就 +1 (这个称为信号量的 V 操作)

如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.

Semaphore 的 PV 操作中的加减计数器操作都是原⼦的, 可以在多线程环境下直接使用
也可以当锁用
`

public class Demo32 {`  
    `private static int count=0;`  
    `public static void main(String[] args) throws InterruptedException {`  
        `Semaphore semaphore=new Semaphore(1);`  
        `Thread t1=new Thread(new Runnable() {`  
            `@Override`  
            `public void run() {`  
                `for(int i=0;i<50000;i++){`  
                    `try {`  
                        `semaphore.acquire();`  
                    `} catch (InterruptedException e) {`  
                        `throw new RuntimeException(e);`  
                    `}`  
                    `count++;`  
                    `semaphore.release();`  
                `}`  
            `}`  
        `});`  
        `Thread t2=new Thread(new Runnable() {`  
            `@Override`  
            `public void run() {`  
                `for(int i=0;i<50000;i++){`  
                    `try {`  
                        `semaphore.acquire();`  
                    `} catch (InterruptedException e) {`  
                        `throw new RuntimeException(e);`  
                    `}`  
                    `count++;`  
                    `semaphore.release();`  
                `}`  
            `}`  
        `});`  
        `t1.start();`  
        `t2.start();`  
        `t1.join();`  
        `t2.join();`  
        `System.out.println("Fina count = "+count);`  
  
    `}`  
`}`

9. CountDownLatch

同时等待 N 个任务执⾏结束
好像跑步⽐赛,10个选⼿依次就位,哨声响才同时出发;所有选⼿都通过终点,才能公布成绩。
构造 CountDownLatch 实例, 初始化 10 表⽰有 10 个任务需要完成.
每个任务执⾏完毕, 都调⽤ latch.countDown() . 在 CountDownLatch 内部的计数器同时⾃减
主线程使用latch.await()阻塞等待所有任务执行完成,相当于计数器为0了

**`public class Demo33 {**`  
    `**public static void main(String[] args) throws InterruptedException {**`  
        `**//构造方法中的参数表示任务的个数**`  
        `**CountDownLatch latch=new CountDownLatch(10);**`  
        `**ExecutorService executorService= Executors.newFixedThreadPool(4);**`  
        `**for (int i=0;i<10;i++){**`  
  
        

`**int id=i;**`  
        `**executorService.submit(()->{**`  
            `**System.out.println("子任务开始执行:"+id );**`  
            `**try {**`  
                `**Thread.sleep(1000);**`  
            `**} catch (InterruptedException e) {**`  
                `**throw new RuntimeException(e);**`  
            `**}**`  
            `**System.out.println("子任务结束执行:"+id);**`  
            `**latch.countDown();**`  
        `**});**`  
  
    `**}**`  
    `**latch.await();**`  
    `**System.out.println("所有任务都执行完毕");**`  
    `**executorService.shutdown();**`  
`**}**`  

`**}`**

10. 线程安全的集合类

原来的集合类, ⼤部分都不是线程安全的.

Vector, Stack, HashTable, 是线程安全的(不建议⽤), 其他的集合类不是线程安全的

10.1 多线程环境使⽤ ArrayList

  1. ⾃⼰使⽤同步机制 (synchronized 或者 ReentrantLock)
    前⾯做过很多相关的讨论了. 此处不再展开.
  2. Collections.synchronizedList(new ArrayList);
    synchronizedList 是标准库提供的⼀个基于 synchronized 进⾏线程同步的 List. synchronizedList
    的关键操作上都带有 synchronized(代价较高)
  3. 使⽤ CopyOnWriteArrayList
    CopyOnWrite容器即写时复制的容器。
    当我们往⼀个容器添加元素的时候,不直接往当前容器添加,⽽是先将当前容器进⾏Copy,复制
    出⼀个新的容器,然后新的容器⾥添加元素,
    添加完元素之后,再将原容器的引⽤指向新的容器
    这样做的好处是我们可以对CopyOnWrite容器进⾏并发的读,⽽不需要加锁,因为当前容器不会添

加任何元素。

所以CopyOnWrite容器也是⼀种读写分离的思想,读和写不同的容器。

优点:

在读多写少的场景下, 性能很⾼, 不需要加锁竞争.

缺点:

  1. 占⽤内存较多.

  2. 新写的数据不能被第⼀时间读取到

10.2 多线程环境使⽤队列

  1. ArrayBlockingQueue

比特就业课基于数组实现的阻塞队列

  1. LinkedBlockingQueue

基于链表实现的阻塞队列

  1. PriorityBlockingQueue

基于堆实现的带优先级的阻塞队列

  1. TransferQueue

最多只包含⼀个元素的阻塞队列

10.3多线程环境使⽤哈希表

HashMap 本⾝不是线程安全的.
在多线程环境下使⽤哈希表可以使⽤:
Hashtable
ConcurrentHashMap

  1. Hashtable

只是简单的把关键⽅法加上了 synchronized 关键字
这相当于直接针对 Hashtable 对象本⾝加锁.

如果多线程访问同⼀个 Hashtable 就会直接造成锁冲突.

size 属性也是通过 synchronized 来控制同步, 也是⽐较慢的.

⼀旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到⼤量的元素拷⻉, 效率会⾮常低
2. ConcurrentHashMap
读操作没有加锁(但是使⽤了 volatile 保证从内存读取结果), 只对写操作进⾏加锁. 加锁的⽅式仍然
是是⽤ synchronized, 但是不是锁整个对象, ⽽是 “锁桶” (⽤每个链表的头结点作为锁对象), ⼤⼤降
低了锁冲突的概率.

  • 充分利⽤ CAS 特性. ⽐如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.
  • 优化了扩容⽅式: 化整为零
  • 发现需要扩容的线程, 只需要创建⼀个新的数组, 同时只搬⼏个元素过去.
  • 扩容期间, 新⽼数组同时存在.
  • 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运⼀⼩部
    分元素
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐