我之见--java多线程之可重入锁,读写锁源码分析 及自定义锁AQS
来源:互联网 发布:笑话软件 编辑:程序博客网 时间:2024/06/06 11:56
ReentrantLock锁是jdk1.5之后加的轻量级锁,相对以前的重量级锁,它有很多的优势。ReentrantLock只支持独占方式的获取操作,它将同步状态用于保存锁获取操作的次数,并且还维护一个owner变量来保存当前所有的线程标识符,只有当线程获取或者释放锁的时候才会修改这个变量。 1. 可重入锁的源码分析:
当我打开的ReentrantLock源码的时候发现它的代码却是非常简单的。总共有三个内部类:第一个抽象内部类Sync (abstract static class Sync extends AbstractQueuedSynchronize) 直接继承AQS 我们可以大概了解一下什么 AQS? AQS是Java并发类库的基础,其提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架。该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础。使用的方法是继承,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态。然而多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握,需要使用 这个同步器提供的常用方法对状态进行操作;
第二个内部类NonfairSync(非公平锁):非公平锁是直接获取锁,没有维护等待队列.第三个内部类FairSync(公平锁):当遇到阻塞的时候,会把请求锁的进程添加到维护等待队列,下次释放锁的时候会从队列的头节头进行处理。
锁的申请和释放都是成对出现的,我们先来看一下ReentrantLock对常规lock和unlock的处理.对于常规的独占锁,ReentrantLock用0和1 分别表示是否有线程持有锁。0代表没有线程持有锁 ,如果有线程申请锁就会把状态改为1,如果释放锁了,就会把状态改为0;
我们来看一下源码:
lock方法:
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }compareAndSetState比较当前的状态是否是0,如果是0的同时,会把当前状态设置成1。如果两个步骤都完成,证明获取锁成功,同时设置进程状态为当前进程。
unlock方法:
public void unlock() { sync.release(1); }简单的把当前状态减1.
可重入方法trylock:
public boolean tryLock() { return sync.nonfairTryAcquire(1); }再看nonfairTryAcquire方法:
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; }如果当前锁状态为0,那么直接获取锁并且返回,如果锁的状态不是0,证明有线程持有锁,再比较当前线程与请求线程是否是同一条线程,则会把累加当前持有的进程数,否则获取锁失败。
目前为此,我们大概已经可以明白可重入锁的实现了,主要是借助AQS 框架来实现,后面会再分析AQS。
2. 读写锁的源码分析:
读写锁和可重入锁是都是基于AQS 来实现的,所以读写内部还是会有一个Sync类。除此之外还有两个类:ReadLock和WriteLock类,不过这两个类都是用同一个 Sync的,当初没有源码的时候,我以为会有两个Sync类,所以读写锁是用一个AQS子类 同时管理讯读取加锁和写入加锁。AQS在内部维护一个等待线程队列,其中记录了某个线程是独占访问(相当于写)还是共享访问(相当于读)。当锁可用时,如果位于队列头部的线程是执行写入操作,那么线程会得到这个锁,如果位于队列头部的线程是读取访问,那么队列中在第一个写入线程之前的所有线程都将获得这个锁。这是一种没有读或者写优先等待的策略。下面简单的分析一下(因为和上面太多相同):看一下ReadLock的lock方法:public void lock() {sync.acquireShared(1);}这里申请的是共享锁。WriteLock的lock方法:public void lock() {sync.acquire(1);}这里申请的是独占锁。从这里可以看到AQS的强大,下面我们还是重点看一下AQS。
3. AQS分析
前面已经说过了,AQS用一个int来表示的状态,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态。然而多线程环境中对状态的操纵必须确保原子性,因此AQS提供getState,setState 和compareAndSetState 这三个原子方法.常用的获取锁和释放锁的流程:获取操作过程如下:if(尝试获取成功){return;}else{加入等待队列;park自己}释放操作:if(尝试释放成功){unpark等待队列中第一个节点}else{return false}
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }tryAcquire再次尝试获取锁,如果还是失败,就会添加到队列:
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }首先:判断尾节点是否为空,如果不为空,直接插入到尾部,如果为空则做特殊处理
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); } }
需要获取当前节点的前驱节点,而头结点所对应的含义是当前站有锁且正在运行。
2. 当前驱节点是头结点并且能够获取状态,代表该当前节点占有锁;
如果满足上述条件,那么代表能够占有锁,根据节点对锁占有的含义,设置头结点为当前节点。
3. 否则进入等待状态。
如果没有轮到当前节点运行,那么将当前线程从线程调度器上摘下,也就是进入等待状态。我们再来看一下AQS的release方法:
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }1. 尝试释放状态;
tryRelease能够保证原子化的将状态设置回去,当然需要使用compareAndSet来保证。如果释放状态成功过之后,将会进入后继节点的唤醒过程。一般由子类实现。
2. 唤醒当前节点的后继节点所包含的线程。
通过LockSupport的unpark方法将休眠中的线程唤醒,让其继续acquire状态。
private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }先取出当前节点的那个一个节点,如果为空,则从最后一个节点一直遍历到第一个节点,直到找到阻塞的节点,同时会唤醒该节点.
package javaThread;import java.util.concurrent.locks.AbstractQueuedLongSynchronizer;public class MyAQSLock { private final Sync sync = new Sync(); public void signal() { sync.releaseShared(0); } public void await() throws InterruptedException { sync.acquireSharedInterruptibly(0); } private class Sync extends AbstractQueuedLongSynchronizer { @Override protected long tryAcquireShared(long arg) { return (getState() == 1) ? 1 : -1; } @Override protected boolean tryReleaseShared(long arg) { setState(1); return true; } }}
这里只是一个简单的锁,用0表示关闭,1表示打开. 当调用 await方法的时候,然后会调用 tryAcquireShared方法,如果已经打开了闭锁,那么就允许通过。
- 我之见--java多线程之可重入锁,读写锁源码分析 及自定义锁AQS
- java多线程 之 AQS
- AQS源码分析之独占锁和共享锁
- AQS源码分析之ConditionObject
- java多线程系列--AQS-01之独占锁原理浅析
- java多线程系列--AQS-02之锁中断
- 我之见--java 多线程 线程池ThreadPoolExecutor源码分析
- 我之见--java多线程 ConcurrentHashMap 源码分析
- JAVA并发编程学习笔记之AQS源码分析
- Java AQS源码分析
- 多线程之AQS原理
- Java多线程系列(六)—AQS源码分析
- 多线程之读写锁
- 多线程之读写锁
- Java多线程之读写锁经典案例
- java多线程读写文件之文件锁
- Java多线程Lock对象之读写锁
- Java多线程(七)之同步器基础:AQS框架深入分析
- std list 主要函数
- 基于oSIP开源库PartySIP服务器之uClibc库的移植和配置[二]
- 应用 Valgrind 发现 Linux 程序的内存问题
- 团队rpg成长记(二)
- Android学习记录-关于BaseAdapter和实体类
- 我之见--java多线程之可重入锁,读写锁源码分析 及自定义锁AQS
- 设计模式-限制性多例模式
- memcached配置及c++使用
- Lucene中如何判断是否使用cfs格式
- linux中的一些基本操作
- 华为机试:大数相减
- cocos2d-x实现3D模型的换装系统的研究
- 计算机网络常见笔试题
- ARM处理器中的“8位位图”