【Java学习笔记】多线程 Part 3 - ReentrantLock

1. ReentrantLock 加锁原理

简单测试一下两个线程分别给count变量做加1操作20000次

class Process implements Runnable
{
	int count = 0;
	private ReentrantLock lock = new ReentrantLock();

	public void increment()
	{
		count++;	
	}
	
	public void process()
	{
		for (int i = 0; i < 20000; i++)
		{			
			lock.lock();
			try
			{
				increment();
			}
			finally
			{
				lock.unlock();
			}
		}
	}
	
	@Override
	public void run() {
		 process();
	}
}
public class TestLocks {
	public static void main(String[] args) throws InterruptedException {
		Process obj = new Process();
		Thread t1 = new Thread(obj);
		Thread t2 = new Thread(obj);
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(obj.count);
	}
}

输出:

40000

但当我们使用ReentrantLock加锁的时候,到底发生了什么?

通过源码分析(基于Java 8):

  1. 从执行 lock() 方法开始
lock.lock();
  1. 找到内置 sync 的 lock() 方法
public void lock() {
	sync.lock();
}
  1. 找到公平锁的实现方法
final void lock() {
    acquire(1);
}
  1. 进入 acquire() 方法
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  1. 进入 tryAcquire() 方法,如果锁自由,那判断当前线程要不要排队
/**
  * Fair version of tryAcquire.  Don't grant access unless
  * recursive call or no waiters or is first.
  */
 protected final boolean tryAcquire(int acquires) {
     final Thread current = Thread.currentThread(); //得到当前线程
     int c = getState(); //得到锁状态
     if (c == 0) {
     	 //如果锁状态为0,即锁自由,判断当前线程要不要排队
         if (!hasQueuedPredecessors() &&
             compareAndSetState(0, acquires)) {
             setExclusiveOwnerThread(current);
             return true;
         }
     }
     //如果锁状态不为0,判断当前线程是否持有锁,是的话,进行重入
     else if (current == getExclusiveOwnerThread()) {
         int nextc = c + acquires; //acquires值为1,给锁状态再加1
         if (nextc < 0)
             throw new Error("Maximum lock count exceeded");
         setState(nextc); //更新锁状态
         return true;
     }
     return false;
 }
  1. 我们进入 hasQueuedPredecessors() 方法,判断当前线程要不要去队列排队,要的话返回true
public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
  1. 如果当前线程能拿到锁的话,回到第3步 acquire() 方法就执行结束了,否则要继续下去,我们先进入 addWaiter() 方法
/**
 * Creates and enqueues node for current thread and given mode.
 *
 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
 * @return the new node
 */
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) {
    	//队尾节点next指向当前节点,当前节点prev指向队尾节点,更新队尾为当前节点
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果队列不存在,进入enq()
    enq(node);
    return node;
}
  1. 进入 enq()方法,把当前node放入队列
/**
 * Inserts node into queue, initializing if necessary. See picture above.
 * @param node the node to insert
 * @return node's predecessor
 */
private Node enq(final Node node) {
    for (;;) {
        Node t = tail; 							//拿到队尾
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node())) 	//初始化队首,此时注意,head的thread为空
                tail = head;					//队首队尾相连
        } else {
        	//有队列了,把当前节点放入队列
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
  1. 当前node进入队列了,然后我们进入 acquireQueued() 方法。如果当前node是队列里第一个排队的,自旋一次;如果不是要去设置前一个节点的状态,然后回来再自旋一次,再没拿到锁才阻塞
    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    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)) {		//如果前一个节点是head那说明当前节点是队列里第一个排队的,尝试获取锁,自旋一次
                    setHead(node);						//如果得到锁了,队列往前移,当前节点变成队首
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //如果没有拿到锁,准备park阻塞
                if (shouldParkAfterFailedAcquire(p, node) && //如果前一个节点状态不是-1,要置为-1,然后循环回去再尝试一遍tryAcquire(arg),也就是再一次自旋
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  1. 进入 shouldParkAfterFailedAcquire() 方法,要把前一个节点状态置为Node.SIGNAL也就是-1
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus; //拿到前一个节点的状态
    if (ws == Node.SIGNAL) //判断前一个节点的状态是不是Node.SIGNAL
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //把前一个节点的状态置为-1
    }
    return false;
}
  1. parkAndCheckInterrupt() 方法才真正的阻塞线程
/**
 * Convenience method to park and then check if interrupted
 *
 * @return {@code true} if interrupted
 */
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

ReentrantLock加锁的流程图

