我们之前介绍过synchronized关键字实现程序的原子性操作,它的内部也是一种加锁和解锁机制,是一种声明式的编程方式,我们只需要对方法或者代码块进行声明,Java内部帮我们在调用方法之前和结束时加锁和解锁。而我们本篇将要介绍的显式锁是一种手动式的实现方式,程序员控制锁的具体实现,虽然现在越来越趋向于使用synchronized直接实现原子操作,但是了解了Lock接口的具体实现机制将有助于我们对synchronized的使用。本文主要涉及以下一些内容:

  • 接口Lock的基本组成成员
  • 可重入锁ReentrantLock的基本使用
  • 深入ReentrantLock的实现原理

一、接口Lock的基本组成成员

     Lock 位于java.util.concurrent.locks包下,源码如下:

public interface Lock {
void lock();
void lockInterruptibly()
boolean tryLock();
boolean tryLock(long time, TimeUnit unit)
void unlock();
Condition newCondition();
}

其中,

  • void lock();:调用该方法将获得一个锁的入口
  • lockInterruptibly():该方法也是去获得一个锁,但是它是响应中断的,一旦在获取的过程中遭遇中断将抛出 InterruptedException。
  • boolean tryLock();:该方法尝试着去获得一个锁,如果获取失败将返回false,并不会阻塞当前线程
  • boolean tryLock(long time, TimeUnit unit):尝试着去获取一个锁,如果获取失败,将阻塞等待指定的时间,期间如果能够获得锁将返回true,否则返回false,响应中断请求。
  • void unlock();:释放一个锁
  • Condition newCondition();:条件变量,留待下篇文章学习

二、可重入锁ReentrantLock的基本使用

     ReentrantLock是接口 Lock的一个最主要的实现类,不仅实现了Lock中的基本的加锁释放锁的方法,还扩展了自己的方法。它有两个构造方法:

public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

参数 fair用于保证锁机制的公平策略,公平的策略会是的等待时间越长的线程优先获得锁。保证公平必然会降低性能,所以ReentrantLock默认并不保证公平。我们用ReentrantLock来实现对程序的原子操作:

public class MyThread extends Thread{

