原创 张力尹 2025-06-04 08:31 重庆
Java 线程请求某一个资源失败的时候就会进入阻塞状态,处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列等待执行。 当线程调用wait、join、pack函数时候会进入等待状态,需要其它线程显性的唤醒否则会处于等待状态。
🔑Java中的锁分为多种类型,包括独占锁(如synchronized)、共享锁(如读写锁)、公平锁和非公平锁等。不同锁在共享粒度和公平性方面有所差异,适用于不同的并发场景。
💡synchronized是JVM提供的内置锁,通过对象头中的Mark Word和Monitor实现。它支持锁状态的动态升级,包括偏向锁、轻量级锁和重量级锁,以优化性能。synchronized适用于线程数少、锁竞争低的场景。
⚙️ReentrantLock是Java中灵活的锁机制,基于AbstractQueuedSynchronizer(AQS)实现,提供了可重入性、可中断性、非阻塞尝试和超时获取等高级功能。ReentrantLock支持公平和非公平模式,适用于需要更细粒度控制的场景。
🚀Kotlin协程提供了Mutex等轻量级锁,以及Channel和Actor等并发工具。Mutex不会阻塞线程,而是挂起协程,Channel用于线程安全的数据传递,Actor通过单线程状态访问避免锁竞争,提供了更高效的并发编程方式。
原创 张力尹 2025-06-04 08:31 重庆
Java 线程请求某一个资源失败的时候就会进入阻塞状态,处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列等待执行。 当线程调用wait、join、pack函数时候会进入等待状态,需要其它线程显性的唤醒否则会处于等待状态。
自旋锁
自旋锁是指线程在获取锁失败时,不立即挂起,而是执行一段空循环(自旋),尝试重新获取锁。自旋锁适用于「线程持有锁的时间很短」的场景,避免了线程挂起和唤醒的开销。JVM 的轻量级锁会利用自旋锁,在一定次数自旋后如果还未获取锁,就会升级为重量级锁。自旋锁是轻量级锁的一部分策略
,用于短时间等待锁释放。「CAS」 是轻量级锁实现的核心,提供无锁的原子操作。在自旋锁中,CAS 是实现锁状态更新的核心技术。当多个线程竞争锁时,自旋锁通过 CAS 判断当前锁是否可用,并在锁可用时将其状态更新为已占用。「自旋锁」「CAS」是一种锁机制,强调等待策略(自旋)。
是硬件支持的原子操作。
用于在竞争条件下协调线程的访问。
用于实现无锁状态的更新。
内部常使用 CAS 判断和更新锁状态。
可单独用于原子变量的更新(如计数)。
重量级锁如果自旋等待的线程越来越多,或者线程自旋的时间过长,轻量级锁会升级为重量级锁。 重量级锁通过操作系统的互斥量(Mutex)实现,会导致线程挂起和上下文切换,性能较低。 「特点」:适合高竞争场景,但代价较高。按锁的性能优化方式自旋锁 线程在尝试获取锁时,不直接阻塞,而是自旋一段时间再尝试获取。典型代表:JVM 的轻量级锁使用自旋锁。无锁 基于 CAS 实现,没有真正的锁,适用于简单的原子操作。典型代表:AtomicInteger、AtomicReference。读写锁 区分读操作和写操作,读操作可以并发,写操作需要独占。典型代表:ReentrantReadWriteLock。按锁的可中断性可中断锁 可以在获取锁时响应中断,避免线程一直等待。典型代表:ReentrantLock 提供的 lockInterruptibly() 方法。不可中断锁 线程在等待锁时无法响应中断。典型代表:synchronized。按锁的可重入性可重入锁 一个线程获取锁后,可以再次获取该锁(递归调用时无需阻塞)。典型代表:ReentrantLock、synchronized。特点:避免死锁问题,适用于递归调用或多方法协作场景。非可重入锁 一个线程获取锁后,如果再次尝试获取,会发生死锁。典型代表:java.util.concurrent.locks.Lock 接口的某些实现。按是否为显式锁显式锁 开发者需要手动控制锁的获取和释放。典型代表:ReentrantLock、ReadWriteLock。灵活性更高,支持尝试加锁、超时加锁等高级功能。需要显式调用 lock() 和 unlock() 方法来加锁和解锁,容易出现忘记释放锁的问题。隐式锁 由 JVM 自动管理,无需开发者手动处理。典型代表:synchronized。使用简单,只需在方法或代码块前加 synchronized 关键字。自动释放锁(如方法或代码块执行结束时)。还有其他的分类标准,在此不赘述。JVM 平台的锁实现synchronizedReentrantLock / ReentrantReadWriteLock基于 CAS 的无锁机制:java 提供的 java.util.concurrent.atomic 包StampedLock:Java 8 引入的一种优化锁,支持三种模式:「写锁」:独占锁。「读锁」:允许多个线程访问。「乐观读」:非阻塞读取。synchronized 详解在 Java 中,synchronized 是一种重量级锁,属于 JVM 提供的内置同步机制,用于保证多线程环境下的共享资源访问安全。其实现依赖 JVM 的内置机制,如对象头(Object Header)和监视器(Monitor)。JVM 对象内存布局在 JVM 中,每个对象在内存中分为以下几部分:对象头指向对象所属类的元数据,表示对象的类型。通常是一个指针的大小(4 字节或 8 字节)。存储对象的运行时数据,例如锁状态、GC 标记、哈希值等。Mark Word 是一个 32 位或 64 位字段(取决于 JVM 位数)。根据对象的状态(如锁状态、垃圾回收阶段)内容会有所不同。Mark WordKlass Pointer「锁状态」「Mark Word 内容」「标志位」「线程 ID」(可能包含)
「无锁」对象哈希码、GC 信息
01
无
「偏向锁」持有锁的线程 ID 和时间戳
01
是
「轻量级锁」指向线程栈中锁记录的指针
00
是
「重量级锁」「指向 Monitor 的指针」10
无
「GC 标记」GC 信息
11
无
实例数据(Instance Data)实例数据是对象的主要部分,存储对象的实际字段(成员变量)值。按字段在类中声明的顺序存储。优先分配基本数据类型,按字节对齐规则存储(以提高访问效率)。对象的字段值,包括基本类型和引用类型的指针。对齐填充(Padding)为了满足内存对齐要求,填充无意义的字节。JVM 通常要求对象的起始地址是 8 字节或 16 字节的整数倍。减少内存碎片,提高访问效率。实现机制偏向锁如果一个线程首次访问对象,JVM 将线程 ID 写入对象头的 Mark Word。后续该线程访问时,只需检查 Mark Word 是否匹配,无需进行 CAS 操作。如果另一个线程尝试获取偏向锁,则撤销锁,升级为轻量级锁。轻量级锁线程尝试通过 CAS 操作将对象头的 Mark Word 替换为指向线程栈中锁记录的指针。如果 CAS 失败,表示存在锁竞争,线程会自旋尝试获取锁。线程在短时间内自旋尝试获取锁,避免线程阻塞。自旋的次数由 JVM 参数 -XX:PreBlockSpin 决定。重量级锁当自旋失败次数超过限制,锁升级为重量级锁,所有竞争锁的线程都会进入阻塞状态。JVM 为每个对象关联一个 Monitor,阻塞线程会进入 Monitor 的等待队列。当锁释放时,Monitor 负责唤醒等待队列中的线程。从字节码角度解析synchronized 的实现依赖 JVM 指令集中的 「Monitor」 操作。package com;public class tetst { int a = 0; public synchronized void aaa(){ System.out.println("Inside synchronized block"); a= 1; } public void bbb(){ synchronized (this){ a= 2; } }}
上述代码,先执行 javac tetst.java,再执行 javap -c tetst,可输出如下:Compiled from "tetst.java"public class com.tetst { int a; public com.tetst(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_0 6: putfield #2 // Field a:I 9: return public synchronized void aaa(); Code: 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #4 // String Inside synchronized block 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: aload_0 9: iconst_1 10: putfield #2 // Field a:I 13: return public void bbb(); Code: 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: aload_0 5: iconst_2 6: putfield #2 // Field a:I 9: aload_1 10: monitorexit 11: goto 19 14: astore_2 15: aload_1 16: monitorexit 17: aload_2 18: athrow 19: return Exception table: from to target type 4 11 14 any 14 17 14 any}
同步代码块,字节码中包含monitorenter
和monitorexit
指令。同步方法,不需要显式的 monitorenter 和 monitorexit 指令。JVM 会在方法的访问标志中添加ACC_SYNCHRONIZED
标志。ps. 上面看不到ACC_SYNCHRONIZED
标志,在反编译时更换为语句:javap -v -c tetst 即可。使用-v
(verbose)选项获取更详细的字节码信息。Monitor 详解Monitor 是 JVM 内部的一种同步结构,用来实现 Java 中的同步机制。存储在 JVM 的堆或方法区中的专用数据结构里。Monitor 组成:「Owner」:当前持有锁的线程。「Entry List」:等待进入 Monitor 的线程列表。「Wait Set」:调用wait()
的线程列表。「计数器」:记录锁的重入次数。每次同一线程获取锁时,计数器递增。每次释放锁时,计数器递减。当计数器降为 0 时,锁才真正释放,其他线程才能获得锁。在 JVM 层面,synchronized 是通过 「对象头中的 Mark Word」 和 「Monitor」 实现的。「重入计数器」:Monitor
中维护了一个计数器来记录锁的重入次数。每次同一线程获取锁时,计数器递增。每次释放锁时,计数器递减。当计数器降为 0 时,锁才真正释放,其他线程才能获得锁。小结:synchronized 基于对象头的 Mark Word 和 Monitor 实现。支持锁状态的动态升级机制。synchronized
适用于线程数少、锁竞争低的场景。高并发场景下,可以考虑使用更高效的锁实现(如ReentrantLock
或无锁数据结构)。当然了,如果是 kotlin,还是使用其他方式来避免使用锁。ReentrantLock 详解ReentrantLock 是 Java 中一种灵活且高效的锁机制,属于 java.util.concurrent.locks 包的一部分。相比 synchronized,它提供了更细粒度的控制能力,如可重入性、可中断性、非阻塞尝试、超时获取等。「可重入性」:允许同一线程多次获取同一把锁。支持公平与非公平模式「支持中断」:支持线程在等待锁的过程中响应中断信号,通过 trylock(long timeout,TimeUnit unit) 设置超时方法或者将 lockInterruptibly() 放到代码块中,调用 interrupt 方法进行中断。提供tryLock
方法,可以立即返回锁定结果或超时等待。支持显式加锁和解锁,避免隐式释放锁的限制。ReentrantLock 是基于AbstractQueuedSynchronizer
(AQS)实现的,可将其视为一个增强版的互斥锁。private final Sync sync;
Sync
是 ReentrantLock 的核心,负责具体的加锁、解锁逻辑,继承自 AQS。分为 FairSync 公平锁 和 NonFairSync 非公平锁 两种实现。AQS 详解AbstractQueuedSynchronizer 是 java.util.concurrent 包的核心组件,用于构建锁和同步器。「状态字段」:int state,表示同步状态,对于 ReentrantLock,state 的值记录了锁的重入次数。「等待队列」:AQS 维护了一个 FIFO 的双向链表,存储等待获取锁的线程。「条件队列」:是一个单向链表,里面储存的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了同步队列的队尾通过 acquire 和 tryAcquire 尝试获取锁。通过 release 和 tryRelease 释放锁。当获取锁失败时,线程会被加入到 AQS 的等待队列中,并进入阻塞状态。获取资源失败入队、线程唤醒、线程的状态等,AQS 已经实现好,实现 AQS 的子类的任务是:通过CAS
操作维护共享变量state
重写资源的获取方式重写资源释放的方式AQS 的这种设计模式也是模版方法模式。ReentrantLock 代码实现非公平模式:static final class NonfairSync extends Sync { final boolean initialTryLock() { Thread current = Thread.currentThread(); if (compareAndSetState(0, 1)) { //优先尝试抢占锁而不是按队列顺序等待 setExclusiveOwnerThread(current); return true; } else if (getExclusiveOwnerThread() == current) { //如果当前线程已经持有锁,只需增加 state 的值即可。体现可重入性 int c = getState() + 1; if (c < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(c); return true; } else return false; } protected final boolean tryAcquire(int acquires) { if (getState() == 0 && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; }}
公平模式static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final boolean initialTryLock() { Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //检查当前线程是否有前驱节点。如果有,则进入等待队列。 if (!hasQueuedThreads() && compareAndSetState(0, 1)) { setExclusiveOwnerThread(current); return true; } } else if (getExclusiveOwnerThread() == current) { //如果当前线程已经持有锁,只需增加 state 的值即可。体现可重入性 if (++c < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(c); return true; } return false; } protected final boolean tryAcquire(int acquires) { if (getState() == 0 && !hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; }}
AQS 中大量使用了 CAS(Compare-And-Swap)操作,确保状态修改的原子性。 CAS 是通过 JVM 提供的Unsafe
类实现的。是基于硬件实现的无锁操作,直接操作内存地址,性能更高。synchronized vs. ReentrantLock特性
synchronized
ReentrantLock
「是否支持重入」是
是
「实现方式」JVM
实现,其中synchronized
又有多个类型的锁,除了重量级锁是通过monitor
对象 (操作系统 mutex 互斥原语) 实现外,其它类型的通过对象头实现。基于 AQS(AbstractQueuedSynchronizer)
「锁释放」隐式释放(方法或代码块结束)
unlock()
不支持,默认非公平
支持公平和非公平模式
「中断响应」不支持
支持
「超时获取锁」不支持
tryLock(timeout)
)Mutex 是非阻塞的
,与传统线程锁不同,它不会阻塞线程,而是挂起协程
,等待锁被释放。「挂起协程而非线程」:当协程尝试获取已被占用的Mutex
时,该协程会挂起,直到锁释放。「公平性」:非公平「原子操作」:底层使用 CAS(Compare-And-Swap)实现状态的原子更新,确保线程安全。「队列管理」:维护一个等待队列,存储当前正在等待锁的协程方法:lock():尝试获取锁,如果锁被占用,则挂起当前协程。unlock():释放锁,并唤醒等待队列中的下一个协程。tryLock():尝试获取锁,不挂起协程,如果获取失败立即返回 false
。withLock():自动处理锁的获取和释放,项目中用的最多。Mutex 支持协程取消,当协程被取消时,会从等待队列中移除,避免死锁。线程因为 synchronized 被阻塞时,无法响应线程中断或取消信号,会一直等待,直到锁被释放。具体的实现细节比较简单,这里不想说了,可以自行查阅。那能不能用 synchronized 和 ReentrantLock 来保证协程 Coroutine 环境下的数据安全呢synchronized 和 ReentrantLock,都能保证同一时刻,只有一个线程可以访问同步的代码块或临界区,在进入同步块时,会从主内存读取变量,离开同步块时,会将变量刷新到主内存,因此可以解决线程竞争问题。结论:synchronized 和 ReentrantLock 在协程中可以保证数据安全。synchronized 和 ReentrantLock,不适合协程,它会阻塞线程
。如果协程在 synchronized 块中挂起或者说在持有锁的时候挂起,会导致整个线程被阻塞,其他协程无法利用该线程执行,降低了并发性能。概念一、临界区在多线程编程中,为了保证共享资源的正确访问,在某一时间段内,只允许一个线程进行临界区代码的执行,保证代码的正确性和稳定性。private val mutex = Mutex() private var count = 0 suspend fun addCount() { mutex.withLock { //这里开始,获取到锁之后就进入临界区 count++ //这里就是执行临界区代码 }//执行完毕 退出临界区 }
二、内存屏障脱离 Java,单独看内存屏障,有以下几大分类:屏障名称
含义
LoadLoad
前面的读必须完成,后面的读才能开始
LoadStore
前面的读必须完成,后面的写才能开始
StoreStore
前面的写必须完成,后面的写才能开始
StoreLoad
前面的写必须完成,后面的读才能开始「最强」
这些组合就像是在「两个内存访问操作之间加了一堵墙」,确保不会被 CPU 或编译器优化重排。JMM(Java 内存模型)本身不直接暴露内存屏障指令,但 JVM 会根据 Java 语义编译时「插入对应的屏障指令」,尤其在使用volatile
、synchronized
等关键字时。volatile「可见性保证」(确保变量写入被其他线程看到)「重排序限制」(禁止指令调换位置导致逻辑错误)行为
内存屏障
volatile 读
LoadLoad + LoadStore
volatile 写
StoreStore + StoreLoad
所以,JMM 在实现 volatile 时,直接使用上述屏障类型。但实际真正作用到 cpu 会经过一系列编译转换,可以去自行了解。但是初步理解内存屏障是啥,volatile 作用,目前是够了。synchronized 上面说了实现中使用到了 monitor,其内存屏障行为可以用下面的表格去理解:行为
内存屏障
enter monitor 加锁
LoadLoad + LoadStore
exit monitor 释放锁
StoreStore + StoreLoad
三、CASJava 是在Unsafe(sun.misc.Unsafe)
类实现CAS
的操作,而我们知道 Java 是无法直接访问操作系统底层的 API 的 (原因是 Java 的跨平台性限制了 Java 不能和操作系统耦合),所以 Java 并没有在Unsafe
类直接实现CAS
的操作,而是通过 「JDI(Java Native Interface)」 本地调用C/C++
语言来实现CAS
操作的。CAS 导致的 ABA 问题,但可以加版本号解决,细节可以自行查询。CAS 操作并不会锁住共享变量,也就是一种「非阻塞」的同步机制,CAS 就是乐观锁
的实现。Java 利用 CAS 的乐观锁、原子性的特性高效解决了多线程的安全性问题,例如 JDK1.8 中的集合类 ConcurrentHashMap、关键字 volatile、ReentrantLock 等。四、线程的阻塞状态 vs 等待状态Java 线程请求某一个资源失败的时候就会进入「阻塞状态」,处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。 当线程调用wait
、join
、pack
函数时候会进入「等待状态」,需要其它线程显性的唤醒否则会无限期的处于等待状态。ps.开始动笔是在 2025-1-8 号,我看我到底能把这篇文章拖到啥时候写完...哈哈哈 2025-4-11 号,读了一遍,继续完善 哈哈哈,重度拖延症患者...后续我会补齐一些带有歧义的应用场景AI编程资讯AI Coding专区指南:https://aicoding.juejin.cn/aicoding
点击"阅读原文"了解详情~
AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。
鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