ReentrantLock,AQS源码阅读(一)

168 阅读12分钟

ReentrantLock锁简介

  • ReentrantLock是可重入的独占锁,同时也提供了可以响应中断的加锁方法

  • ReentrantLock通过Sync继承AQS来使用AQS提供的锁框架的独占模式,有公平锁非公平锁两种

		
	 abstract static class Sync extends AbstractQueuedSynchronizer
	 
	 //非公平锁
	 static final class NonfairSync extends Sync 
	 
	 //公平锁
	 static final class FairSync extends Sync
	

  • AQS使用state值来表示锁资源,对于独占锁,当有线程抢锁时会尝试设置state,设置成功即表示加锁成功

Markdown

AQS字段

  • AQS中使用Node来包装未拿到锁的线程,抢锁失败时线程会把自己的Node结点放入阻塞队列

  • 对于阻塞队列来说,头结点可以理解为拥有锁的线程结点,当持有锁的线程释放锁时,会唤醒头结点的后继的第一个正常结点

  • 对于AQS的共享模式和条件队列,本节暂不涉及

		
	//AQS中用来包装线程的结点
	static final class Node {
        
        //当前结点是共享模式
        static final Node SHARED = new Node();
        
        //当前结点是独占模式
        static final Node EXCLUSIVE = null;

        //当前结点处于取消状态
        static final int CANCELLED =  1;
        
        //当前结点需要唤醒它的后继节点
        static final int SIGNAL    = -1;
		
        //当前结点在条件队列中的状态
        static final int CONDITION = -2;
        
        //当前结点为传播状态
        static final int PROPAGATE = -3;

        //当线程被包装入结点时,初始默认状态为0
		//当前结点的状态,可选值为0, SIGNAl(-1), CANCELLED(1), CONDITION(-2), PROPAGATE(-3)
        volatile int waitStatus;

        //阻塞队列中前驱节点
        volatile Node prev;

        //阻塞队列中后继结点
        volatile Node next;

        //Node封装的线程
        volatile Thread thread;

		//阻塞队列中用来标识是独占锁还是共享锁
        //条件队列中用来指向下一个结点
        Node nextWaiter;

        //当前结点是否是共享模式
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        //返回当前结点的前驱节点
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
		
		//用来初始化头结点,或者SHARED结点
        Node() {    // Used to establish initial head or SHARED marker
        }
		
		//把结点加入阻塞队列时使用的构造方法
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
		
		//把结点加入条件队列时使用的构造方法
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

    //头结点:当有线程释放锁时,会唤醒头结点后面的第一个正常结点
    private transient volatile Node head;

    //尾结点
    private transient volatile Node tail;

    //锁资源:初始状态为0,即没有线程持有锁,每加锁一次,state就加1(独占模式下)
    private volatile int state;

    //当前锁的值
    protected final int getState() {
        return state;
    }
	

以公平锁为例解析AQS的加锁流程

  • 公平锁中lock()提供了拿锁操作,具体加锁流程是调用AQS中的模板方法

  • 公平锁重写了AQS中的tryAcquire()方法

		
	//lock()方法直接调用了AQS的acquire()
	final void lock() {
         acquire(1);
    }
	
	//AQS的acquire()方法:拿锁
	public final void acquire(int arg) {
	
		//tryAcquire(arg):尝试获取锁,获取成功返回true,获取失败返回false,具体后面会写
		//addWaiter(Node.EXCLUSIVE), arg):将当前线程封装成Node,并加入阻塞队列,具体后面会写
		//acquireQueued(addWaiter(Node.EXCLUSIVE), arg):尝试获取锁,并挂起线程,线程被唤醒后,会再次尝试拿锁,具体后面会写
		if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
			
			//acquireQueued()方法返回的是挂起过程中是否被中断唤醒
			//selfInterrupt()方法:线程给自己一个中断信号
			selfInterrupt();
	}
	
	//这是AQS的方法,公平锁重写了此方法
	protected final boolean tryAcquire(int acquires) {
		
		//当前线程
		final Thread current = Thread.currentThread();
		
		//AQS中的state值:表示当前锁资源
		int c = getState();
		
		//当前AQS处于无锁状态
		if (c == 0) {
			
			//公平锁,需要检查一下,阻塞队列中是否有结点等待
			//hasQueuedPredecessors():true 阻塞队列中有结点在等待
			//hasQueuedPredecessors():false 阻塞队列中没有结点等待

			//compareAndSetState():CAS操作设置state值,拿锁
			if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
					
				//设置当前线程为持有锁线程
				setExclusiveOwnerThread(current);
				return true;
			}
		}
		
		//c != 0 此时需要检查一下,当前线程是不是持有锁的线程,ReentrantLock可以重入
		
		//当前线程就为持有锁线程
		else if (current == getExclusiveOwnerThread()) {
			
			//重入之后的state值
			int nextc = c + acquires;
			
			//判断nextc是否已经超过int最大值,如果锁重入了很多次,c是Integer.MAX_VALUE时再增加就越界了
			if (nextc < 0)
				throw new Error("Maximum lock count exceeded");
			
			//设置state值
			setState(nextc);
			return true;
		}

		
		//情况一:c == 0 时,阻塞队列已经有线程在等待了,公平锁不能抢锁
		//情况二:c == 0 时,CAS竞争失败
		//情况三:c > 0 时,持有锁的线程不是自己
		return false;
	}
	
	

  • 当线程抢锁失败后,会把自己封装成Node结点,加入阻塞队列,如果是第一个抢锁失败的线程会为阻塞队列构造一个空的头结点
		
	//AQS的addWaiter():将线程封装为Node结点尝试入队
	private Node addWaiter(Node mode) {
		
		//构建Node,把当前线程封装到Node中
		//如果是独占锁,mode是Node.EXCLUSIVE
		//如果是共享锁,mode是Node.SHARED
		Node node = new Node(Thread.currentThread(), mode);
		 
		//队尾结点
		Node pred = tail;
		
		//队列中已经有结点
		if (pred != null) {
			
			//当前节点的前驱指向尾结点
			node.prev = pred;
			
			//CAS操作将队列尾节点设置为当前Node:入队成功
			if (compareAndSetTail(pred, node)) {
				
				//原队尾结点的next指向现队尾结点
				pred.next = node;
				return node;
			}
		}

		//情况一:当前队列是空队列
		//情况二:CAS竞争失败

		//继续入队,具体后面会写
		enq(node);
		return node;
	}

	//AQS中的enq():将当前Node加入阻塞队列
	private Node enq(final Node node) {
		
		//自旋入队
		for (;;) {
			
			//尾结点
			Node t = tail;
			
			//当前队列是空队列,当前线程有可能是第一个获取锁失败的线程
			if (t == null) { 
					
				//当前线程需要入队,但队列为空,当前线程需要创建一个头结点
				//因为第一个持有锁的线程不会自己入队,需要后面拿锁失败的线程自己创建一个空的头结点new Node()
					
				//设置自己创建的头结点
				if (compareAndSetHead(new Node()))
					tail = head;
			
			//队列不为空,把自己加入队列
			} else {
				
				//入队:如果失败继续自旋
				node.prev = t;
				if (compareAndSetTail(t, node)) {
					t.next = node;
					
					//返回旧的尾结点,即当前结点的前驱结点
					return t;
				}
			}
		}
	}
	

  • 线程的Node结点加入阻塞队列后会被阻塞挂起,在挂起之前会尝试拿锁,被唤醒之后继续尝试拿锁

  • 由于当前加锁流程是不响应中断的,线程被中断唤醒后会把自己的中断标记置为false,不过acquireQueued()方法会返回线程是否被中断过

  • 响应中断的加锁会在线程被中断唤醒后抛出异常,后面具体有写

		
	
	//AQS的acquireQueued():尝试拿锁,失败就挂起,醒来后继续拿锁,失败继续挂起
	//Node:就是包装当前线程的结点,此时已经加入阻塞队列
	//arg:当前线程拿锁时用来设置state值时
	final boolean acquireQueued(final Node node, int arg) {
		
		
		//正常情况下拿锁后被置为false
		//不正常情况会在finally中取消该结点
		boolean failed = true;
		try {
		
			//当前线程是否被中断过
			boolean interrupted = false;
			
			//自旋..
			for (;;) {

				//情况一:第一次for循环时,线程尚未park
				//情况二:线程被unpark唤醒

				//当前节点的前置节点
				final Node p = node.predecessor();
				
				//p == head:当前节点为头结点下一个节点,此时有权利去抢锁
				//tryAcquire(arg) true:头结点对应的线程已经释放锁,此时当前结点正好抢到了锁
				//				 false:头结点还持有锁,当前节点仍旧要被park
				if (p == head && tryAcquire(arg)) {
				
					//拿到锁之后,设置自己为头节点。
					setHead(node);
					
					//将旧的头结点的next置为null
					p.next = null; // help GC
					
					//当前线程正常拿锁
					failed = false;
					
					//返回当前线程是否被中断过
					return interrupted;
				}

				//shouldParkAfterFailedAcquire():判断当前线程是否需要被挂起,在这个方法中会保证前置结点状态为SIGNAL
				//true:当前线程会被parkAndCheckInterrupt()挂起  
				
				//parkAndCheckInterrupt():挂起当前线程,线程被唤醒后返回当前线程的中断标记
				if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
					
					//parkAndCheckInterrupt()== true:当前线程是被中断信号唤醒的
					//interrupted置为true,线程被中断唤醒
					interrupted = true;
			}
		} finally {
		
			//什么时候failed为true?上面拿锁过程出现异常或者Error,方法不能正常返回了,此时取消当前结点
			if (failed)
			
				//这个方法后面会写,在响应中断加锁流程里会具体写
				cancelAcquire(node);
		}
	}

	//AQS的shouldParkAfterFailedAcquire():1.node结点的前置结点pred为取消状态,越过取消状态的结点,返回false
	//                                    2.node结点的前置结点pred为默认状态,将前置结点的状态设置为SIGNAl,返回false
	//                                    3.node结点的前置结点pred为SIGNAl状态,返回true
	private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	
		//前置结点的状态: 默认状态 SIGNAL状态  CANCELED状态
		int ws = pred.waitStatus;
		
		//前置结点是SIGNAL状态:前置结点可以唤醒当前结点的节点,直接返回true,让parkAndCheckInterrupt()park当前线程
		//这是因为在线程被park之前,需要保证当前结点的前置结点可以唤醒当前结点,即前置结点状态为SIGNAL
		if (ws == Node.SIGNAL)
			return true;

		//前置结点是CANCELED状态:已经被取消
		if (ws > 0) {
			
			do {
				//设置前置结点,一直向前找
				node.prev = pred = pred.prev;
				
			//向前寻找waitStatus <= 0 的结点
			} while (pred.waitStatus > 0);
			
			//找到后,将找到的结点后继设为当前结点
			//也就是这两者之间的CANCELED状态的结点就出队了
			pred.next = node;

		} else {
		
			//前置结点的状态是默认状态0
			//将前置结点的状态设置为SIGNAl,表示前置结点点释放锁之后需要唤醒当前结点
			compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
		}
		return false;
	}
	
	
	//AQS的parkAndCheckInterrupt():挂起当前线程,在其醒来后返回中断标志位,并重置中断标志为false
	private final boolean parkAndCheckInterrupt() {
		//利用LockSupport.park挂起当前线程
		LockSupport.park(this);
		
		//线程如何被唤醒? 1.被前驱结点用LockSupport.unpark唤醒,此时线程中断标记是false;
		//                2.被中断唤醒,此时线程中断标记是true;
		
		//Thread.interrupted()返回当前线程的中断标志位,然后把中断标志位置为false;
		return Thread.interrupted();
	}
	

