java可重入读_我之见--java多线程之可重入锁,读写锁源码分析 及自定义锁AQS

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

}

在多线程中也必须保证等待队列是线程安全,而且是非阻塞式的。我们来看一下队列的实现:

Node类里面有分别pre指向上一个节点,next指向下一个节点。同时有SHARED共享和EXCLUSIVE独占两种模式.我们先接着从前面的ReentrantLock的lock 方法分     析,如果没有获取锁就会调用 acquire(1) 方法。我们接着AQS里面的方法:

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;

}首先:判断尾节点是否为空,如果不为空,直接插入到尾部,如果为空则做特殊处理

0818b9ca8b590ca3270a3433284dd417.png

我们再来看一下:

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

}

}

1. 获取当前节点的前驱节点; 需要获取当前节点的前驱节点,而头结点所对应的含义是当前站有锁且正在运行。 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状态。

我们再来看一下unparkSuccessor方法:

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

}先取出当前节点的那个一个节点,如果为空,则从最后一个节点一直遍历到第一个节点,直到找到阻塞的节点,同时会唤醒该节点.

下面我们再看一下利用AQS自定义的锁:

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方法,如果已经打开了闭锁,那么就允许通过。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值