	private static Lock lock = new ReentrantLock();
public static int count; @Override
public void run() {
try {
Thread.sleep((int)Math.random()*100);
lock.lock();
count++;
lock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

当我们在主程序中启动一百个线程随机唤醒对count进行加一时,无论运行多少次,结果都是一百,也就是说我们的ReentrantLock是可以为我们保证原子操作的。

ReentrantLock还有一个特性就是可以重入性,即在本身获得某个锁的前提下可以随意进入被该锁锁住的其他方法,对于一个锁可以重复进入。除此之外,ReentrantLock还具有一些其他的有关锁信息的方法:

  • public int getHoldCount():表示当前线程持有该锁的数量
  • public boolean isHeldByCurrentThread():判断锁是否为当前线程持有
  • public boolean isLocked():判断锁是否为任意一个线程持有,如果有则返回true,否则返回false
  • public final boolean hasQueuedThreads():判断该锁上是否有线程进行等待
  • public final int getQueueLength():返回当前等待队列的长度,也就是等待进入该锁的线程个数

三、深入ReentrantLock的实现原理

     ReentrantLock依赖CAS和LockSupport来实现,LockSupport有点像工具类,它主要提供两类方法,park和unpark。

  • public static void park()
  • public static void parkNanos(long nanos)
  • public static void parkUntil(long deadline)
  • public static void unpark(Thread thread)

调用park方法会使得当前线程丢失CPU使用权,从Runnable状态转变为Waiting状态。而unpark方法则反过来让Waiting状态的某个线程转变状态为Runnable,等待操作系统调度。parkNanos和parkUntil是和时间相关的两个park的变种,parkNanos指定线程要等待的时间,parkUntil则指定线程要等待到什么时候,这个时间是一个绝对时间,相对于纪元的毫秒数。

Java的并发包中有很多并发工具,ReentrantReadWriteLock,Semaphore,CountDownLatch,ReentrantLock等。这些工具有很多的共同特性,于是Java为我们抽象了一个类AbstractQueuedSynchronizer(AQS)来表示这些工具的共性。ReentrantLock是其的一个实现类,内部有三个内部类:

abstract static class Sync extends AbstractQueuedSynchronizer{
//......
}
static final class NonfairSync extends Sync{
//...........
}
static final class FairSync extends Sync {
//.............
}

Sync 继承了AQS并对其中的大部分代码进行了简单的实现,FairSync 和NonfairSync 是针对公平策略而定义的,如果构造ReentrantLock的时候指定公平的策略,那么其内部的所有方法都依赖这个FairSync ,否则就全部依赖NonfairSync。接着看ReentrantLock的构造函数:

private final Sync sync;

public ReentrantLock() {
sync = new NonfairSync();
} public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

两个构造方法最终会对sync进行初始化,而sync的将在后续的方法中起到相当大的作用。我们先看lock方法的具体实现:

public void lock() {
sync.lock();
}

ReentrantLock的lock方法调用的sync的lock方法,而在sync中的lock方法是一个抽象的方法,也就是说这个方法的具体实现在子类中,我们看NonfairSync中的实现:

final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

AQS中有一个整型类型的State变量,它用于标识当前锁被持有的次数,该值为0表示当前锁没有被任何线程持有。compareAndSetState是AQS中的方法,该方法调用了unsafe.compareAndSwapInt方法以CAS方式对State进行了更新,如果state的值为0,说明该锁并没有被任何线程持有,那么当前线程将持有该锁并将state的值赋为1。

这就完成了获取的动作,一旦后续的线程尝试访问临界区代码,在前面的线程没有释放锁之前,将会调用 acquire(1)。

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

tryAcquire还是调用了AQS中的实现,

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;
}

第一个if判断,想要持有的锁是否被持有(虽然之前判断过了,但是有可能在我们调用nonfairTryAcquire方法的期间,之前的线程释放了该锁),如果未被任何线程持有,那么将直接持有该锁。

第二个if判断,如果当前锁的持有者就是当前线程,表示这是同线程的重入操作,于是增加锁定次数并设置state的值。

整个方法结束之后,如果当前线程获得了锁,都将返回true,否则都会返回false。而如果tryAcquire方法返回true,那么整个acquire方法也将结束,否则就说明当前线程并没有通过锁,需要被阻塞。那么就会调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。

private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}

addWaiter方法将当前线程包裹成一个Node结点,添加到AQS内部所维护的一个等待队列并返回该Node结点。最后调用acquireQueued方法:

final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

该方法首先会去获得node的前一个结点,判断如果是head结点,那么说明当前的node结点是整个等待队列上的第一个等待的结点。于是让它尝试着去获得锁,如果能够获得锁,将从等待队列中清除它并返回。

如果发现当前结点前面还有等待的结点或者尝试获取锁失败,那么将会调用shouldParkAfterFailedAcquire方法判断该结点锁对应的线程是否需要被park阻塞,并最终调用LockSupport.park(this)阻塞当前线程。

在第一个线程持有该锁的前提下,成功阻塞了第二个线程。这大概就是整个lock方法的调用链流程。

接下来看看unlock的具体实现,

public void unlock() {
sync.release(1);
}

这是ReentrantLock中对AQS的unlock的具体实现,调用了sync的release方法,这个方法是其父类AQS中的方法:

public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

tryRelease被sync重写,具体代码如下:

protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

首先判断如果当前线程并不是锁的当前持有者,抛出异常(不持有该锁自然不能释放该锁)。如果c等于0则表示,当前锁只被持有一次,也就是当前线程并没有多次重入该锁,于是将该锁的持有者设置为null,表示未被任何线程持有。如果c不等于0,那么说明该锁被当前线程重入多次,于是对state减一并设置state的值。最终如果返回true则说明该锁被释放了,否则说明当前线程依然持有该锁。

回到release方法,如果tryRelease(arg)返回true,那么方法体会判断当前等待队列是否有结点在等待该锁,如果有则调用unparkSuccessor(h)方法唤醒等待队列上的第一个等待的结点线程并返回true。

这里有一个细节,其实所有未能获得锁的线程都被阻塞在方法中:

final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//******等待线程唤醒的起始位置********//
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

未能获得锁的线程被方法parkAndCheckInterrupt阻塞了,所以当我们在unlock中调用unpark唤醒一个等待队列上的线程结点时,线程将从此处重新进入死循环尝试去获取锁。如果能够获得锁,将从等待队列中移除自己,并返回,否则再次被阻塞等待唤醒。

整个unlock方法的执行流程也已经大致介绍完成,最后我们看看可重入锁ReentrantLock和synchronized的一些对比。

四、ReentrantLock对比synchronized

     synchronized更倾向于一种声明式的编程方式,我们在方法前使用synchronized修饰,Java会自动为我们实现其内部的细节,什么时候加锁,什么时候释放锁都是它负责的。

     而对于我们的ReentrantLock重入锁来说,需要我们自己手动的去加锁和释放锁,对于逻辑的要求更高,也相对更难。

     而随着jvm版本的更新和优化,ReentrantLock和synchronized在性能上的差别在逐渐缩小,所以一般建议使用synchronized而尽量避免复杂难操作的ReentrantLock。

对于显式锁的基本情况大致介绍如上,如有错误之处,望指出!

Java并发编程之显式锁机制的更多相关文章

