之前我们学习了synchronized在偏向锁、轻量级锁、重量级锁下的不同的原理(并发基础之Synchronized原理),这篇文章我们来讲讲另一个锁Lock的原理。
java除了使用关键字synchronized外,还可以使用ReentrantLock实现独占锁的功能。而且ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。这篇文章主要是从使用的角度来分析一下ReentrantLock。
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。关于CAS,请看:并发基础之CAS
Lock和synchronized的区别
首先synchronized是java内置关键字,在jvm层面,Lock是个接口;
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁(lock.isHeldByCurrentThread());
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
ReentrantLock原理
基本使用:
private Lock lock = new ReentrantLock();
public void test(){
lock.lock();
try{
doSomeThing();
}catch (Exception e){
// ignored
}finally {
lock.unlock();
}
}
ReentrantLock是AQS的一个实现,本身没编写多少代码,主要的功能还是AQS来提供的,所以应该重点关注AQS。
ReentrantLock支持公平锁和非公平锁,并且ReentrantLock的底层就是由AQS来实现的。
AQS 全称是 AbstractQueuedSynchronizer,是一个用来构建「锁」和「同步器」的框架,它维护了一个共享资源 state 和一个 FIFO 的等待队列(即上文中管程的入口等待队列),底层利用了 CAS 机制来保证操作的原子性。
以实现独占锁为例(即当前资源只能被一个线程占有),其实现原理如下:
「state」 初始化 0,在多线程条件下,线程要执行临界区的代码,必须首先获取 state,某个线程获取成功之后,state加 1,其他线程再获取的话由于共享资源已被占用,所以会到 「FIFO」 等待队列去等待,等占有state的线程执行完临界区的代码释放资源(state 减 1)后,会唤醒 FIFO 中的下一个等待线程(head 中的下一个结点)去获取state。
「state」 由于是多线程共享变量,所以必须定义成 「volatile」,以保证 state的可见性,同时虽然 volatile 能保证可见性,但不能保证原子性,所以 AQS 提供了对state的原子操作方法,保证了线程安全。
另外 AQS 中实现的 FIFO 队列(「CLH」 队列)其实是双向链表实现的,由 head、tail 节点表示,head 结点代表当前占用的线程,其他节点由于暂时获取不到锁所以依次排队等待锁释放。