多线程 之 Lock 锁的实现原理

1. Lock 的简介及使用


Lock完全用Java写成,在java这个层面是无关JVM实现的,Lock 接口主要有一下实现


//尝试获取锁,获取成功则返回,否则阻塞当前线程
void lock(); 

//尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常 
void lockInterruptibly() throws InterruptedException; 

//尝试获取锁,获取锁成功则返回true,否则返回false 
boolean tryLock(); 

//尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常 
boolean tryLock(long time, TimeUnit unit) 
                                   throws InterruptedException; 

//释放锁
void unlock(); 

//返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
Condition newCondition();



2. 实现Lock接口的基本思想(AQS、CAS)


AQS : AbstractQueuedSynchronized 核心概念:一个是表示(锁)状态的变量、一个是队列

多线程 之 Lock 锁的实现原理_第1张图片
C AS: 简单的来说,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。 当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V 。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。缺点:会产生ABA问题,即A被修改成B,然后又被修改成A,不能感知到修改


3.ReentrantLock具体实现


在java.util.concurrent.locks包中有很多Lock的实现类,常用的有ReentrantLock、ReadWriteLock(实现类ReentrantReadWriteLock),其实现都依赖java.util.concurrent.AbstractQueuedSynchronizer类,实现思路都大同小异,因此我们。以ReentrantLock作为讲解切入点。



ReentrantLock的Lock过程

ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer(简称AQS)


多线程 之 Lock 锁的实现原理_第2张图片

1、在初始化ReentrantLock的时候,如果我们不传参数是否公平,那么默认使用非公平锁,也就是NonfairSync。

2、当我们调用ReentrantLock的lock方法的时候,实际上是调用了NonfairSync的lock方法,这个方法先用CAS操作,去尝试抢占该锁。

如果成功,就把当前线程设置在这个锁上,表示抢占成功。如果失败,则调用acquire模板方法,等待抢占。

3、调用acquire(1)实际上使用的是AbstractQueuedSynchronizer的acquire方法,它是一套锁抢占的模板,总体原理是先去尝试获取锁,

如果没有获取成功,就在CLH队列中增加一个当前线程的节点,表示等待抢占。然后进入CLH队列的抢占模式,进入的时候也会去执行一

次获取锁的操作,如果还是获取不到,就调用LockSupport.park将当前线程挂起。当持有锁的那个线程调用unlock的时候,会将CLH

队列的头节点的下一个节点上的线程唤醒,调用的是LockSupport.unpark方法。


ReentrantLock的UnLock过程


多线程 之 Lock 锁的实现原理_第3张图片


1、调用unlock方法,其实是直接调用AbstractQueuedSynchronizer的release操作。

2、进入release方法,内部先尝试tryRelease操作,主要是去除锁的独占线程,然后将状态减一,这里减一主要是考虑到可重入锁可能自身会

多次占用锁,只有当状态变成0,才表示完全释放了锁。

3、一旦tryRelease成功,则将CHL队列的头节点的状态设置为0,然后唤醒下一个非取消的节点线程。

4、一旦下一个节点的线程被唤醒,被唤醒的线程就会进入acquireQueued代码流程中,去获取锁。


线程使用ReentrantLock获取锁分为两个阶段,第一个阶段是初次竞争,第二个阶段是基于CHL队列的竞争。在初次竞争的时候是否考虑队列节点直接区分出了公平锁和非公平锁。在基于CHL队列的锁竞争中,依靠CAS操作保证原子操作,依靠LockSupport来做线程的挂起和唤醒,使用队列来保证并发执行变成了串行执行,从而消除了并发所带来的问题。总体来说,ReentrantLock是一个比较轻量级的锁,而且使用面向对象的思想去实现了锁的功能,比原来的synchronized关键字更加好理解


4. Lock 与Synchronized的区别

1、synchronized内置关键字,在JVM层面实现,发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

2、Lock具有高级特性:时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票,可以知道有没有成功获取锁

3、当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized




结论: 建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用  ReentrantLock  “性能会更好”。


在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的Lock对象,性能更高一些。Brian Goetz对这两种锁在JDK1.5、单核处理器及双Xeon处理器环境下做了一组吞吐量对比的实验,发现多线程环境下,synchronized的吞吐量下降的非常严重,而ReentrankLock则能基本保持在同一个比较稳定的水平上。但与其说ReetrantLock性能好,倒不如说synchronized还有非常大的优化余地,于是到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。



你可能感兴趣的:(多线程)