java锁之ReentrantLock及Condition

前言

最近在看java的LinkedBlockingQueue数据结构时,发现里面使用了ReentrantLock,为了更好的理解LinkedBlockingQueue的线程安全原理,就不得不搞清楚ReentrantLock的背后原理,本篇文章详细介绍ReentrantLock的加锁、解锁、公平锁、非公平锁的幕后故事。

1、ReentrantLock

1.1、ReentrantLock的数据结构

        ReentrantLock中有一个Sync对象sync,Sync继承自AbstractQueuedSynchronizer(AQS)。Sync的具体实现有两个NonfairSync(非公平锁)和FairSync(公平锁),继承关系如下:

java锁之ReentrantLock及Condition_第1张图片

因为都继承自AQS,所以无论是NofairSync还是FairSync两种锁的数据结构是一样的,主要的类成员如下:

java锁之ReentrantLock及Condition_第2张图片

-- head,tail:保存等待获取锁的一个链表队列,head指向链表头,tail指向链表尾

-- state:状态,如果大于0,说明锁已被使用。

-- exclusiveOwnerThread:保存占据锁的线程

1.2 ReentrantLock的初始化

 ReentrantLock有两个构造函数,默认构造函数使用的是非公平锁;如果设置参数为true,则为公平锁,代码如下:

public ReentrantLock() {
	sync = new NonfairSync(); //作者注:默认非公平锁
}

public ReentrantLock(boolean fair) {
	sync = fair ? new FairSync() : new NonfairSync(); //作者注:通过fair决定使用公平锁还是非公平锁
}

2、公平锁

        在调用ReentrantLock的lock方法时,实际上最终调用到Sync的lock方法,当锁是公平锁时,调用到FairSync的lock方法,整个获取锁的代码调用过程如下:

//作者注:FairSync的lock方法
final void lock() {
    acquire(1);
}

//作者注:AQS的acquire方法
public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

//作者注:FairSync的tryAcquire
protected final boolean tryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		if (!hasQueuedPredecessors() &&
			compareAndSetState(0, acquires)) {
			setExclusiveOwnerThread(current);
			return true;
		}
	}
	else if (current == getExclusiveOwnerThread()) {
		int nextc = c + acquires;
		if (nextc < 0)
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	return false;
}

1、调用tryAcquire方法尝试去获取锁:

        -- 如果当前锁没有线程使用(state=0)并且队列里面没有等待的线程(!hasQueuedPredecessors()),则获取锁,将state置为1且exclusiveOwnerThread置为当前线程,获取成功返回true;

        -- 如果锁已被当前线程获取,则将state=state+1,获取成功返回true。(这种叫做重入锁,也就是同一个线程内可以多次获取锁,当然一个线程获取了几次锁,最终也要释放几次),

        -- 获取锁失败,返回false。

2、如果获取锁失败,则调用addWaiter(Node.EXCLUSIVE), arg)方法,将当前线程放到等待队列里面,等待别的线程释放锁。假如thread-0获取了锁,未释放状态下,后续到来要获取锁的线程都放到链表里面去。具体数据结构如下:

(这个图是一个大概的情况,里面涉及的一些细节没有展示,比如head指向的其实是一个空的节点,空的节点后面才跟实际的线程节点)

java锁之ReentrantLock及Condition_第3张图片

3、非公平锁

非公平锁和公平锁的加锁过程的唯一区别如下:

//作者注:NonFairSync的lock方法
final void lock() {
	if (compareAndSetState(0, 1)) //作者注:直接尝试获取锁,有可能插队成功。
		setExclusiveOwnerThread(Thread.currentThread());
	else
		acquire(1); //作者注:如果获取失败,也要乖乖的去队列排队去。
}

由2中知,公平锁在获取锁之前,先去判断有没有其他线程占用锁(state是否等于0)以及队列里面是否有等待的线程,如果有,则将当前线程加入到等待队列。

但是非公平锁不判断等待队列里是否有等待的线程,而是直接尝试去重新设置锁的状态(compareAndSetState),如果设置成功,说明锁已被释放,该线程直接占有锁。所以我的理解就是:

公平锁:所有线程都要先判断队列里是否有等待的线程,如果有,要乖乖的到队列里去排队。

非公平锁:不判断队列里是否有等待的线程,直接尝试获取锁(也就是插队),可能比队列里的线程优先获得锁。

当然,如果当前线程没有获取到锁,最终还是要乖乖的去队列里排队去(acquire(1)后续的执行过程和公平锁是一模一样的)。

4、锁的释放

锁的释放入口函数如下:

//作者注:AbstractQueuedSynchronizer里的方法
public final boolean release(int arg) {
	if (tryRelease(arg)) {
		Node h = head;
		if (h != null && h.waitStatus != 0)
			unparkSuccessor(h);
		return true;
	}
	return false;
}

//作者注:Sync里的方法
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);
	}
	setState(c);
	return free;
}

1、tryRealease(arg)中将state置为0,且将exclusiveOwnerThread置为null,

