从ReentrantLock的角度来看AQS原理

ReentrantLock

ReentrantLock作为java api层面的加锁方式,其性能比synchronized更好(synchronized进行优化后性能差不太多),灵活性更强

ReentrantLocksynchronized
可重入可重入
可尝试加锁不能尝试加锁
加锁时支持打断加锁时不支持打断
加锁时可设置超时时间不能设置超时时间
关联多个条件队列关联一个条件队列
需要手动释放锁自动释放锁
公平&非公平非公平

       // 获取锁
        reentrantLock.lock();
        // 获取锁时可相应中断
        reentrantLock.lockInterruptibly();
        // 尝试获取锁 获取成功返回 true
        boolean b = reentrantLock.tryLock();
        // 尝试获取锁,如果超过指定时间则超时, 获取成功返回 true
        boolean b1 = reentrantLock.tryLock(1, TimeUnit.SECONDS);

ReentrantLock 中有一个 Sync 抽象类继承自 AbstractQueuedSynchronizer
Sync 又有两个子类,分别是 FairSyncNonfairSync 公平锁的实现和非公平锁的实现

在这里插入图片描述
我们再看看 AQS

AQS就是java中的一个类 全名叫做 AbstractQueuedSynchronizer 内部维护了一个双向列表,列表中除头节点以外其他节点都是一个阻塞的线程,每个节点都是用它的一个内部类 Node 进行封装

Node中有下列这些字段

名称类型默认值含义
SHAREDNodenew Node()表示共享模式
EXCLUSIVENodenull表示独占模式
CANCELLEDint1waitStatus的状态,该状态表示此线程放弃竞争锁
SIGNALint-1waitStatus的状态,表示节点在等待队列中,节点线程等待唤醒
CONDITIONint-2waitStatus的状态., 该状态表示此节点在等待队列中,等待被唤醒
PROPAGATEint-3只有共享模式下才会使用,这里先不解释
waitStatusint0去上面那几种状态
prevNodenull该节点的前驱节点
nextNodenull该节点的后继节点
threadThreadnull该节点表示的线程

AQS中维护了一个 state 状态,该状态可表示是不是有线程获取了锁,具体的含义由子类进行实现
它还维护了分别指向头部和尾部的节点

AQS中的头节点是个哨兵节点,不代表任何线程

ReentrantLock 本身只实现了 Lock 接口
在这里插入图片描述

ReentrantLock 有两个构造方法,通过默认的构造方法创建的ReentrantLock对象默认使用的是非公平锁


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


ReentrantLock实现了 Lock 接口,所以要重写 lock() 方法


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

它内部调用了 sync.lock() 如果我们使用默认的构造方法创建的 ReentrantLock 那么sync就是 NonfairSync

这里我们以非公平锁 NonfairSync 来讲解

假如我们有下列代码,有一个线程 t1 和 t2 ,我们先让 t1 进行启动并获取锁

public static void main(String[] args) throws InterruptedException {
        ReentrantLock reentrantLock = new ReentrantLock();

        // 获取锁
        reentrantLock.lock();
        
        new Thread(() -> {
            reentrantLock.lock();
            try {
                while (true) {}
            }finally {
                reentrantLock.unlock();
            }
        }, "t1").start();
    
        Thread.sleep(1000);
        new Thread(() -> {

            reentrantLock.lock();
            try {
                while (true) {}
            }finally {
                reentrantLock.unlock();
            }
        }, "t2").start();

    }

加锁流程

此时 t1 先执行 lock 方法


 final void lock() {
 			// 使用CAS的方式尝试将state更改为1
 			// 如果更改成功则代表锁获取成功
 			// t1 进来的时候 t2 并没有启动,那么t1肯定能获取到锁
            if (compareAndSetState(0, 1))
            	// 获取锁成功,将锁的持有者设置为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
            	// 获取锁失败走这个流程
                acquire(1);
        }

t2 启动,并进入 lock() 方法


 final void lock() {
 			// 由于 t1 已经获取到锁了,也就是将 state 修改成 1 了
 			// 那么此时t2通过 CAS的方式再次尝试获取锁将 失败
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
            	// 获取锁失败走这个流程
                acquire(1);
        }

