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方法有哪些:

image.png

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

  protected final boolean tryRelease(int release) 
  protected final boolean isHeldExclusively() 

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

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

public class ReentrantLock implements Lock { 
    private final Sync sync; 
    public ReentrantLock() { 
        sync = new NofairSync(); 
    } 
    public ReentrantLock(boolean fair) { 
        sync = fair ? new FairSync() : new NonfairSync(); 
    } 
    …… 
} 

1.lock()

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

public class ReentrantLock implements Lock { 
  …… 
  public void lock() { 
    sync.lock(); 
  }   
  …… 
}

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

static final class NonfairSync extends Sync { 
  final void lock() { 
    if (compareAndSetState(0, 1))  
       setExclusiveOwnerThread(Thread.currentThread()); 
    else 
      acquire(1); 
  } 
  protected final boolean tryAcquire(int acquire) {//在ReentrantLock$NonFairLock才终于看到了对AbstractQueuedSynchronizer#tryAcquire的具体实现。 
    return nonfairTryAcquire(acquires);//而tryAcquire的实现实际上又是在其父类ReentrantLock$Lock中实现的,好像有点绕,一会子类实现,一会父类实现。可以先这么来理解,既然它把tryAcquire的具体实现又定义在了父类,那说明这一定是父类对公共方法的抽取(Extract Method),其他地方一定有用到nonfairTryAcquire方法,不然JDK的作者不会闲的蛋疼。 
  } 
} 

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

final boolean nonfairTryAcquire(int acquires) { 
    final Thread current = Thread.currentThread(); 
    int c = getState(); 
    if (c == 0) { 
        if (compareAndSetState(0, acquires)) { 
            setExclusiveOwnerThread(current); 
            return true; 
        } 
    } 
    else if (current == getExclusiveOwnerThread()) { 
        int nextc = c + acquires; 
        if (nextc < 0) // overflow 
            throw new Error("Maximum lock count exceeded"); 
        setState(nextc); 
        return true; 
    } 
    return false; 
} 

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

static final class FairSync extends Sync { 
  inal void lock() { 
    acquire(1); 

  } 
  protected final boolean tryAcquire(int acquires) {//在ReentrantLock$NonFairLock我们看到它老老实实的实现了AQS定义的tryAcquire方法,而没有调用父类的方法,从这里我们也基本能推断在ReentrantLock中没有其他地方会引用到这个方法。 
    final Thread current = Thread.currentThread(); 
    int c = getState(); 
    if (c == 0) { 
      if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { 
        setExclusiveOwnerThread(current); 
        return true; 
      } 
    } 
    else if (current == getExclusiveOwnerThread()) { 
      int nextc = c + acquires; 
      if (nextc < 0)  
        throw new Error(“Maximum lock count exceeded”); 
      setState(nextc); 
      return true; 
    }     
    return false; 
  } 
} 

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

  2.lockInterruptibly()

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

//ReentrantLock#lockInterruptibly 
public void lockInterruptibly() throws InterruptedException { 
    sync.acquireInterruptibly(1);//因为ReentrantLock是排它锁,故调用AQS的acquireInterruptibly方法 
} 
//AbstractQueuedSynchronizer#acquireInterruptibly 

public final void acquireInterruptibly(int arg) throws InterruptedException{ 
  if (Thread.interrupted()) //线程是否被中断,中断则抛出中断异常,并停止阻塞 
    throw new InterruptedException; 
  if (!tryAcquire(arg)) //首先还是获取锁,具体参照上文 
    doAcquireInterruptibly(arg);//独占模式下中断获取同步状态 
} 

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

  3.tryLock()

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

//ReentrantLock#tryLock 
public boolean tryLock() { 
  return sync.nonfairTryAcquire(1); 
} 

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

4.tryLock(long timeout, TimeUnit unit)

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

//ReentrantLock#tryLock 
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { 
  return sync.tryAcquireNanos(1, unit.toNanos(timeout)); 
} 

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

//AbstractQueuedSynchronizer#tryAcquireNanos 

public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { 
  if (Thread.interrupted())  
    throw new InterruptedException();//可以看到前面和lockInterruptibly一样 
  return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);//首先也会先尝试获取锁 
} 

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏图像识别与深度学习

蓝牙项目开发心得

35790
来自专栏大数据架构

Java进阶(三)多线程开发关键技术

其实这个问题应该这么问——sleep和wait有什么相同点。因为这两个方法除了都能让当前线程暂停执行完,几乎没有其它相同点。

372180
来自专栏Java成神之路

Java微信开发_Exception_01_The type org.xmlpull.v1.XmlPullParser cannot be resolved. It is indirectly ref

这个异常是在做微信开发时出现的,在引入了XStream的jar包之后,还是出现了如下错误信息:

11430
来自专栏专注 Java 基础分享

Java并发编程之显式锁机制

     我们之前介绍过synchronized关键字实现程序的原子性操作,它的内部也是一种加锁和解锁机制,是一种声明式的编程方式,我们只需要对方法或者代码块进...

26480
来自专栏hbbliyong

Linq to xml 操作带命名空间的xml

 昨天需要操作用代码操作csproj文件,实现不同vs版本的切换。 在用XElement读取了csproj文件以后怎么也获取不到想要的对象。 反反复复试验了好多...

377110
来自专栏Spark学习技巧

textFile构建RDD的分区及compute计算策略

1,textFile A),第一点,就是输入格式,key,value类型及并行度的意义。 def textFile( path: String, mi...

27170
来自专栏IMWeb前端团队

Redux系列x:源码分析

写在前面 redux的源码很简洁,除了applyMiddleware比较绕难以理解外,大部分还是 这里假设读者对redux有一定了解,就不科普redux的概念和...

25960
来自专栏数据小魔方

Julia语言初体验

最近MIT发布的julia 1.0.0版,据传整合了C、Python、R等诸多语言特色,是数据科学领域又一把顶级利器。

2.9K20
来自专栏XAI

【人工智能】动物、植物、车型、菜品、LOGO识别示例代码

图像识别部分接口Java-API调用示例代码 https://gitee.com/xshuai/ai/不是完整的web项目大家没必要下载运行。复制|下载相关代码...

925110
来自专栏我叫刘半仙

【JDK并发包基础】工具类详解

       在写并发代码来提升性能时,会遵循某些最佳写法,而不是只用基础的wait和notify来控制复杂的流程。Java.util.concurrent 包...

39550

扫码关注云+社区

领取腾讯云代金券