  1. 并发编程 19—— 显式的Conditon 对象

    Java并发编程实践 目录 并发编程 01—— ThreadLocal 并发编程 02—— ConcurrentHashMap 并发编程 03—— 阻塞队列和生产者-消费者模式 并发编程 04—— 闭 ...

  2. Java并发编程实战 05等待-通知机制和活跃性问题

    Java并发编程系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 Java并发编程实 ...

  3. Java并发编程实战 03互斥锁 解决原子性问题

    文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和 ...

  4. java之AQS和显式锁

    本次内容主要介绍AQS.AQS的设计及使用.ReentrantLock.ReentrantReadWriteLock以及手写一个可重入独占锁 1.什么是AQS? AQS,队列同步器AbstractQu ...

  5. Java并发编程:线程和锁的使用与解析

    线程的使用  新建线程 新建一个线程有两种方法:继承Thread类,然后重写run方法:实现Runnable接口,然后实现run方法.实际上Thread类也是实现的Runnable接口,再加上类只能单 ...

  6. Java并发编程(五)锁的使用(下)

    显式锁 上篇讲了使用synchronized关键字来定义锁,其实Java除了使用这个关键字外还可以使用Lock接口及其实现的子类来定义锁,ReentrantLock类是Lock接口的一个实现,Reen ...

  7. Java并发编程(05):悲观锁和乐观锁机制

    本文源码:GitHub·点这里 || GitEE·点这里 一.资源和加锁 1.场景描述 多线程并发访问同一个资源问题,假如线程A获取变量之后修改变量值,线程C在此时也获取变量值并且修改,两个线程同时并 ...

  8. Java并发编程(02):线程核心机制,基础概念扩展

    本文源码:GitHub·点这里 || GitEE·点这里 一.线程基本机制 1.概念描述 并发编程的特点是:可以将程序划分为多个分离且独立运行的任务,通过线程来驱动这些独立的任务执行,从而提升整体的效 ...

  9. Java并发编程-可重入锁

    可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍可以获取该锁而不受影响.在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁. publ ...

随机推荐

  1. 201521123051《Java程序设计》第十四周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多数据库相关内容. 2. 书面作业 1. MySQL数据库基本操作 建立数据库,将自己的姓名.学号作为一条记录插入.(截图,需出现自 ...

  2. 201521123110《Java程序与设计》第13周学习总结

    1. 本周学习总结 2. 书面作业 1. 网络基础 1.1 比较ping www.baidu.com与ping cec.jmu.edu.cn,分析返回结果有何不同?为什么会有这样的不同? 时间数据不同 ...

  3. 关于args的一个小bug

    我在开始学习Java的时候就有点疑惑,到底main方法中的args到底是什么?经过我的一些思考,然后结合代码写一点自己的看法. 下面来看一段代码: /** * @author 薛定谔的猫 * 关于ar ...

  4. js中变量base64加密传输

    首先对base64进行定义: var Base64 = { _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01 ...

  5. linux防火墙简单的使用

    Centos升级到7之后,内置的防火墙已经从iptables变成了firewalld.所以,端口的开启还是要从两种情况来说明的,那就是iptables和firewalld.本文章参考官网教程基础 一. ...

  6. lintcode.177 把排序数组转换为高度最小的二叉搜索树

    把排序数组转换为高度最小的二叉搜索树    描述 笔记 数据 评测 给一个排序数组(从小到大),将其转换为一棵高度最小的排序二叉树. 注意事项 There may exist multiple val ...

  7. JVM菜鸟进阶高手之路九(解惑)

    转载请注明原创出处,谢谢! 在第八系列最后有些疑惑的地方,后来还是在我坚持不懈不断打扰笨神,阿飞,ak大神等,终于解决了该问题.第八系列地址:http://www.jianshu.com/p/7f7c ...

  8. 庞玉栋:浅谈seo优化对于网站建设的重要性

    根据最近做SEO优化经验而写  写的也都是我的方法 大神勿喷 SEO:英文Search Engine Optimization缩写而来, 中文意译为搜索引擎优化 如果你连个网站都没有那就点这里:如何拥 ...

  9. git fatal: I don't handle protocol 'https'问题的解决

    问题重现 新建的仓库,再把本地的代码往上push的时候Git提示 $ fatal: I don't handle protocol 'https' 问题分析 Git是支持https的,这点毋庸置疑,所 ...

  10. getField()和select()方法的区别

    在ThinkPHP中,查询数据库是必不可少的操作. 那么,getField()方法和select()方法都是查询的方法,到底有什么不同呢? 案例来说明: A.select()方法 例子1 $acces ...