t2 进入该acquire(1) 方法
这个方法位于 AQS中,并且是final修饰的,不让子类进行重写
事实上基于AQS实现的独占锁在获取锁时一般都会调用这个方法

	
	// 这个方法在 Sync中实现
	final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 这时候 t1 是持有锁的,这个state已经被t1修改成 1 了
            // 不过如果此时 t1 恰巧释放了锁那么 t2 state就等于0了,现在不考虑这种情况
            int c = getState();
            // t2 执行到这里的时候很明显该条件不成立
            
            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;
    }	

	// 非公平锁中的实现
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }	
	
	// arg 这个 参数是在实现可重入锁时使用的,暂时忽略
   public final void acquire(int arg) {
   		//  首先会执行 tryAcquire(1) 该方法是要子类重写的,默认抛出异常
   		// 其实tryAcquire(1) 就是尝试获取所,如果获取成功了则返回true
   		// 否则会执行  acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

由于 t1 此时持有锁,t2再去获取时就是获取锁失败

那么会执行这句代码 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

先来看看 addWaiter 干了什么事


	// 刚刚调用那里传过来的mode为 : Node.EXCLUSIVE
	// 	表示独占锁模式
   private Node addWaiter(Node mode) {
   		// 将当前线程封装成一个 mode
        Node node = new Node(Thread.currentThread(), mode);
        
        Node pred = tail;
        // 由于t2是第一个获取锁失败的线程,此时队列是空的
        //  该条件不成立
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        // 将t2 封装的 Node返回了出去
        return node;
    }
    
    private Node enq(final Node node) {
    	// 注意这里是个死循环
        for (;;) {
            Node t = tail;
            // 如果此时队列是空的那么将初始化这个队列
            //  经过第一次循环后队列不为空了
            if (t == null) {
                if (compareAndSetHead(new Node()))
                	// 并把头部的节点也指向了尾部
                	// 然后要进行下一次循环了
                    tail = head;
            } else {
            	//  第二次循环会进入到这里
            	// 把 当前的尾部节点 t 作为 我们传过来的那个节点的前驱节点
            	// 我们传过来的节点就是线程 t2 封装的那个
                node.prev = t;
                // 然后将 t2 封装的那个节点设置成尾节点
                if (compareAndSetTail(t, node)) {
                	// 将之前尾节点的下一个节点执行 t2
                    t.next = node;
                    // 返回的是我们当前要插入的节点也就是t2的前驱节点
                    // 也就是上一个尾节点
                    return t;
                }
            }
        }
    }

执行完 addWaiter 之后列表是这样的
现在所有的waitStatus都等于0
在这里插入图片描述

接下来看看 acquireQueued方法的执行


// node 参数是 addWaiter(Node.EXCLUSIVE), arg) 返回的
//  返回的其实就是线程t2封装的那个node
//  arg 还是 1
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
        	// 这里是标记线程在获取锁阻塞期间是否被打断过
            boolean interrupted = false;
            // 这里死循环
            for (;;) {
            	// node 就是 t2,现在位于队列中的第二个,他的前驱节点是头节点
                final Node p = node.predecessor();
                // 判断t2的前驱节点是不是头节点 也就是判断当前这个node是不是在队列中位于第二个节点的位置
                // tryAcquire(arg) 就是尝试去获取一下锁,如果获取成功了就返回true
                // 但是现在 t1 还未释放锁 所以仍然是获取锁失败的
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; 
                    failed = false;
                    return interrupted;
                }
                // shouldParkAfterFailedAcquire() 表示获取锁失败了是否需要阻塞住,如果要阻塞住返回true
                //  parkAndCheckInterrupt() 就是阻塞住当前线程
                //  当第一次执行到这里会返回false
                //  第二次执行时会返回true 然后就会阻塞住
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }


// pred 就是 node 的前驱节点,也就是头节点
// node 就是 t2 封装的节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
		// 拿到头节点的watiStatus
        int ws = pred.waitStatus;
        // 如果等于 -1 就代表 需要阻塞住
        if (ws == Node.SIGNAL)
            return true;
        // 大于 0 则代表放弃竞争锁
        if (ws > 0) {
            do {
            	// 循环向前查找取消节点,把取消节点从队列中剔除
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        	// 将头节点的 waitStatus设置为 -1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        // 返回false 不阻塞
        return false;
    }

 // 会将当前线程阻塞住,如果是被打断的就会清除打断标记
 // 因为 LockSupport.prk() 不会清除打断标记,需要手动调用 Thread.interrupted() 
 // 如果不清除打断标记的话,下一次LockSupport.park() 会阻塞不了
  private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

    


执行完上列方法会 线程的waitStatus会发生改变,但是队尾的waitStatus没有

在这里插入图片描述

解锁流程

假如此时 t1 要释放锁了


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


    public final boolean release(int arg) {
    	// 尝试释放锁
        if (tryRelease(arg)) {
            Node h = head;
            // 释放锁成功后需要有没有后继节点
            // 如果有则需要唤醒后继节点
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }


	protected final boolean tryRelease(int releases) {
			// 因为线程一 只加了一次锁即没有重入
			// 所以state等于1 releases 这时也是1  1-1=0
            int c = getState() - releases;
            // 判断当前当前线程是否是锁的持有者
            // 如果你都不持有锁肯定不会让你释放
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 等于0就代表释放成功了
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 因为只能有一个线程持有锁,所以释放锁的时候只会有一个线程执行到这里所以不需要通过CAS的方法修改state
            setState(c);
            return free;
        }

	private void unparkSuccessor(Node node) {
    
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 如果没有 下一个节点或者下一个节点已经被取消了
            // 那么判断是否有尾节点并且尾节点不是当前节点
            //  如果成立则从尾部向前找直到找到waitStatus小于等于0的节点为止
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
        	// 唤醒后继节点 这里将t2唤醒了
            LockSupport.unpark(s.thread);
    }

再来看看将t2唤醒之后 t2获取锁的流程



final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //  这时候 t1已经释放锁了
                // 并且t2 的前驱节点就是头节点
                if (p == head && tryAcquire(arg)) {
                	// 获取锁成功之后会将当前t2的这个节点设置为头节点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //  parkAndCheckInterrupt() t2 阻塞在这个方法里
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

其实整个AQS的加锁流程还是相对简单的,只要搞懂了其中涉及到的一些概念

在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值