一文足以了解什么是 Java 中的锁
作者 | cxuan
责编 | Elle
Java 锁分类你是否曾为Java并发编程中的锁选择而纠结?面对“悲观锁”、“自旋锁”、“偏向锁”等诸多概念感到无从下手?本文将为你彻底厘清Java锁的完整知识体系。掌握这些,你就能在各类高并发场景中游刃有余地选择最合适的“锁”略。
Java中的锁种类繁多,但万变不离其宗。我们可以从多个维度对其进行清晰归类,下图便是一张全面的锁分类图谱:
从加锁思想层面,可分为悲观锁与乐观锁。
从等待策略层面,可分为自旋锁。
从锁的优化状态层面(针对Synchronized),可分为无锁、偏向锁、轻量级锁、重量级锁。
从获取锁的公平性层面,可分为公平锁与非公平锁。
从锁的重入性层面,可分为可重入锁与不可重入锁。
从锁的共享性层面,可分为共享锁与排他锁(独占锁)。
接下来,我们将逐一深入剖析,为你揭开每种锁的神秘面纱与应用场景。
一、核心战术思想:悲观锁 vs 乐观锁这并非具体的锁实现,而是两种根本性的并发控制策略,是你设计高并发系统的第一道选择题。
悲观锁:坚信“总会有人抢”
悲观锁持一种“最坏打算”的哲学:它认定共享数据随时可能被其他线程修改。它在访问数据前会果断“上锁”,独占资源,直到操作完成才释放。在此期间,其他所有尝试访问的线程都必须挂起等待。
典型实现:数据库中的行锁、表锁、读写锁;Java中的 `synchronized` 关键字和 `ReentrantLock` 独占锁。它们本质上都是这种“先获取,后操作”思想的体现。
乐观锁:相信“冲突不常有”
乐观锁则更为“乐观”,它假定并发冲突很少发生,因此读数据时不上锁。但在更新数据时,会通过某种机制(如版本号或CAS)判断在此期间数据是否被他人改动过。如果没有,则更新成功;如果有,则通常采取重试策略。
典型实现:版本号机制、CAS(比较并交换)算法。Java中的 `java.util.concurrent.atomic` 包下的原子类,即是CAS的经典应用。
战术抉择:如何选择?
两种策略各有其最佳战场:
悲观锁 适合“写多读少”的激烈冲突场景。例如,一段关键金融交易代码,确保绝对的数据一致性优先级最高,此时性能开销可以接受。一次典型的数据库悲观锁调用如下:
select from student where name="cxuan" for update
乐观锁 则更适合“读多写少”的高并发场景。它能极大提升系统吞吐量,避免了加锁解锁的巨大开销。典型的应用如商品库存扣减、成本系统金额核对等,在极少发生写入冲突的情况下,性能优势显著。
乐观锁的两种经典实现
1. 版本号机制
在数据表中增加一个 `version` 字段。每次更新时,均需验证当前读取的版本号是否与数据库中的一致,一致则更新并将版本号+1,否则重试。这完美解决了类似“两个柜员同时操作同一账户”的并发问题。
通过版本号的严格递增与校验,确保后续的更新不会覆盖掉前序有效的更新。
2. CAS算法
CAS(Compare And Swap)是一种硬件级别的原子操作。它包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。当且仅当V的值等于A时,才会用B更新V,否则不做任何操作。整个过程是原子的。
Java通过 `sun.misc.Unsafe` 类提供的本地方法,并封装在 `AtomicInteger`、`AtomicLong` 等原子类中,让我们能够直接使用CAS的强大能力,实现无锁线程安全。
注意:乐观锁的弱点
没有完美的方案,乐观锁需警惕以下问题:
ABA问题
一个值从A变成B,又变回A,CAS检查时会认为它从未改变。对于敏感操作,这可能引发问题。解决方案是使用类似 `AtomicStampedReference` 的类,附加一个版本戳或时间戳。
自旋开销
在竞争激烈时,线程可能长时间循环重试(自旋),消耗大量CPU资源。乐观锁不适合长时间持有或竞争剧烈的场景。
终极选择:CAS 还是 synchronized?
低冲突(多读)场景:首选CAS,其性能远超synchronized,避免了用户态/内核态切换和线程调度的开销。
高冲突(多写)场景:选择synchronized(或ReentrantLock)。此时CAS因频繁自旋反而会降低性能,而synchronized会让竞争失败的线程直接挂起,减少无谓的CPU消耗。
自旋锁的诞生
当线程请求一个已被占用的锁时,系统该如何处理?有两种经典策略:一是让线程“休眠”(阻塞),等待被唤醒,这是互斥锁的做法;二是让线程“原地等待一会儿,并不断重试”,这就是自旋锁。
什么是自旋锁?
自旋锁采用“循环尝试”的策略:线程发现锁被占用时,并不立即放弃CPU进入阻塞状态,而是执行一个忙循环(自旋),不断检查锁是否被释放,一旦释放就立即获取。
自旋锁的精髓
其核心优势在于:避免了线程状态切换的开销(用户态到内核态的转换、上下文切换)。如果锁的持有时间非常短,自旋等待的代价远小于让线程阻塞再唤醒的代价。
但自旋锁是“双刃剑”。若锁被长时间持有,自旋线程会白白消耗CPU周期。JVM引入了适应性自旋:自旋时间不再固定,而是由前一次在同一个锁上的自旋成功与否及锁持有者的状态动态决定。
实现一个简单的自旋锁
public class SpinLockTest {
private AtomicBoolean available = new AtomicBoolean(false);
public void lock {
// 循环尝试获取锁
while (!tryLock) {
// 自旋等待...
}
}
public boolean tryLock {
return available.compareAndSet(false, true);
}
public void unLock {
if(!available.compareAndSet(true, false)){
throw new RuntimeException("释放锁失败");
}
}
}
但上述实现是非公平的,可能引发线程“饥饿”。为此,科学家们设计了更高级的排队自旋锁,如 TicketLock、CLHLock 和 MCSLock,它们通过FIFO队列保证了公平性,并减少了在多处理器下的缓存同步流量,性能更优。
CLHLock 与 MCSLock
两者都是基于链表的先进排队自旋锁。主要区别在于:CLHLock 是隐式链表,通过监控前驱节点的状态来获取锁;MCSLock 是显式链表,每个节点持有其后继节点的引用。它们都将自旋的“位置”从全局变量转移到了本地变量,大幅提升了可扩展性。
三、锁的优化之路:Synchronized的四种状态synchronized 是 Java 中最常用的锁,但早期的它是纯粹的“重量级锁”,性能堪忧。为了优化,JVM 团队为 synchronized 设计了逐步升级的锁状态。
基石:Java对象头与Monitor
synchronized 的锁信息存储在对象头中。对象头主要包含两部分:Mark Word(存储哈希码、GC分代年龄、锁标志位等)和类型指针。锁的升级过程,就是Mark Word中内容变化的过程。
锁的状态信息就编码在 Mark Word 中,随着竞争加剧,锁会从无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁方向升级(不可降级)。
锁的升级全景图
1. 无锁状态
没有任何线程竞争资源。共享数据仅靠 volatile 或 CAS 等无锁算法保证线程安全,效率最高。
2. 偏向锁
“偏袒”第一个获得它的线程。在无竞争的情况下,整个同步周期内都无需再进行任何同步操作(如CAS)。只需在对象头和栈帧的锁记录中存储持有锁的线程ID。以后该线程再进入时,只需简单比较线程ID即可,代价极低。
一旦有另一个线程尝试获取锁,偏向模式立即结束,根据竞争情况升级为轻量级锁。
3. 轻量级锁
当有轻微竞争时(如两个线程交替执行),JVM 会使用轻量级锁。其本质是:在虚拟机栈中创建锁记录空间,将对象头的 Mark Word 复制到然后尝试用 CAS 将对象头指向这个锁记录。如果成功,当前线程获得锁;如果失败,说明有竞争,线程会先自旋重试,自旋失败则升级为重量级锁。
4. 重量级锁
当竞争进一步加剧(多线程同时竞争),轻量级锁会膨胀为重量级锁。此时,未获得锁的线程会被挂起,进入阻塞队列,等待操作系统调度唤醒。这就是最传统的 synchronized 实现,涉及到用户态到内核态的转换,开销最大。
这个升级过程是单向且自动的,目的是在无竞争、低竞争、高竞争的不同场景下,都能达到最优的性能平衡。
四、秩序之争:公平锁与非公平锁想象一下银行柜台:公平锁就像大家严格按取号顺序办理业务,先来后到。而非公平锁则允许新来的客户“插队”尝试,如果恰巧柜员空闲,他就能立刻办理,无需理会等候更久的人。
在Java中,`ReentrantLock` 是区分公平与非公平的典型代表,通过构造函数的布尔参数 `fair` 控制。
如何实现公平性?
公平锁的核心在于 `hasQueuedPredecessors()` 方法。它检查同步队列中是否有比当前线程等待时间更长的线程。如果有,当前线程乖乖排队;如果没有,才能尝试获取锁。而非公平锁则直接尝试获取,无视队列顺序。
性能权衡:公平锁保证了顺序,但增加了线程切换的开销,通常吞吐量低于非公平锁。非公平锁虽可能造成线程“饥饿”,但减少了挂起和唤醒的开销,吞吐量更高。大多数情况下,默认的非公平锁是更好的选择。
五、可重入性:可重入锁 vs 不可重入锁可重入锁(递归锁)
指在同一个线程中,外层方法获取锁后,内层方法可以自动获取同一把锁。这是防止死锁的重要机制。Java 中,`synchronized` 和 `ReentrantLock` 都是可重入锁。
private synchronized void doSomething {
System.out.println("doSomething...");
doSomethingElse; // 可重入,不会死锁
}
private synchronized void doSomethingElse {
System.out.println("doSomethingElse...");
}
不可重入锁
与上述相反,如果锁不可重入,在上面的例子中,调用 `doSomethingElse` 时,线程会因为试图获取自己已持有的锁而发生死锁。不可重入锁在实际开发中极少使用。
六、共享性:共享锁 vs 独占锁独占锁(排他锁)
同一时刻只允许一个线程持有,如 `ReentrantLock`、`synchronized`。写操作必须使用独占锁。
共享锁
同一时刻允许多个线程持有,但通常仅限于读操作。典型的实现是 `ReentrantReadWriteLock` 中的 ReadLock。
读写锁(`ReadWriteLock`)完美结合了二者:允许多个读锁同时进行,但写锁是独占的,且与读锁互斥。这极大地提升了“读多写少”场景下的并发性能。
声明:本文为作者投稿,版权归作者个人所有。
【End】
☞干货分享: 服务器处理器基础知识
相关问答
JAVA题:编写一个加密程序,对用户输入的字符串加密后输出。加...
JAVA题:编写一个加密程序,对用户输入的字符串加密后输出。加密的方法是将各个字符映射成字母表中的对称JAV讨论回答(6)justingdingpublicclassChangeCha....
javaweb的数据传输加密?
现在流行的是用RSA进行加密,然后传输。也有很多人直接用md5进行签名,也说不清楚,感觉像是加密了。至于传输,给传输通道加密,好像有点大动干戈的意思。还是...
在JAVA中输入密码。,怎么可以设置为仅能为数字或者字母。是...
在JAVA中输入密码。,怎么可以设置为仅能为数字或者字母。是编程,如果输入其他的提示错误。JAV讨论回答(5)是你的正则写的有问题Stringregex="^[A-Za-z...
java怎么验证用户名和密码?
可能LZ对使用浏览器进行用户名密码认证比较清楚SOCKET走的是TCP/IP协议,而浏览器方式走的是HTTP协议不管哪种方式,都是通过客户端程序上发到服务器端,而浏览器...
java怎么实现登录成功后页面跳转并弹出修改密码的对话框?
带个参数redirect_url,登录成功,判断有没有redirect_url,有就重定向就完事了带个参数redirect_url,登录成功,判断有没有redirect_url,有就重定向就...
java中使用MD5加密后也不安全了吗?
MD5是hash算法本来就不是用来加密的,只不过因为其极少的碰撞才被大量用来加密.完全可以用穷举法去尝试....MD5是hash算法本来就不是用来加密的,只不过因为其...
如何用java编程显示手机中所有保存的wifi名称及密码?
用java编程软件设置可以显示手机所保存wifi。编程就是为了借助于计算机来达到目的或解决问题,使用程序设计语言编写程序代码,并最终得到结果的过程。计算机功...
多个线程可以读一个变量,只有一个线程可以对这个变量进行写,到底要不要加锁?
其实加锁也很简单,Java中对该变量进行volatile修饰即可!不了解volatile的同学需要学习了,本质上是为了保证变量操作时候的内存可见性。那怎么保证内存可见...
ppcjava模拟器_九州醉餐饮网
ppcjava模拟器勒索病毒让无数的电脑中招,现在阿里云安全团队终于发布了勒索病毒专杀工具,据说可以一键修复,彻底查杀哦,我们一起来看看。阿里云云盾勒索加密文件...
在座的大侠跪求java加密有什么安全方案?_天涯问答_天涯社区
[回答]IP-guard反对solidworks,你这个图片看上去样子就是IP-guard加密系统IP-guard反对solidworks软件,反对其图纸格式的自动加密维护,只要在部署了IP-gua...