解析ReentrantLock解锁流程

  • ReentrantLock解锁流程是调用的AQS的模板方法,其中Sync重写了tryRelease()方法
		
	//ReentrantLock提供的解锁入口,直接调用AQS的模板方法
	public void unlock() {
        sync.release(1);
    }
	
	//AQS的release()
	public final boolean release(int arg) {
		
		//尝试释放锁 true:当前线程已经完全释放锁
		            false:当前线程尚未完全释放锁,因为锁是可重入的,重入几次,释放几次
		if (tryRelease(arg)) {

			//什么情况下有头结点?
			//情况一:当持锁线程拥有锁时,有其它线程想要拿锁时失败,此时队列是空队列,该线程会创建一个空结点
			//情况二:线程成为头结点的下一个结点时,被唤醒拿锁会成为新的头结点
			Node h = head;

			//头结点不为空且状态不为0:什么时候不为0?
			//当有结点插入时,会把前置结点状态置为-1,当然取消状态会被出队
			if (h != null && h.waitStatus != 0)
			
				//唤醒后继节点,具体后面会写
				unparkSuccessor(h);
			return true;
		}

		return false;
	}
	
	 //ReentrantLock中的Sync重写的tryRelease()
	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);
		}
		
		//更新AQS的state值
		setState(c);
		return free;
	}
	
	//AQS的unparkSuccessor():唤醒当前结点的第一个正常的后继结点
	private void unparkSuccessor(Node node) {
	
		//当前节点的状态
		int ws = node.waitStatus;
		
		
		//-1 Signal  
		if (ws < 0)
		
			//改成零的原因:当前结点要唤醒后继结点了,后继结点会把当前结点出队
			compareAndSetWaitStatus(node, ws, 0);

		//s是当前结点的第一个后继节点
		Node s = node.next;

		//s 什么时候为空?
		//情况一:当前结点是尾结点
		//情况二:当新结点入队未完成时,下面从后向前查找就是这个原因

		//s.waitStatus > 0 当前结点的后继节点是取消状态,找一个最近的可以被唤醒的结点
		if (s == null || s.waitStatus > 0) {
		
			//为什么从后向前查找?
			//因为如果此时有新结点正在入队,
			//第一步是新节点的prev指向尾结点
			//第二步设置新节点为尾结点   ----------- 如果这一步完成后,此时开始查找,最后一个链还没接上,因此需要从后向前找
			//第三步是旧尾结点next指向新尾结点
			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模板方法,而非公平锁先尝试抢锁

  • 公平锁和非公平锁,都重写了tryAcquire()方法:公平锁在抢锁前会判断阻塞队列是否有结点在等待锁,而非公平锁直接尝试抢锁

  • 非公平锁相对不公平的相同之处:抢锁失败的处理流程相同,解锁流程相同

Markdown

Markdown

ReentrantLock加锁响应中断

  • 响应中断的加锁方法lockInterruptibly()和lock()不同之处

  • 1.在拿锁前先判断中断标志位

  • 2.在线程拿锁失败被挂起期间如果被中断唤醒,抛出中断异常,在抛出异常前取消该结点cancelAcquire(),下面具体写

Markdown

Markdown

		
	//AQS的cancelAcquire():将该结点设置为取消状态
	private void cancelAcquire(Node node) {
	
		//空判断
		if (node == null)
			return;

		//当前线程已经不参与排队拿锁了,所以把结点的线程置为空
		node.thread = null;

		//当前结点的前驱
		Node pred = node.prev;

		//向前找非取消状态的结点
		while (pred.waitStatus > 0)
			node.prev = pred = pred.prev;

		//情况一:predNext是当前结点
		//情况二:predNext是 ws > 0 的节点
		Node predNext = pred.next;

		//将当前结点状态设置为取消状态  
		node.waitStatus = Node.CANCELLED;

		//情况一:当前结点是队尾结点,直接CAS改变队尾为pred:pred是前面找到的当前结点前面的非取消状态的结点
		if (node == tail && compareAndSetTail(node, pred)) {
		
			//修改pred.next=null
			compareAndSetNext(pred, predNext, null);

		} else {
			
			//情况二:当前结点不是队尾,也不是头结点的后继结点

			//保存结点状态
			int ws;

			//pred != head :说明当前结点不是head.next节点
			
			//(ws = pred.waitStatus) == Node.SIGNAL:说明pred的状态是SIGNAL状态   pred状态可能是0,也可能取消排队变为CANCELLED
			
			//(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)):假设pred状态是 <= 0 则设置pred状态为SIGNAL状态
			
			//pred.thread != null:pred并未取消
			if (pred != head &&
					((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
					pred.thread != null) {
				
				//pred -> node -> node.next
				//让pred.next = node.next:当node.next结点被唤醒后,该结点的shouldParkAfterFailedAcquire()会跳过取消状态的结点
				Node next = node.next;
				if (next != null && next.waitStatus <= 0)
					compareAndSetNext(pred, predNext, next);

			} else {
			
				//情况三:当前结点是头结点的后继结点,此时队列情况是: head -> 当前结点 -> 第三个结点...
				
				//这个时候用unparkSuccessor()去唤醒那个第三个结点,和情况二一样它会调用shouldParkAfterFailedAcquire()跳过取消状态的结点
				
				//在shouldParkAfterFailedAcquire()后,队列情况是head-> 第三个node  当前结点被出队
				unparkSuccessor(node);
			}

			node.next = node; // help GC
		}
	}