6.5 KiB
目录
- 思维导图
- 概述
- 常见术语
- 悲观锁/乐观锁
- 公平锁/非公平锁
- 可重入锁/不可重入锁
- 死锁
- synchronized
- 使用方式
- 实现原理
- 锁优化
- CAS
- AQS
- Lock
- ReadWriteLock
- 参考
思维导图
概述
Java 中的并发锁大致上可以分为隐式锁和显式锁两种。隐式锁的代表就是 synchronized 关键字,显式锁主要包含两个接口:Lock 和 ReadWriteLock,主要实现类分别为 ReentrantLock 和 ReentrantReadWriteLock,这两个类都是基于 AQS(AbstractQueuedSynchronizer)队列同步器实现的,还有的地方把 CAS 也称为一种锁,在包括 AQS 在内的很多并发相关类中,CAS 都扮演了很重要的角色,比如 AtomicInteger 等原子操作类。
常见术语
悲观锁和乐观锁
悲观锁和独占锁是一个意思,它假设一定会发生冲突,因此获取到锁之后会阻塞其他等待线程。这么做的好处是简单安全,但是挂起线程和恢复线程都需要转入内核态进行,这样做会带来很大的性能开销。悲观锁的代表是 synchronized,然鹅在真实环境中,大部分时候都不会产生冲突。而乐观锁不一样,它假设不会产生冲突,先去尝试执行某项操作,失败了再进行其他处理(一般都是不断循环重试),这种锁不会阻塞其他线程,也不涉及上下文切换,性能开销小,代表实现是 CAS。
公平锁和非公平锁
公平锁是指各个线程在获取锁前先检查有无排队的线程,按排队顺序去获取锁。非公平锁是指线程获取锁前不考虑排队问题,直接尝试获取锁。值得注意是,在 AQS 的实现中,一旦线程进入排队队列,即使是非公平锁,线程也得乖乖排队。
可重入锁和不可重入锁
如果一个线程已经获取到了锁,那么它可以访问被这个锁锁住的所有代码块,不可重入锁与之相反。可重入锁包括 synchronized 和 ReentrantLock。
死锁
synchronized
悲观、可重入,已经不那么重量级的锁。
使用方式
- 修饰静态方法,锁 class 对象
- 修饰非静态方法,锁当前实例对象
- 修饰代码块,锁的是代码块里的对象
每个对象都有一个锁和一个等待队列,锁只能被一个线程持有,其他需要获取锁的线程需要阻塞等待,锁释放之后,对象会从队列中取出一个线程并唤醒,唤醒哪个线程是不确定的,不保证公平性。
多个线程是可以同时执行同一个 synchronized 实例方法的,只要它们访问的对象是不同的。
synchronized 锁住的是对象而非代码,只要访问的是同一个对象的方法,都得顺序访问。
实现原理
synchronized 是基于 Java 对象头和 Monitor 机制来实现的。
-
Java 对象头
一个对象在内存中包含三个部分:对象头、实例数据、对其填充。其中 Java 对象头包含两个部分:
-
Class Metadata Adderess 类型指针
存储类的元数据的指针,虚拟机通过这个指针找到它是哪个类的实例。
-
Mark Work 标记字段
存储对象自身的运行时数据,包括 hashCode、GC 分代年龄、锁状态标志等。
-
-
Monitor
Mark Word 有一个字段指向 monitor 对象,monitor 中记录了锁的持有线程,等待的线程队列等信息,前面说的每个对象都有一个锁和一个等待队列,就是在这实现的。
那 JVM 是如何把 synchronized 与 monitor 关联起来的呢?可以分为两种情况:
- 同步代码块,编译时会直接在同步代码块前加上 monitorenter 指令,代码块后加上 monitorexit 指令。
- 同步方法,虚拟机会为方法设置 ACC_SYNCHRONIZED 标志,在调用方法的时候根据这个标志判断是否是同步方法。
锁优化
JDK 1.6 对锁的实现引入了大量的优化,包括自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁来减少锁操作的开销。
锁主要存在四种状态:依次是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁可以升级但是不能降级,这种策略是为了提高获取锁和释放锁的效率。
-
重量级锁
重量级锁就是采用互斥量 Mutex 来控制对互斥资源的访问,在 Java 中被抽象为监视器锁 Monitor,这种同步方式成本非常高,包括内核态到用户态切换,性能差。
-
自旋锁与自适应自旋
在许多应用中,锁定状态只会持续很短的时间,为了这么一点时间就去挂起、恢复线程,不值得。我们可以让等待线程执行一定次数的循环,在循环中去获取锁,这项技术称为自旋锁,它可以节省系统切换线程的消耗,但仍然占有处理器。在之后又引入自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
-
锁消除
虚拟机在运行时,如果发现一段被锁住的代码中不可能存在共享数据,就会将这个锁清除。
-
锁粗化
当虚拟机检测到有一串零碎的操作都对同一个对象加锁时,会把锁扩展到整个操作序列外部。如 StringBuffer 的 append 操作。
-
轻量级锁
对绝大部分锁来说,整个同步周期内不存在竞争,如果没有竞争,轻量级锁可以使用 CAS 操作来避免使用互斥量的开销。
-
偏向锁
偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁。
CAS
CAS 即 CompareAndSwap 比较并替换。CAS 机制当中使用了三个基本操作数:内存地址 V,旧的预期值 A,要修改的新值 B。更新一个变量的时候,只有当变量的预期值 A 和内存地址 V 和实际值相同时,才会将内存地址 V 对应的值修改为 B。
但是问题也是显而易见的:
-
ABA 问题
加一个版本号。
-
不能保证代码块的原子性
-
在多次尝试更新一个值时不成功,CPU 开销大
在 Java 并发包中 AtomicInteger、等都是它的实现。