1.Java开发场景中常用的锁总结(上)
在进行Java开发或者后端开发过程中,项目要想运行的精准无误,特别是某些高并发场景下,想要良好的呈现业务需求,锁一定是大家绕不开要学习的知识。这一章节就从我个人学习过程中用到的锁进行探讨分类。
一、锁的分类方式
我们在接触一个新的知识时候一定要先从概念入手,只有清楚地知道事物的概念、分类、分类依据、使用场景,才能运用自如。对于常用的锁分类方式非常复杂,我们主要按照实现方式、作用范围和功能特性进行分类,并关注每一种锁是否支持互斥。
1.1按实现方式分类
从总的实现方式来看,我们常用到锁的概念就两种,一种Java原生的,一种外部锁。
Java自带的就是我们在学习Java并发时候常见的比如: synchronized关键字、ReentrantLock;
外部锁就是借助中间件如Redis、MySQL、ZooKeeper提供的操作,比如Redis中的SETNX(注:这里是指直接使用中间件的操作方法,如果只是引用来存储 本质上还是使用内置锁),这些中间件提供的也就是分布式锁。
| 区别 | 实现 | 架构 | 常见 |
| Java原生 | Java自带 | 单JVM | synchroized、ReentrantLock、 |
| 外部锁 | 中间件提供 | 分布式 |
Redis的SETNX命令、Redisson |
1.2按作用范围分类
所谓作用范围,也就是你的项目涉及的是单个JVM还是多个JVM,因为Java原生锁比如使用synchronized关键字只在自己所在的JVM生效,其他服务还是可以正常进行业务。如果涉及集群架构,你的关键字就是白加了,这个时候就需要使用分布式锁(由中间件提供)。到这你也就能知道,其实第一个分类和第二个分类是一样的,Java原生的锁都是单机锁,而中间件提供的锁都是分布式锁。
1.3按功能特性分类
这一类别就比较复杂了,有一些甚至不是一个具体的锁而是一种思想,比如乐观锁。
我们常见的锁的功能可以分为可重入锁、读写锁、公平/非公平锁、乐/悲观锁、自旋锁。这些锁有的是特性有的是实现思想,要看在不同场景下如何去选择。比如上面讲的用Java原生synchronized实现的锁就是一种可重入锁,同时也是悲观锁的一种实现办法;分布式锁redisson又是一种可重入锁,自带可重入的解决方案;Java的AtomicInteger等类基于CAS算法实现了乐观锁等。
简单了解一下概念即可:
可重入锁就是指同一个线程可以再一次去获取锁操作资源;
公平和非公平:指线程释放锁之后其他线程是否按照请求顺序来获取锁
乐观悲观锁:实现思想,后面会专门讲
自旋:轻量级锁,获取锁失败不会阻塞而是循环
下面我们将挨个进行介绍总结。
二、Java原生和外部锁
2.1Java原生
这里我先基于gpt生成的表再结合背的八股进行一个修改总结。我们将常见的第1、2、3、5进行对比总结,而乐观锁那部分会在后面章节具体讲解。
|
名称 |
类型 |
是否互斥 |
是否阻塞 |
可重入 |
公平性 |
核心特点 |
典型应用场景 |
|---|---|---|---|---|---|---|---|
|
1.synchronized |
内置锁(Monitor) |
✅ |
✅ |
✅ |
❌ |
简单、安全,自动释放 |
普通临界区、业务代码 |
|
2.ReentrantLock |
显式互斥锁 |
✅ |
✅ |
✅ |
✅/❌ |
可中断、超时、公平锁 |
复杂并发控制 |
|
3.ReentrantReadWriteLock – 写锁 |
写锁 |
✅ |
✅ |
✅ |
❌ |
写独占 |
写操作 |
|
4.ReentrantReadWriteLock – 读锁 |
读锁 |
❌ |
❌ |
✅ |
❌ |
多线程可并发读 |
读多写少 |
|
5.StampedLock – 写锁 |
写锁 |
✅ |
✅ |
❌ |
❌ |
性能高,API复杂 |
高并发写 |
|
6.StampedLock – 悲观读 |
读锁 |
❌ |
❌ |
❌ |
❌ |
并发读 |
高并发读 |
|
7.StampedLock – 乐观读 |
乐观读 |
❌ |
❌ |
❌ |
❌ |
几乎无锁 |
极端读多场景 |
|
8.CAS / Atomic:AtomicInteger类等* |
乐观锁 |
❌ |
❌ |
❌ |
❌ |
自旋 + CAS |
计数器、状态 |
|
9.LongAdder / LongAccumulator |
分段 CAS |
❌ |
❌ |
❌ |
❌ |
高并发下性能更好 |
高并发统计 |
通过这个表格我们不难发现,对于锁的定义其实还不是很清楚,习惯上我们认为锁一定是排他机制的,是能让自己享有当前临界资源的,这也是我们常用的锁是互斥锁而带来的混淆。Java中认为,锁就是一种用于控制并发访问的机制,只要是对并发访问有一定约束规则的都可以称为锁。比如这里的读写锁,允许读共享但是写操作排他,这就是一种并发控制的规则,所以也是一种锁;再比如第8、9个提到的CAS算法思想以及实现类如AtomicInteger,这其实不是一种锁,他是基于CPU原子操作实现的并发控制,同一时间所有线程都可以操作变量。因此我们可以总结一下Java中锁的定义至少要满足:
1.具有约束规则:比如互斥访问资源
2.有当前持有者
3.有获取释放语义
4.系统维护
2.1.1synchronized关键字
Q1:什么是synchronized:
中文意思就是同步的意思,他可以控制由他定义的代码块在同一时间只会有一个线程持有,解决线程访问的同步性。
在Java6之前,synchronized关键字效率低下,属于重量级锁。早期实现依赖于底层操作系统(mutex lock),而Java的线程是映射到os的原生线程之上的,所以每一次线程的切换都需要os的帮忙,我们知道操作系统切换一次线程要从用户态转到内核态,需要消耗很长的时间。
Java6之后,该关键字进行优化,引入了自旋锁、适应性自旋锁、轻量级锁等技术进行减少锁操作的开销,提升了效率
Q2:如何使用synchronized关键字:
关键字加在哪,锁住的对象就看该方法的作用范围,锁的粒度就是锁住的东西。
比如加在了静态方法上,那就是锁的整个类class,表示进入这个方法之前需要获得当前class的锁(静态方法不属于任何一个对象,归整个类所有),粒度就是静态方法
加在了实例方法上,那锁对象就是当前实例对象;加在类上和加在静态方法上锁对象都是类。
锁代码块上可以指定是锁类还是锁对象,粒度都是当前的代码块
synchronized(object)表示进入同步代码块前要获得 给定对象的锁。synchronized(类.class)表示进入同步代码块前要获得 给定 Class 的锁
synchronized(this) {
//业务代码
}
Q3:synchronized底层原理:
修饰同步语句块:
属于jvm层面,主要是获取对象监视器(monitor)的特权,其中monitorenter指令指向同步代码块的开始位置,monitorexit指向结束位置。每次执行monitorenter时会尝试获取对象锁,并查看是否为0,若不为0获取锁失败,并且由持有线程执行monitorexit来释放锁。
修饰方法:
JVM通过ACC_SYNCHRONIZED标识来判别方法是否是同步方法,而不再使用上述的指令。
不管修饰哪一种,本质上都是要获取对象监视器的特权
2.1.2ReentrantLock类
这是一个实现了Lock接口的类,是一个可重入且独占式的锁;代码中有一个内部类Sync,他继承了AQS,获得锁和释放锁的操作都是在Sync中实现的,可以看到Sync有公平锁和非公平锁两个子类,默认情况下ReentranrLock使用非公平锁。