ReentrantLock加锁的流程图

有几个需要注意的地方:

  • 队列里面的队首head的thread永远都是null
  • 在head之后的一个node才是第一个在排队的线程
  • node进队列后,阻塞之前,会有两次自旋尝试能否拿到锁
  • 每个node的状态永远都是后一个node来设置,这样设计是因为线程自己睡眠之后便无法执行代码,只能由后面线程来设置它的节点为睡眠状态
  • 持有锁的线程永远都不在队列里

这里我们再看几个细节:

如果在队列里的线程自旋拿到锁了,会怎么样?

队列会往前移,但这个过程有哪些步骤呢?在 acquireQueued() 方法里,源码是这样的:

if (p == head && tryAcquire(arg)) {
    setHead(node);
    p.next = null; // help GC
    failed = false;
    return interrupted;
}

其中 setHead() 是这样的:

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

那就很清晰了,分为5步:

  1. 把当前线程节点设为head头结点
  2. thread置空
  3. 当前节点的前一个节点置空
  4. 原本的head头结点的后一个节点置空,也就是把原本的头结点从队列里分离开来了,那它不指向任何引用了,就会被垃圾回收
  5. failed 置为 false

hasQueuedPredecessors() 方法在判断要不要排队的时候,到底在判断什么?

public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

这个方法返回false代表不用排队,而且注意这个方法只有在锁状态为0时才会执行

它也分为几个步骤:

  1. h != t ?
    h != t 就说明队列里有线程正在排队,反过来 h==t 说明队列里没有线程在排队,这个时候无非是队列没有初始化,就是初始化了,队首队尾相连,而还没有线程进来排队。既然没有线程在排队(包括当前线程自己),当前线程就不需要去排队,此时锁状态为0,当前线程可以直接拿到锁。

  2. (s = h.next) == null ?
    如果会执行这个,说明 h!=t,也就是队列里有线程在排队。那这里这句就是在判断头结点的后一个是不是为空。一般情况下,队首在队列里好好的,队列里又有线程在排队,队首后一个不会为空,返回false。但如果为空的话,那后一句 s.thread 就根本无法执行了。

  3. s.thread != Thread.currentThread() ?
    队列里有线程在排队,队首后一个也确实不为空,那就要判断当前线程是不是第一个在排队的线程。如果是的话,就不用排队,锁状态为0可以去拿锁;不是的话,说明前面还有线程在排队,那当前线程当然只能乖乖排队。

2. ReentrantLock 解锁原理

还是从源码分析:

  1. 解锁从 unlock() 开始
public void unlock() {
    sync.release(1);
}
  1. 进入 release() 方法,如果释放锁成功,返回true
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() ,如果释放锁成功返回true
protected final boolean tryRelease(int releases) {
    int c = getState() - releases; 								//release值为1,将锁状态减1
    if (Thread.currentThread() != getExclusiveOwnerThread()) 	//如果当前线程,也就是要释放锁的线程,不是持有锁的线程,那就出问题了
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {												//如果锁状态为0,那锁就释放成功了,将持有锁的线程置空
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);												//更新锁状态
    return free;
}
  1. 如果释放锁成功,会进入unparkSuccessor()方法,唤醒传入节点后一个线程,如果有的话
/**
  * Wakes up node's successor, if one exists.
  *
  * @param node the node
  */
 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)													//队首节点在睡眠时状态是-1,这里把它置为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) {							
     	 //如果队首后的节点是空,或者状态大于1,也就是被取消,我们从队列尾部往前遍历,寻找第一个在队列里面非空切状态小于等于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); //找到要被唤醒的线程节点,然后唤醒这个线程
 }

ReentrantLock解锁的流程图

解锁流程图
需要注意的地方:

  • 每次 tryRelease() 都是给锁状态减1,那么减1之后的锁状态一定为0吗?当然是不一定的,也就是说原有的锁状态大于1。这种情况就是重入。在加锁时,如果要加锁的是持有锁的线程,那就会重入,锁状态会继续加1。那显然,如果加锁了两次,也要解锁两次,锁状态才会回到0,否则队列里排队的线程永远拿不到锁。
  • 那么锁状态会被减为负吗?如果加锁一次,试图解锁两次,第一次解锁成功之后,线程就不再拥有锁,第二次再解锁就会报异常,因为当前线程不持有锁却试图解锁,非法。

这里我们就写个代码测试一下锁重入

t1线程先启动,故意加锁两次,再解锁两次