2、具体队列里的线程重新获取锁是在什么地方调用的,我没找到。(不是unparkSuccessor这个方法)

5、Condition

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。

在实际使用中,Condition配合Sync进行使用,为了了解Condition和Sync的关系,我通过以下方式建立了两个Contition,运行之后进行debug,看看这两个Condition里面都有什么。

ReentrantLock lock = new ReentrantLock();
Condition cond1 = lock.newCondition();
Condition cond2 = lock.newCondition();

debug看一下对象里面的信息:

java锁之ReentrantLock及Condition_第4张图片

可以看到cond1和cond2共用一个Sync。所以通过上述建立Condition的方式,知道生成的数据结构如下:

java锁之ReentrantLock及Condition_第5张图片

等待线程和被阻塞线程的区别是:

等待线程:有权利获取锁。

被阻塞线程:没有权利获取锁,需要被唤醒后加入到AQS等待队列才有权利获取锁。

5.1、await和signal:手动阻塞线程和唤起线程

调用condition的await方法,旨在说明要对当前线程进行堵塞,并释放当前线程持有的锁,将当前线程添加到condition的阻塞队列中,调用代码如下:

public final void await() throws InterruptedException {
	if (Thread.interrupted())
		throw new InterruptedException();
    //作者注:将当前线程添加到Condition的阻塞线程队里的末尾
	Node node = addConditionWaiter();
    //作者注:释放当前线程持有的锁
	int savedState = fullyRelease(node);
	int interruptMode = 0;
	while (!isOnSyncQueue(node)) {
        //作者注:将当前线程挂起
		LockSupport.park(this);
		if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
			break;
	}
    //作者注:如果当前线程被唤起,尝试去获取锁
	if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
		interruptMode = REINTERRUPT;
	if (node.nextWaiter != null) // clean up if cancelled
		unlinkCancelledWaiters();
	if (interruptMode != 0)
		reportInterruptAfterWait(interruptMode);
}

为了更直观的看到执行过程,我们使用简单的代码进行debug测试,写一个如下简单的代码:

public class ReentrantLockTest {

    public static final ReentrantLock lock = new ReentrantLock();

    public static final Condition condition = lock.newCondition();

    public static class Td1 extends Thread {
        ReentrantLock lock ;
        Condition condition ;
        public Td1(ReentrantLock lock,Condition condition){
            this.lock = lock;
            this.condition = condition;
        }
        @Override
        public void run() {

            try {
                lock.lock();      // 代码1

                condition.await(); //代码2

                System.out.println("thread-0");

                lock.unlock(); //代码3
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static class Td2 extends Thread {
        ReentrantLock lock ;
        Condition condition ;
        public Td2(ReentrantLock lock,Condition condition){
            this.lock = lock;
            this.condition = condition;
        }
        @Override
        public void run() {

            try {
                Thread.sleep(5000); //为了保证先运行线程1,后运行线程2,在这里暂停5秒

                lock.lock(); //代码4

                condition.signal(); //代码5

                System.out.println("thread-1");

                lock.unlock(); //代码6
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Td1 td1 = new Td1(lock,condition);
        Td2 td2 = new Td2(lock,condition);

        td1.start();
        td2.start();
    }
}

1、运行到代码1:可以看到lock的debug数据如下,锁被Thread-0占据(exclusiveOwnerThread=thread-0,state=1):

java锁之ReentrantLock及Condition_第6张图片

 2、运行到代码2:因为此时线程被挂起,为了看到具体数据,我们debug到await代码内部的int interruptMode = 0这一行,可以看到debug数据如下:thread-0被添加到condition的队列中,而锁被释放(exclusiveOwnerThread=null,state=0)

java锁之ReentrantLock及Condition_第7张图片

 3、运行到代码4:此时thread-0还在condition队列,而锁被thread-1占据

java锁之ReentrantLock及Condition_第8张图片

4、运行到代码5:唤起condition,将thread-0从condition队列清除,添加到lock的等待队列;但因为此时thread-1还没有释放锁,锁还是被thread-1持有。

java锁之ReentrantLock及Condition_第9张图片

 5、运行到代码6:此时thread-1释放锁,因为运行速度较快,没有看到锁空闲时段,就已经被thread-0捕获到:

java锁之ReentrantLock及Condition_第10张图片

 6、此时thread-0已经被唤起,代码运行到代码3处,thread-0释放锁整个代码运行结束。

以上就是ReentrantLock和Condition的配合使用整个流程,从debug过程中,可以清晰的看到整个内部数据结构和锁持有的变化情况。

6、再次说明

本篇文章从整体的锁的持有变化及ReentrantLock和Condition内部数据结构进行了框架性的说明,其实内部的锁实现机制有很多细节,在这里并没有展示,因为作者的目的是让大家看清楚锁是如何被获取和释放的。如果想了解具体的细节机制,可以参考下面这个作者写的,非常详细,但是需要仔细的品味,想过一遍就看懂是不太可能的。

ReentrantLock锁内部详细实现机制

你可能感兴趣的:(java,数据结构系列,java)