天天看点

5.Lock接口及其实现ReentrantLock

jdk1.7.0_79

  在java.util.concurrent.locks这个包中定义了和synchronized不一样的锁,重入锁——ReentrantLock,读写锁——ReadWriteLock等。在已经有了内置锁synchronized的情况下,为什么又出现了Lock显示锁呢?本文将以Lock作为Java并发包源码解读的开始.

  Lock定义最基本的加锁和解锁操作。

  

Lock 

void lock(); 

阻塞方式获取锁,直到获取锁后才返回 

void locklnterruptibly(); 

获取锁,除非当前线程被中断 

Condition newCondition(); 

返回一个Condition实例绑定到这个锁实例 

boolean tryLock(); 

不管是否获取到锁,都立即返回,非阻塞 

boolean tryLock(long time, TimeUnit unit); 

在一定时间内阻塞获取锁 

void unlock(); 

释放锁 

  Lock接口有一个实现类——重入锁ReentrantLock。进入ReentrantLock类中我们就发现它对于Lock接口的实现基本上都借助于一个抽象静态内部类Sync,该内部类继承自AbstractQueuedSynchronizer,接着又发现两个静态内部类NonfairSync、FairSync,这两个静态内部类又是继承自刚刚的Sync。这里就要引入两个新的概念了——公平锁与非公平锁。在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁。(《Java并发编程实战》)

  ReentrantLock你可以称之为重入锁(递归锁)、显示锁、排他锁(独占锁),显示锁很好理解,即线程在获取锁和释放锁的时候都需要代码显示操作。重入锁是什么概念呢?synchronized实际上也是可重入锁,意思就是一个线程已经持有这个锁,当这个线程再次获得这个锁的时候不会被阻塞,而是使其同步状态计数器+1,这样做的目的当然就是为了防止死锁,当线程释放这个锁的时候,同步状态计数器-1,直到递减至0才表示这个锁完全释放完毕,其他线程可以获取。那什么又是排他锁呢?我们直到AQS定义了两种模式下获取锁与释放锁的操作,那就是独占模式和共享模式,所谓独占模式就是只有一个线程能持有这个锁,而共享模式则是这个锁可以由多个线程所持有。例如ReebtrabtReadWriteLock的读锁就能由多个线程所持有。在知道了ReentrantLock的特性之后,我们再来看它其内部实现。 

  在前两节解析AQS的时候我们就提到,AQS所提供的同步器是实现锁的基础框架,固然ReentrantLock同样也是基于AQS,而ReentrantLock并没有直接实现AQS抽象类,而是将在其内部定义一个Sync内部类来聚合AQS,这样聚合而不是继承的目的是为了将锁的具体实现与锁的使用做一个隔离,锁的使用者关心的是锁如何才能被正确使用,而锁的实现者关心的是锁如何基于AQS被正确的实现。先讨论ReentrantLock$Sync抽象内部类。在讨论前先回顾一下能够被子类重写的AQS方法有哪些: 

AbstractQueuedSynchronizer 

protected boolean tryAcquire(int arg) 

子类可实现在独占模式下获取同步状态的具体方法。 

protected boolean tryRelease(int arg) 

子类可实现在独占模式下释放同步状态的具体方法。 

protected int tryAcquireShared(int arg) 

子类可实现在共享模式下获取同步状态的具体方法。 

protected int tryReleaseShared() 

子类可实现在共享模式下释放同步状态的具体方法。 

protected boolean isHeldExclusively() 

当前同步器是否在独占模式下被线程占用,一般表示该方法是否被当前线程所独占。 

  通过查看ReentrantLock$Sync的源码可知,Sync一共对AQS重写了这么几个方法: 

  为什么Sync只重写了这两个方法呢?实际上在ReentrantLock的内部还有另外两个内部类NonfairSync非公平锁和FairSync公平锁,这两个内部类是Sync的具体实现,很显然能够得出,对于锁的获取非公平锁和公平锁的实现是不一样的,而对于锁的释放两者均是相同实现。针对ReentrantLock的非公平锁和公平锁接下来我们来一一探讨他们的不同点和相同点。 

  在ReentrantLock定义了一个成员变量——sync,并且他提供了两个构造方法,其默认无参构造方法创建的是非公平锁,而有参的构造方法则传入一个boolean类型来决定构造一个公平锁还是非公平锁。 

  针对开篇提到的Lock接口定义的方法,我们先来看ReentrantLock对Lock#lock的实现: 

  这个方法是抽象内部类定义的一个抽象方法,从命名可以看到这个类实际上就是AQS的acquire获取锁的具体实现,在这里我们能看到非公平锁和公平锁对获取锁的不同实现,我们先来看非公平锁对Sync#lock的实现: 

  ReentrantLock$Sync中非公平锁的获取锁的实现 

  我们说回ReentrantLock$NonFairLock非公平锁的lock阻塞获取锁的实现,在调用非公平锁的lock方法时,就会首先进行“抢锁”操作,也就是compareAndSetState这个方法是利用的CAS底层方法看能否抢占到锁,而不是按照先后获取获取锁的方式放到同步队列中获取锁,公平锁就是这样。既然说到了公平锁获取锁的方式,我们不妨和ReentrantLock$FairLock作一个对比: 

  从公平锁(nonfairTryAcquire)和非公平锁(tryAcquire)的两个方法对比可知:公平锁在获取锁的时候首先会判断当前线程是否有前驱节点已经在队列中等待,如果有返回true,则不进行获取锁的操作,这一点就是和公平锁最大的区别,只有当前线程没有前驱节点才获取锁。ReentrantLock默认构造非公平锁,而实际上用得最多的也是非公平锁,公平锁从一定程度上能防止“饥饿”,但非公平锁在性能上却优于公平锁,我们做以下试验得知的确如此(详细试验过程《【试验局】ReentrantLock中非公平锁与公平锁的性能测试》) 

这个方法和lock方法的区别就是,lock会一直阻塞下去直到获取到锁,而lockInterruptibly则不一样,它可以响应中断而停止阻塞返回。ReentrantLock对其的实现是调用的Sync的父类AbstractQueuedSynchronizer#acquireInterruptibly方法: 

  通过查看doAcquireInterruptibly的方法实现不难发现它和acquireQueued大同小异,前者抛出异常,后者返回boolean。具体实在不再讨论,参照源码以及《2.从AbstractQueuedSynchronizer(AQS)说起(1)——独占模式的锁获取与释放》。 

  此方法为非阻塞式的获取锁,不管有没有获取锁都返回一个boolean值。 

  可以看到它实际调用了Sync#nonfairTryAcquire非公平锁获取锁的方法,这个方法我们在上文lock()方法非公平锁获取锁的时候有提到,而且还特地强调了该方法不是在NonfairSync实现,而是在Sync中实现很有可能这个方法是一个公共方法,果然在非阻塞获取锁的时候调用的是此方法。详细解析参照上文。 

此方法是表示在超时时间内获取到同步状态则返回true,获取不到则返回false。由此可以联想到AQS的tryAcquireNanos(int arg, long nanosTimeOut)方法 

  果然Sync实际上调用了父类AQS的tryAcquireNanos方法。 

  在doAcquireNanos实际也和acquireQueued、doAcquireInterruptibly差不多,不同的是增加了超时判断。 

  关于Lock和ReentrantLock介绍到这里,在AQS和这里遗留了一个问题——Condition,在下一节中单独介绍Condition。

本文转自zsdnr  51CTO博客,原文链接:http://blog.51cto.com/12942149/1929697,如需转载请自行联系原作者