Java 线程总结(六) —— 显式锁
ReentrantLock 锁相关
注意:在学习显示锁的时候要跟 synchronized 锁对比的学,synchronized 锁是由 JVM 去实现的,而显示锁是由我们来实现的,但是它们的功能都很类似,都可以对比着来学的。线程也会去争夺显示锁,争夺不到的时候也可能会阻塞,当前线程也会被放到等待队列中。
1、Java 并发包中的显式锁接口和类位于包 java.util.concurrent.locks 下,主要接口和类有:
锁接口 Lock,主要实现类是 ReentrantLock。
读写锁接口 ReadWriteLock,主要实现类是 ReentrantReadWriteLock。
相比 synchronized,显式锁支持以非阻塞方式获取锁、可以响应中断、可以限时,这使得它灵活的多。
2、可重入锁 ReentrantLock
Lock 接口的主要实现类是 ReentrantLock,它的基本用法 lock/unlock 实现了与 synchronized 一样的语义,包括:
可重入,一个线程在持有一个锁的前提下,可以继续获得该锁。
可以解决竞态条件问题。
可以保证内存可见性。
public ReentrantLock(boolean fair)
参数 fair 表示是否保证公平,不指定的情况下,默认为 false,表示不保证公平。所谓公平是指,等待时间最长的线程优先获得锁。保证公平会影响性能,一般也不需要,所以默认不保证,synchronized 锁也是不保证公平的。
使用显式锁,一定要记得调用 unlock。 一般而言,应该将 lock 之后的代码包装到 try 语句内,在 finally 语句内释放锁。
3、ReentrantLock 的实现原理
在最底层,它依赖于 CAS 方法,另外,它依赖于类 LockSupport 中的一些方法。
LockSupport 锁相关
4、类 LockSupport 也位于包 java.util.concurrent.locks 下。public static void park()
park 方法使得当前线程放弃 CPU,进入等待状态(WAITING),操作系统不再对它进行调度,只有其他线程对它调用了 unpark,unpark 需要指定一个线程,unpark 会使之恢复可运行状态。
LockSupport.park() 不同于 Thread.yield(),yield 只是告诉操作系统可以先让其他线程运行,但自己依然是可运行状态,而 park 会放弃调度资格,使线程进入 WAITING 状态。park 是响应中断的,当有中断发生时,park 会返回,线程的中断状态会被设置。另外,需要说明一下,park 可能会无缘无故的返回,程序应该重新检查 park 等待的条件是否满足。
park/unpark 方法的底层实现调用了 Unsafe 类中的对应方法,Unsafe 类最终调用了操作系统的 API,从程序员的角度,我们可以认为 LockSupport 中的这些方法就是基本操作。
AQS 相关
AQS:AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如 ReentrantLock、ReentrantReadWriteLock、Semaphore 等),JUC 并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。它是 JUC 并发包中的核心基础组件。AQS 解决了实现同步器时涉及到的大量细节问题,例如获取同步状态、FIFO 同步队列。在基于 AQS 构建的同步器中,只能在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量。AQS 的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。AQS 的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。AbstractQueuedSynchronizer 为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。
5、AQS (AbstractQueuedSynchronizer)
利用 CAS 和 LockSupport 提供的基本方法,就可以用来实现 ReentrantLock 了。但 Java 中还有很多其他并发工具,如 ReentrantReadWriteLock、Semaphore、CountDownLatch,它们的实现有很多类似的地方,为了复用代码,Java 提供了一个抽象类 AbstractQueuedSynchronizer,我们简称为 AQS。
用于实现锁时,AQS 可以保存锁的当前持有线程,提供了方法进行查询和设置:
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread t);
protected final Thread getExclusiveOwnerThread();
AQS 内部维护了一个等待队列,借助 CAS 方法实现了无阻塞算法进行更新。等待队列是 FIFO 先进先出,只有前一个节点的状态为 SIGNAL 时,当前节点的线程才能被挂起。子类重写 tryAcquire 和 tryRelease 方法通过 CAS 指令修改状态变量 state。
6、保证公平整体性能比较低,低的原因不是这个检查慢,而是会让活跃线程得不到锁,进入等待状态,引起上下文切换,降低了整体的效率,通常情况下,谁先运行关系不大,而且长时间运行,从统计角度而言,虽然不保证公平,也基本是公平的。
需要说明是,即使 fair 参数为 true,ReentrantLock 中不带参数的 tryLock 方法也是不保证公平的,它不会检查是否有其他等待时间更长的线程。
7、ReentrantLock 对比 synchronized
相比 synchronized,ReentrantLock 可以实现与 synchronized 相同的语义,但还支持以非阻塞方式获取锁、可以响应中断、可以限时等,更为灵活。不过,synchronized 的使用更为简单,写的代码更少,也更不容易出错。
synchronized 代表一种声明式编程,程序员更多的是表达一种同步声明,由 Java 系统负责具体实现,程序员不知道其实现细节,显式锁代表一种命令式编程,程序员实现所有细节。
声明式编程的好处除了简单,还在于性能,在较新版本的 JVM 上,ReentrantLock 和 synchronized 的性能是接近的,但 Java 编译器和虚拟机可以不断优化 synchronized 的实现,比如,自动分析 synchronized 的使用,对于没有锁竞争的场景,自动省略对锁获取/释放的调用。
8、显式锁 ReentrantLock 使用 CAS、LockSupport 和 AQS 实现的。
9、总结:能用 synchronized 就用 synchronized,不满足要求,再考虑 ReentrantLock。