Java中的锁分类
乐观锁&悲观锁
乐观锁:持乐观态度,认为并发冲突的概率较低,在提交时检查数据是否被修改,若未被修改则提交,否则重试或抛出异常。
例如 版本号法、CAS(compare and swap),即乐观锁实现
应用场景:
①适合读多写少(并发冲突的概率低),可以降低锁定带来的性能开销
②系统能够容忍或者处理失败的情况,因为乐观锁可能会因为并发冲突导致执行失败。
=》并发性能高
悲观锁:持悲观态度,认为并发冲突的概率高,每次操作数据时都加锁,确保线程安全。
例如 synchronized,即悲观锁实现
应用场景:
①适合写多读少(并发冲突的概率高),避免频繁冲突导致的多次重试
②数据一致性要求极高,如金融系统
=》并发性能低(串行化操作)
按 synchronized 的优化过程:无锁 → 偏向锁(JDK15 后已移除)→ 轻量级锁 → 重量级锁,锁只能升级不能降级。
CAS 原理
Compare And Swap(比较再交换):在更新前检查数据是否被修改,若未被修改则更新,否则自旋重试。
具体解释:主存中有一个共享变量,当线程想要去修改这个变量的值的时候,需要先把这个变量的值拷贝到线程的工作内存中,然后进行计算操作,得到一个新的值。然后线程带着旧值和新值再次找到主存中的共享变量。先比较旧值和现在主存中的共享变量是否相同。
若相同,说明在这期间没有其他线程修改过主存中的共享变量,是线程安全的。因此把新值写入主存的共享变量中。
若不同,说明在这期间有其他线程修改过主存中的共享变量,不是线程安全的。因此此时需要自旋重试。
自旋:再次取得主存中共享变量的值。回到线程的工作内存进行计算操作得到新值。再次比较旧值和主存中的共享变量的值是否相同。又回到刚刚的逻辑。直至自旋成功。
-
优点:不加锁,无锁并发保存线程安全。
-
缺点:自旋占用CPU。
Synchronized原理
背景知识:每个Java对象头部都有一个MarkWord,存储着对象的运行时数据,如哈希码、锁状态信息、GC分代年龄等,锁状态信息标志了Synchronized的状态
①在JVM字节码层面,synchronized 对应的是 monitorenter 和 monitorexit 两条字节码指令,表示进入临界区,告诉JVM在此处需要加锁和解锁。
②在JVM逻辑层面,JVM内部通过 Monitor(监视器)来管理线程的竞争和等待
每个锁对象都关联一个Monitor,内部通过逻辑区域管理线程竞争,具体流程:
当线程执行 monitorenter 时,JVM会检查 锁对象 的Monitor。
若 Owner 为空,线程成为 Owner;若 Owner 被占用,线程进入 EntryList 阻塞。
当线程执行 monitorexit 时,JVM释放锁,并唤醒 EntryList 中的线程,让它们去竞争锁。
③在操作系统层面,需要从用户态切换到内核态,向操作系统底层申请互斥量mutex,性能开销很大,因此称其为重量级锁。
锁升级过程(JDK1.6对synchronized进行了优化)
无锁->偏向锁
最初是无锁的状态,当一个synchronized代码块被线程首次进入时,JVM会将锁标记为偏向锁,且会在锁对象头中记录下该线程的ID。如果该线程再次请求同一把锁(即重入锁),通过比较线程ID它会直接获得锁,是为了没有锁竞争且需要重入的场景而引入的。
偏向锁->轻量级锁
如果有其他线程来请求获取锁,此时偏向锁就会被撤销,两个线程会通过CAS的方式自旋尝试获取锁,获取锁后JVM会将锁标记为轻量级锁。
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
轻量级锁->重量级锁
在轻量级锁状态下继续锁竞争,如果成功就成功获取轻量级锁。否则进入锁膨胀阶段,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁。
AQS 原理
AQS是多线程同步器,他是JUC包中多个组件的底层实现,比如说Lock、CountDownLatch都是用到了AQS。从本质上来说,AQS实现了两种锁机制,分别是排他锁和共享锁。所谓排他锁就是存在多个线程只能有一个线程去获得锁的资源,比如Lock中的ReentrantLock重入锁。共享锁称为读锁,在同一个时刻允许多个线程同时获得锁的资源,比如CountDownLatch。
AQS作为互斥锁来说,需要解决两个核心问题:
①互斥变量的设计以及多线程同时更新互斥变量的时候线程的安全性。
②未竞争到锁资源的等待以及竞争到锁资源释放锁之后的唤醒。
AQS采用了一个int类型的互斥变量state用来记录锁竞争的状态,0表示当前没有线程竞争到锁资源,而大于等于1表示已经有线程持有锁资源。一个线程来获取锁资源的时候首先会判断state是否等于0,也就是它说是无锁状态,如果是,把state更新为1表示占用到锁,而这个过程中多个线程进行这个操作,就会导致线程安全性问题,因此AQS采用了CAS机制去保证state互斥变量更新的一个原子性,未获取到锁的线程通过Unsafe类中的park方法进行阻塞,把阻塞的线程利用先进先出的原则去加入到一个双向链表的结构中,当获得锁资源的线程在释放锁之后,会从双向链表的头部去唤醒下一个等待的线程再去竞争锁。
synchronized 与 ReentrantLock 的区别
synchronized 和 ReentrantLock 都是可重入的独占锁,但实现方式不同。synchronized 是 JVM 提供的 Monitor 机制,使用简单,进入和退出同步块时由 JVM 自动加锁和释放锁;ReentrantLock 基于 AQS 实现,需要手动调用 lock() 和 unlock()。
功能上,ReentrantLock 更丰富,它支持公平锁、可中断获取锁、超时获取锁、tryLock(),以及通过多个 Condition 实现多条件等待;而 synchronized 不支持这些高级特性,只能通过 wait/notify 使用一个等待队列。