synchronized和Lock

加锁/解锁
ReentrantLock
ReentrantLock是一个在语义和功能上和synchronized显式获取对象监视锁类似的可重入互斥锁。
可通过构造函数指定公平锁或非公平锁两种模式,默认为非公平锁,即新线程可能会与队列中的线程抢锁,不能保证先来先服务。
ReentrantLock底层通过AQS实现,即通过CAS对state进行修改操作,如果成功将state从0修改为1,则表示该线程成功获取锁。

ReentrantLock源码中方法并不多,有加锁解锁的基础API,还提供了判断当前线程是否持有锁、获取当前持有锁的线程引用等方法。除此之外,就是一些获取锁状态的方法,例如是否是公平锁、是否有等待队列(线程阻塞)、获取等待队列长度等。
我们来看一下比较重要的基础API。

如上图是加锁的操作,可以看到,是通过sync变量来实现的。

sync作为AQS抽象队列同步器的抽象子类实现,定义了一些获取锁的基本方法,可以看到,这些方法基本上是和ReetrantLock的方法相同的,即ReentrantLock是通过该实现类完成加锁解锁的一系列操作的。

通过构造函数指定创建NonfairSync或FairSync来确定ReentrantLock是否是公平锁。
lock()
NonfairSync
我们先看默认的非公平锁实现。

调用lock()方法后,非公平锁首先会尝试通过CAS将state从0该成1,如果改成功了,则将AbstractOwnableSynchronizer的持有线程改为自己,这其实就是底层的拿锁逻辑。否则会正常的尝试加锁,调用AQS的acquire(int)方法,进行尝试加锁操作,如果失败,则加入到阻塞队列中。

实际tryAcquire(int)方法被抽象的子类Sync实现了。

FairSync
接下来看一下公平锁的实现。

公平锁的加锁实现其实和非公平锁是很像的,只不过加了一个判断等待队列中,该线程之前是否存在线程,如果不存在,才尝试去加锁。
unlock()
公平锁与非公平锁的加锁操作会有一些不同,但解锁操作两者相同,只需要将state状态-1即可。

我们先来看一下Sync(AQS)中的release操作。解锁操作逻辑就是将state恢复到0后,尝试唤醒队列中的下一个线程。

其中tryRelease(int)操作是交给AQS实现类,也就是ReentrantLock的内部实现类Sync实现的,代码如下。

从代码里可以看到,如果没有把重入次数完全释放,tryRelease方法是不会返回true的,因此release方法中也就不会去唤醒下一个线程了,直到当前持锁线程将锁完全释放后,才会通知下一个线程进行加锁操作。因此,在我们平时使用ReentrantLock时,unlock()必须要一一对应lock(),最好使用try-finally来保证每个lock()操作都被正常解锁。
synchronized
synchronized对于同步代码块,是通过对象的monitor锁实现。从javap -c命令反编译的结果来看,能在同步代码块前后找到monitorenter和monitoexit两条语句。

获取这个monitor对象锁的操作,在逻辑上和ReentrantLock是类似的,也是可重入,通过重入次数来判断是否存在锁。
而对于同步方法来说,反编译的代码中并没有monitorenter和monitorexit两个命令,其是通过ACC_SYNCHRONIZED标识符对方法进行标识,标识该方法为同步方法来实现。

两者异同
synchronized关键字是语言层面的关键字,依赖于JVM实现。这也是synchronized和ReentrantLock最大的区别:synchronized是JVM层面,而ReetrantLock是JDK层面,即API层面。
两者都是可重入锁,都能被同一个线程重入地获取多次,但也必须对应的释放多次(ReentrantLock需要特别注意,synchronized关键字则不需要操心)。
基于ReentrantLock是API层面,那么扩展性肯定是更好的。ReentrantLock比synchronized关键字多了一些高级功能,例如可中断(lockInterruptibley方法)、公平锁、condition通知等。这些高级特性将放在后面进行介绍。