public class TestLocks {
	public static void main(String[] args) throws InterruptedException {
		ReentrantLock reentrantLock = new ReentrantLock(true); //公平锁
		Thread t1 = new Thread(()->{
			//加锁两次
			reentrantLock.lock();
			reentrantLock.lock();
			try {
				System.out.println("t1");
			}
			finally {
				//解锁两次
				reentrantLock.unlock();
				reentrantLock.unlock();
			}
		});
		
		Thread t2 = new Thread(()->{
			reentrantLock.lock();
			try {
				System.out.println("t2");
			}
			finally {
				reentrantLock.unlock();
			}
		});

		t1.start();		
		Thread.sleep(100); //让t1先启动,t2再启动
		t2.start();
		t1.join();
		t2.join();
		
		System.out.println("end"); //等待线程都执行完毕后,打印代表程序执行结束
	}
}

输出:

t1
t2
end

t1 加锁两次,解锁两次后,t2拿到了锁,执行完毕程序结束

现在我们让 t1 加锁两次,但只解锁一次

public class TestLocks {
	public static void main(String[] args) throws InterruptedException {
		ReentrantLock reentrantLock = new ReentrantLock(true); //公平锁
		Thread t1 = new Thread(()->{
			//加锁两次
			reentrantLock.lock();
			reentrantLock.lock();
			try {
				System.out.println("t1");
			}
			finally {
				//解锁一次
				reentrantLock.unlock();
			}
		});
		
		Thread t2 = new Thread(()->{
			reentrantLock.lock();
			try {
				System.out.println("t2");
			}
			finally {
				reentrantLock.unlock();
			}
		});

		t1.start();		
		Thread.sleep(100); //让t1先启动,t2再启动
		t2.start();
		t1.join();
		t2.join();
		
		System.out.println("end"); //等待线程都执行完毕后,打印代表程序执行结束
	}
}

输出:

t1

t2 没有拿到锁。t1 释放一次锁后,在队列里的 t2 被唤醒了一次,但由于锁状态不为0,它没有获取锁,继续睡眠了。

3. ReentrantLock 处理中断

举个例子,银行ATM,银行卡插进去了,要你输密码,你过了3分钟都没有输密码,会怎么样?ATM就会把你的银行卡吐出来,中断业务受理。这就相当于队列里的线程一直在等待唤醒,等待锁状态为0,而持有锁的线程一直在办业务,等待时间超出了限制,那队列里的线程就应该中断,不等了。

ReentrantLock 有个方法 lockInterruptibly() 就可以实现这个工作,我们写个程序测试一下

public class TestLocks {

	static ReentrantLock reentrantLock = new ReentrantLock(true);
	
	public static void process()
	{
		try {
			reentrantLock.lockInterruptibly();								//用lockInterruptibly()加锁
			System.out.println(Thread.currentThread().getName()+" start");	//打印当前线程名字
			Thread.sleep(5000);												//处理业务很久
			System.out.println(Thread.currentThread().getName()+" finish");	//业务结束
		} catch (InterruptedException e) {
			//lockInterruptibly()抛出的异常,当有线程中断时就会抛出异常
			System.out.println("interrupt " +Thread.currentThread().getName()+" thread");
			e.printStackTrace();
		}
		finally {
			reentrantLock.unlock();
		}
	}
	
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(()->{ process(); },"t1");
		Thread t2 = new Thread(()->{ process(); },"t2");
		Thread t3 = new Thread(()->{ process(); },"t3");
		
		//t1, t2, t3的顺序启动线程
		t1.start();		
		Thread.sleep(100);
		t2.start();
		Thread.sleep(100);
		t3.start();
		//过1秒,t2 中断
		Thread.sleep(1000);
		t2.interrupt();
	}
}

输出:
在这里插入图片描述

  • t1 开始,先得到锁,t2,t3入队列
  • 过了1秒,t2不等了,中断
  • 再过一会儿 t1 完成,释放锁
  • t3 得到锁,开始执行,执行完,释放锁

我们去源码看看lockInterruptibly()到底做了什么

  1. 进入lockInterruptibly()
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}
  1. 进入acquireInterruptibly(1)
public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg)) //同样的尝试获取锁的方法
        doAcquireInterruptibly(arg);
}
  1. 进入doAcquireInterruptibly(),这个其实和之前加锁过程的acquireQueued()很像
