在进行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这些,因为不怎么使用,就不多作介绍,下一节主要是讲分布式锁的使用。感谢阅读,内容由个人背八股之余进行总结,希望大家指出文中错误或不严谨的地方。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