读本篇前,面试一定要确保已经读过本公众号的官问个离关于AQS讲解。 我们知道实现一把锁要有如下几个逻辑 我们在讲解AQS的时候说过AQS基本负责了实现锁的全部逻辑,唯独线程抢锁和线程释放锁的问题逻辑是交给子类来实现了,而ReentrantLock作为最常用的面试独占锁,其内部就是官问个离关于包含了AQS的子类实现了线程抢锁和释放锁的逻辑。 我们在使用ReentrantLock的的对答时候一般只会使用如下方法 ReentrantLock lock=new ReentrantLock(); lock.lock(); lock.unlock(); lock.tryLock(); Condition condition=lock.newCondition(); condition.await(); condition.signal(); condition.signalAll(); 如果我们自己来实现一个锁,那么如何设计呢?问题 根据AQS的逻辑,我们写一个子类sync,面试这个类一定会调用父类的acquire方法进行上锁,同时重写tryAcquire方法实现自己抢锁逻辑,官问个离关于也一定会调用release方法进行解锁,的对答同时重写tryRelease方法实现释放锁逻辑。问题 那么ReentrantLock是怎么实现的香港云服务器呢? ReentrantLock的实现的类架构如下,ReentrantLock对外提供作为一把锁应该具备的官问个离关于api,比如lock加锁,的对答unlock解锁等等,而它内部真正的实现是通过静态内部类sync实现,sync是AQS的子类,是真正的锁,因为这把锁需要支持公平和非公平的特性,所以sync又有两个子类FairSync和NonfairSync分别实现公平锁和非公平锁。 因为是否公平说的是抢锁的时候是否公平,那两个子类就要在上锁方法acquire的调用和抢锁方法tryAcquire的重写上做文章。 公平锁做了什么文章? static final class FairSync extends Sync { () { acquire(1); } protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); (c == 0) { (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); ; } } (current == getExclusiveOwnerThread()) { int nextc = c + acquires; (nextc < 0) ); setState(nextc); ; } ; } } 公平锁比较简单,直接调用了父级类AQS的acquire方法,因为AQS的站群服务器锁默认就是公平的排队策略。 重写tryAcquire方法的逻辑为: 公平就公平在老老实实排队。 非公平锁做了什么文章? static final class NonfairSync extends Sync { () { (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); acquire(1); } protected final boolean tryAcquire(int acquires) { nonfairTryAcquire(acquires); } } //nonfairTryAcquire代码在父类sync里面 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); (c == 0) { (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); ; } } (current == getExclusiveOwnerThread()) { int nextc = c + acquires; (nextc < 0) // overflow ); setState(nextc); ; } ; } 非公平锁也很简单,没有直接调用了父级类AQS的acquire方法,而是云南idc服务商先通过cas抢锁,它不管等待队列中有没有其他线程在排队,直接抢锁,这就体现了不公平。 它重写tryAcquire方法的逻辑为: 公平锁和非公平分别重写了tryAcquire方法,来满足公平和非公平的特性。那么tryAcquire方法也是需要子类重写的,因为它和是否公平无关,因此tryAcquire方法被抽象到sync类中重写。 sync类中 protected final boolean tryRelease(int releases) { int c = getState() - releases; (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); ; (c == 0) { ; setExclusiveOwnerThread(null); } setState(c); free; } 释放锁的逻辑如下: 释放锁往往和抢锁逻辑是对应的,每个子类抢锁逻辑不同的话,释放锁的逻辑也会对应不同。 接下来我们通过ReentrantLock的使用看下它的源码实现 class X { private final ReentrantLock lock = new ReentrantLock(); Condition condition1=lock.newCondition(); Condition condition2=lock.newCondition(); () { lock.lock(); try { (条件1){ condition1.await(); } (条件2){ condition2.await(); } } catch (InterruptedException e) { } finally { condition1.signal(); condition2.signal(); lock.unlock(); } } } ReentrantLock类 () { sync.lock(); } NonfairSync 类中 () { (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); acquire(1); } FairSync 类中 () { acquire(1); } 公平锁和非公平锁中都实现了lock方法,公平锁直接调用AQS的acquire,而非公平锁先抢锁,抢不到锁再调用AQS的acquire方法进行上锁 进入acquire方法后的逻辑我们就都知道了。 () { sync.release(1); } unlock方法内直接调用了AQS的Release方法进行解锁的逻辑,进入release方法后逻辑我们都已经知道了,这里不再往下跟。 public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { sync.tryAcquireNanos(1, unit.toNanos(timeout)); } tryLock方法直接调用sync的tryAcquireNanos方法,看过AQS的应该知道tryAcquireNanos这个方法是父类AQS的方法,这个方法和AQS中的四个核心方法中的Acquire方法一样都是上锁的方法,无非是上锁的那几个步骤,调用tryAcquire方法尝试抢锁,抢不到锁就会进入doAcquireNanos方法。 public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { (Thread.interrupted()) throw new InterruptedException(); tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); } doAcquireNanos这个方法做的其实就是入队,阻塞等一系列上锁操作,逻辑和Acquire方法中差不多,但是有两点不同: 看下下面的代码 private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { (nanosTimeout <= 0L) ; final long deadline = System.nanoTime() + nanosTimeout; final Node node = addWaiter(Node.EXCLUSIVE); ; try { (;;) { final Node p = node.predecessor(); (p == head && tryAcquire(arg)) { setHead(node); GC ; ; } nanosTimeout = deadline - System.nanoTime(); (nanosTimeout <= 0L) ; (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanosTimeout); (Thread.interrupted()) throw new InterruptedException(); } } finally { (failed) cancelAcquire(node); } } 这里的阻塞不再是LockSupport类的park方法,而是parkNanos方法,这个方法支持指定时长的阻塞,AQS正是利用这个方法实现阻塞指定时长,当自动唤醒后,循环中会判断是否超过设定时长,如果超过直接返回false跳出循环。 在阻塞期间,如果线程被中断,就会抛出异常,同样会跳出循环,外面可以通过捕获这个异常达到中断阻塞的目的。 可见ReentrantLock其实啥也没做,其tryLock方法完全是依赖AQS实现。 在AQS那篇我们说过Condition是AQS中的条件队列,可以按条件将一批线程由不可唤醒变为可唤醒。 ReentrantLock类 () { sync.newCondition(); } sync静态内部类 () { new ConditionObject(); } sync提供了创建Condition对象的方法,意味着ReentrantLock也拥有Condition的能力。 我们下面说的ReentrantLock其实就是说AQS,因为它的同步实现主要在AQS里面。 实现方面 ReentrantLock是jdk级别实现的,其源码在jdk源码中可以查看,没有脱离java。 synchronized是jvm级别实现的,synchronized只是java端的一个关键字,具体逻辑实现都在jvm中。 性能方面 优化前的synchronized性能很差,主要表现在两个方面: 因为大多数情况下对于资源的争夺并没有那么激烈,甚至于某个时刻可能只有一个线程在工作,在这种没有竞争或者竞争压力很小的情况下,如果每个线程都要进行用户态到内核态的切换其实是很耗时的。 jdk1.6对synchronized底层实现做了优化,优化后,在单线程以及并发不是很高的情况下通过无锁偏向和自旋锁的方式避免用户态到内核态的切换,因此性能提高了,优化后的synchronized和ReentrantLock性能差不多了。 ReentrantLock是在jdk实现的,它申请互斥量就是对锁标识state的争夺,它是通过cas方式实现。在java端实现。 对于争夺不到资源的线程依然要阻塞挂起,但凡阻塞挂起都要依赖于操作系统底层,这一步的用户态到内核态的切换是避免不了的。 因此在单线程进入代码块的时候,效率是很高的,因此我们说ReentrantLock性能高于原始的synchronized 申请互斥量 synchronized的锁其实就是争夺Monitor锁的拥有权,这个争夺过程是通过操作系统底层的互斥原语Mutex实现的,这个过程会有用户态到内核态的切换。 线程阻塞挂起 没能抢到到Monitor锁拥有权的线程要阻塞挂起,阻塞挂起这个动作也是依靠操作系统实现的,这个过程也需要用户态到内核态的切换。 特性方面 两个都是常用的典型的独占锁。 ReentrantLock可重入,可中断,支持公平和非公平锁,可尝试获取锁,可以支持分组将线程由不可唤醒变为可唤醒。 synchronized可重入,不可中断,非公平锁,不可尝试获取锁,只支持一个或者全部线程由不可唤醒到可唤醒。 使用方面 synchronized不需要手动释放锁,ReentrantLock需要手动释放锁,需要考虑异常对释放锁的影响避免异常导致线程一直持有锁。 以下是两个锁的使用方式 class X { private final ReentrantLock lock = new ReentrantLock(); Condition condition1=lock.newCondition(); Condition condition2=lock.newCondition(); () { lock.lock(); try { (1==2){ condition1.await(); } (1==3){ condition2.await(); } } catch (InterruptedException e) { } finally { condition1.signal(); condition2.signal(); lock.unlock(); } } } class X { private final testtest sync=new testtest();; public void m() throws InterruptedException { synchronized(sync){ (1==2){ sync.wait(); } sync.notify(); sync.notifyAll(); } } } 对比代码及特性说明: 两个锁都是依赖一个对象:lock和sync condition和wait方法具有同样的效果,进入condition和wait的线程将陷入等待(不可唤醒状态),只有被分别调用signal和notify方法线程才会重新变为可唤醒状态,请注意是可唤醒,而不是被唤醒。 可唤醒是说具备了竞争资源的资格,资源空闲后,synchronized中会在可唤醒状态的线程中随机挑选一个线程去拿锁,而ReentrantLock中不可唤醒的线程变为可唤醒状态,其实就是将条件队列中的线程搬到等待队列中排队,只有队头的才会去尝试拿锁。 ReentrantLock分批将线程由不可唤醒变为可唤醒也在这段代码中体现了,代码中按照不同的条件将线程放入不同的condition,每个condition就是一个组,释放的时候也可以按照不同的条件进行释放。而synchronized中进入wait的线程不能分组,释放也只能随机释放一个或者全部释放。来源:码农本农
先了解一下
技术架构
具体实现
先看这个方法:lock.lock()
再看这个方法lock.unlock()
public void unlock 最后看这个方法lock.tryLock()
lock.newCondition();
ReentrantLock和synchronized对比