/**
  * Acquires in exclusive interruptible mode.
  * @param arg the acquire argument
  */
 private void doAcquireInterruptibly(int arg)
     throws InterruptedException {
     final Node node = addWaiter(Node.EXCLUSIVE); //把node放入队列,这个和之前lock()入队的过程一样
     boolean failed = true;
     try {
         for (;;) {
             final Node p = node.predecessor(); //这里自旋一次判断是不是可以拿到锁,队列要不要往前移,和lock()时的也一样
             if (p == head && tryAcquire(arg)) {
                 setHead(node);
                 p.next = null; // help GC
                 failed = false;
                 return;
             }
             if (shouldParkAfterFailedAcquire(p, node) && //这里和之前也一样,如果park了返回ture
                 parkAndCheckInterrupt()) //这个我们需要重新进去看看
                 throw new InterruptedException(); //这里不一样,抛出了异常
         }
     } finally {
         if (failed)
             cancelAcquire(node);
     }
 }
  1. 我们进到parkAndCheckInterrupt()看看
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

似乎没有什么高深的地方,阻塞了线程之后,返回了当前线程是不是被中断的判断,但其实这里大有文章。

在这里需要先了解一下,中断线程t2.interrupt()做了啥?

其实它啥也没做,就是设置了一个标记,真正的中断工作是本地方法实现的而不是Java实现的。

//interrupt()方法源码
public void interrupt() {
    if (this != Thread.currentThread()) {
        checkAccess();

        // thread may be blocked in an I/O operation
        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupted = true;
                interrupt0();  // inform VM of interrupt
                b.interrupt(this);
                return;
            }
        }
    }
    interrupted = true; //设置标记
    // inform VM of interrupt
    interrupt0(); //这个是本地方法
}

Thread.interrupted()做了啥,写个测试看一看

public class TestInterrupt {
	public static void main(String[] args) {
		Thread.currentThread().interrupt();
		System.out.println(Thread.interrupted());
		System.out.println(Thread.interrupted());
	}
}

输出:

true
false

首先当前线程先中断,Thread.interrupted()判断了一下,确实它中断了。但是当我再让它判断一次的时候,它却返回了false。这说明Thread.interrupted()第一次判断的时候,还作了一次标记重置。这属于非用户行为。标记重置的目的是为了方便用户下次中断线程时,可以正常操作。

那我们按照这个思路再回到源码看,回到parkAndCheckInterrupt()

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

假设 t2 现在已经park了,然后用户中断了 t2,那么这里parkAndCheckInterrupt()就会返回true。回到doAcquireInterruptibly(),此时就会抛出中断的异常

private void doAcquireInterruptibly(int arg){
	throws InterruptedException {
	final Node node = addWaiter(Node.EXCLUSIVE);
	boolean failed = true;
	try {
	    for (;;) {
			/***省略这里的代码***/
		    if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
		          throw new InterruptedException(); //此时这里就会执行,抛出中断的异常,然后往finally走
	    }
	} finally {
	    if (failed) //此时failed为true,进入cancelAcquire()
	        cancelAcquire(node);
	}
}
  1. 进入cancelAcquire(),简单的说,就是把node的thread置空,把node的状态置为CANCELLED,并且从队列里移除,交给垃圾回收。如果node原本是队列里第一个排队的,那就唤醒它的后一个节点。这也就是为什么队列里可能存在状态为CANCELLED的节点;也解释了之前t1, t2, t3那个例子中,t2中断后,t3如何顺利拿到锁。
private void cancelAcquire(Node node) {
     // Ignore if node doesn't exist
     if (node == null)
         return;

     node.thread = null;

     // Skip cancelled predecessors
     Node pred = node.prev;
     while (pred.waitStatus > 0)
         node.prev = pred = pred.prev;

     // predNext is the apparent node to unsplice. CASes below will
     // fail if not, in which case, we lost race vs another cancel
     // or signal, so no further action is necessary.
     Node predNext = pred.next;

     // Can use unconditional write instead of CAS here.
     // After this atomic step, other Nodes can skip past us.
     // Before, we are free of interference from other threads.
     node.waitStatus = Node.CANCELLED;

     // If we are the tail, remove ourselves.
     if (node == tail && compareAndSetTail(node, pred)) {
         compareAndSetNext(pred, predNext, null);
     } else {
         // If successor needs signal, try to set pred's next-link
         // so it will get one. Otherwise wake it up to propagate.
         int ws;
         if (pred != head &&
             ((ws = pred.waitStatus) == Node.SIGNAL ||
              (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
             pred.thread != null) {
             Node next = node.next;
             if (next != null && next.waitStatus <= 0)
                 compareAndSetNext(pred, predNext, next);
         } else {
             unparkSuccessor(node);
         }

         node.next = node; // help GC
     }
 }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值