图源:JavaGuide
Q4:Synchronized和ReentrantLock有什么区别
- 可重入性:这两个都是可重入的锁,由Lock实现的和Synchronized关键字都是可重入锁
- 实现依赖:Synchronized依赖的是JVM,ReentrantLock依赖的是JDK、
- 功能区别:前者只能是非公平锁而后者可以实现公平锁;后者支持超时,可以采用trylock(timeout)方法制定等待锁的最长时间;后者等待获取锁可被中断,通过lock.lockInterruptibly()实现;后者的通知机制更强大,可以实现分组唤醒线程或者选择性通知,而前者指鞥随机或者全部唤醒
2.1.3读写锁ReentrantReadWriteLock
简单了解即可,没怎么用过
实现的是ReadWriteLock,是一种可重入的读写锁,支持读操作共享,写操作线程安全
主要包含两把锁:WriteLock和ReadLock,写锁是独占锁,读锁是共享锁

图源:JavaGuide
可以看到ReentrantReadWriteLock也一样支持公平和非公平锁,默认使用非公平锁。
要注意的是,如果一个线程已经持有读锁,那它是无法再获得写锁的,理由很简单,因为在读的时候,不管是不是当前线程,都不会执行写操作;反之,如果持有写锁是可以再拿读锁的,当然前提是同一个线程
关于Java原生中的锁主要就介绍这三个,至于StampedLock这些,因为不怎么使用,就不多作介绍,下一节主要是讲分布式锁的使用。感谢阅读,内容由个人背八股之余进行总结,希望大家指出文中错误或不严谨的地方。
更多推荐




所有评论(0)