Java Lock 接口
艿艿:虽然 Lock 也翻译成锁,但是和上面的 「Java 锁」 分开,它更多强调的是 synchronized 和 volatile 关键字带来的重量级和轻量级锁。而 Lock 是 Java 锁接口,提供了更多灵活的功能。
Lock
Java AQS
java.util.concurrent.locks.AbstractQueuedSynchronizer 抽象类,简称 AQS ,是一个用于构建锁和同步容器的同步器。事实上concurrent 包内许多类都是基于 AQS 构建。例如 ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,等。AQS 解决了在实现同步容器时设计的大量细节问题。
AQS 使用一个 FIFO 的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态 waitStatus 。
可能这么说,胖友会一脸懵逼,最好的方式,还是直接去撸源码,可见如下的四篇文章。
可能胖友在阅读时,会有一定的挫败感,没关系,大家都是如此,包括艿艿,还有我认识的各种大佬。
- 《【死磕 Java 并发】—– J.U.C 之 AQS:AQS 简介》
- 《【死磕 Java 并发】—– J.U.C 之 AQS:CLH 同步队列》
- 《【死磕 Java 并发】—– J.U.C 之 AQS:同步状态的获取与释放》
- 《【死磕 Java 并发】—– J.U.C 之 AQS:阻塞和唤醒线程》
什么是 Java Lock 接口?
java.util.concurrent.locks.Lock 接口,比 synchronized 提供更具拓展行的锁操作。它允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。它的优势有:
- 可以使锁更公平。
- 可以使线程在等待锁的时候响应中断。
- 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间。
- 可以在不同的范围,以不同的顺序获取和释放锁。
什么是可重入锁(ReentrantLock)?
举例来说明锁的可重入性。代码如下:
1
public class UnReentrant{ Lock lock = new Lock(); public void outer() { lock.lock(); inner(); lock.unlock(); } public void inner() { lock.lock(); //do something lock.unlock(); }}
- #outer() 方法中调用了 #inner() 方法,#outer() 方法先锁住了 lock ,这样 #inner() 就不能再获取 lock 。
- 其实调用 #outer() 方法的线程已经获取了 lock 锁,但是不能在 #inner() 方法中重复利用已经获取的锁资源,这种锁即称之为不可重入。
- 可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。
synchronized、ReentrantLock 都是可重入的锁,可重入锁相对来说简化了并发编程的开发。
关于 ReentrantLock 类,详细的源码解析,可以看看 《【死磕 Java 并发】—– J.U.C 之重入锁:ReentrantLock》 。
简单来说,ReenTrantLock 的实现是一种自旋锁,通过循环调用 CAS 操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。
🦅 synchronized 和 ReentrantLock 异同?
- 相同点
- 都实现了多线程同步和内存可见性语义。
- 都是可重入锁。
- 不同点
- 同步实现机制不同
- synchronized 通过 Java 对象头锁标记和 Monitor 对象实现同步。
- ReentrantLock 通过CAS、AQS(AbstractQueuedSynchronizer)和 LockSupport(用于阻塞和解除阻塞)实现同步。
- 可见性实现机制不同
- synchronized 依赖 JVM 内存模型保证包含共享变量的多线程内存可见性。
- ReentrantLock 通过 ASQ 的 volatile state 保证包含共享变量的多线程内存可见性。
- 使用方式不同
- synchronized 可以修饰实例方法(锁住实例对象)、静态方法(锁住类对象)、代码块(显示指定锁对象)。
- ReentrantLock 显示调用 tryLock 和 lock 方法,需要在 finally 块中释放锁。
- 功能丰富程度不同
- synchronized 不可设置等待时间、不可被中断(interrupted)。
- ReentrantLock 提供有限时间等候锁(设置过期时间)、可中断锁(lockInterruptibly)、condition(提供 await、condition(提供 await、signal 等方法)等丰富功能
- 锁类型不同
- synchronized 只支持非公平锁。
- ReentrantLock 提供公平锁和非公平锁实现。当然,在大部分情况下,非公平锁是高效的选择。
- 同步实现机制不同
在 synchronized 优化以前,它的性能是比 ReenTrantLock 差很多的,但是自从 synchronized 引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用 synchronized 。
并且,实际代码实战中,可能的优化场景是,通过读写分离,进一步性能的提升,所以使用 ReentrantReadWriteLock 。😝
ReadWriteLock 是什么?
ReadWriteLock ,读写锁是,用来提升并发程序性能的锁分离技术的 Lock 实现类。可以用于 “多读少写” 的场景,读写锁支持多个读操作并发执行,写操作只能由一个线程来操作。
ReadWriteLock 对向数据结构相对不频繁地写入,但是有多个任务要经常读取这个数据结构的这类情况进行了优化。ReadWriteLock 使得你可以同时有多个读取者,只要它们都不试图写入即可。如果写锁已经被其他任务持有,那么任何读取者都不能访问,直至这个写锁被释放为止。
ReadWriteLock 对程序性能的提高主要受制于如下几个因素:
- 数据被读取的频率与被修改的频率相比较的结果。
- 读取和写入的时间
- 有多少线程竞争
- 是否在多处理机器上运行
ReadWriteLock 的源码解析,可以看看 《【死磕 Java 并发】—– J.U.C 之读写锁:ReentrantReadWriteLock》 。
Condition 是什么?
在没有 Lock 之前,我们使用 synchronized 来控制同步,配合 Object 的 #wait()、#notify() 等一系列方法可以实现等待 / 通知模式。在 Java SE 5 后,Java 提供了 Lock 接口,相对于 synchronized 而言,Lock 提供了条件 Condition ,对线程的等待、唤醒操作更加详细和灵活。下图是 Condition 与 Object 的监视器方法的对比(摘自《Java并发编程的艺术》):
Condition 与 Object 的监视器方法的对比
- Condition 的使用,可以看看《怎么理解 Condition》
- Condition 的源码,可以看看 《【死磕 Java 并发】—– J.U.C 之 Condition》 。
🦅** 用三个线程按顺序循环打印 abc 三个字母,比如 abcabcabc ?**
- 使用 Lock + Condition 来实现。具体代码,参看 《用三个线程按顺序循环打印 abc 三个字母,比如 abcabcabc》 。
- 使用 synchronized + await/notifyAll 来实现,参看 《Java用三个线程按顺序循环打印 abc 三个字母,比如 abcabcabc》 。
LockSupport 是什么?
LockSupport 是 JDK 中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞。
- Java 锁和同步器框架的核心 AQS(AbstractQueuedSynchronizer),就是通过调用 LockSupport#park()和 LockSupport#unpark() 方法,来实现线程的阻塞和唤醒的。
- LockSupport 很类似于二元信号量(只有 1 个许可证可供使用),如果这个许可还没有被占用,当前线程获取许可并继续执行;如果许可已经被占用,当前线程阻塞,等待获取许可。
对于 LockSupport 了解即可,面试一般问的不多。感兴趣的胖友,可以看看如下文章:

