ReentrantLock底层原理分析


1 使用

ReentrantLock是Java并发包中互斥锁,它有公平锁和非公平锁两种实现方式,以lock()为例,其使用方式为:

		ReentrantLock takeLock = new ReentrantLock();
		// 获取锁
		takeLock.lock();
		try {
		  // 业务逻辑
		} finally {
		  // 释放锁
		  takeLock.unlock();
		}

1.1 tryLock

tryLock方法尝试锁定,不管锁定与否,方法都将继续执行。
可以根据tryLock的返回值判断是否锁定成功。

另外tryLock还可以设置等待时间,在等待时间内会不断申请锁,一旦失败则返回false,程序继续执行。

public class LockTest {
    public static void main(String[] args) {
        final ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            try {
                lock.tryLock();
                for (int i = 0; i < 10; i++) {
                    System.out.println("线程" + Thread.currentThread().getName() + "正在执行---");
                    TimeUnit.SECONDS.sleep(1);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println("线程" + Thread.currentThread().getName() + "结束执行");
            }
        }, "1");
        t1.start();
        new Thread(() -> {
            boolean f = false;
            try {
                System.out.println("线程" + Thread.currentThread().getName() + "尝试拿到锁");
                f = lock.tryLock(5, TimeUnit.SECONDS);
                if (f) {
                    System.out.println("线程" + Thread.currentThread().getName() + "拿到锁");
                } else {
                    System.out.println("线程" + Thread.currentThread().getName() + "没能拿到锁");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (f) {
                    lock.unlock();
                }
            }
        }, "2").start();

    }
}

程序输出结果:
在这里插入图片描述

1.2 lockInteruptibly

使用lockInterruptibly方法,可以对interrupt方法做出响应。注意synchronized一旦wait了,必须由其他线程notify才能醒来(即synchronized在阻塞时不能响应中断,使用lock方法也不能响应中断)。

public class LockTest {
    public static void main(String[] args) throws InterruptedException {
        final ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {

            try {
                lock.lockInterruptibly();
                Thread.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });
        t1.start();
        Thread.sleep(1000);

        Thread t2 = new Thread(() -> {
            boolean locked = false;
            try {
                System.out.println("线程2尝试拿到锁");
                locked = lock.tryLock(4, TimeUnit.SECONDS);
                if (locked) {
                    System.out.println("线程2拿到锁");
                } else {
                    System.out.println("线程2没能拿到锁");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (locked) {
                    lock.unlock();
                }
            }
        });
        t2.start();
        Thread.sleep(1000);
        t1.interrupt(); // 中断t1线程,使t1线程可以释放锁
    }
}

程序输出结果:
在这里插入图片描述

那么,ReentrantLock内部是如何实现锁的呢?接下来我们就以JDK1.7中的ReentrantLock的lock()为例详细研究下。

2 底层原理

ReentrantLock类实现了Lock和java.io.Serializable接口,其内部有一个实现锁功能的关键成员变量Sync类型的sync,定义如下:

    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

而这个Sync是继承了AbstractQueuedSynchronizer的内部抽象类,主要由它负责实现锁的功能。AbstractQueuedSynchronizer内部存在一个获取锁的等待队列及其互斥锁状态下的int状态位status(0当前没有线程持有该锁、n存在某线程重入锁n次)即可,该状态位也可用于其它诸如共享锁、信号量等功能。

Sync在ReentrantLock中有两种实现类:NonfairSync、FairSync,正好对应了ReentrantLock的非公平锁、公平锁两大类型。

2.1 lock()

/* Acquires the lock.
Acquires the lock if it is not held by another thread and returns immediately, setting the lock hold count to one.
If the current thread already holds the lock then the hold count is incremented by one and the method returns immediately.
If the lock is held by another thread then the current thread becomes disabled for thread scheduling purposes and lies dormant until the lock has been acquired, at which time the lock hold count is set to one. */
public void lock() {
   sync.acquire(1);
}

lock方法就是做上面这个事。其中acquire(1)的1表示锁的重入量增加1。

2.1.1 sync.acquire方法

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

tryAcquire表示尝试去获取锁。上述if的逻辑是首先尝试去获取锁(tryAcquire)。如果获取到,则直接返回;如果没获取到,则尝试把当前线程加入到队列里。

2.1.1.1 tryAcquire方法(以FairSync子类(公平锁)实现为例)
        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;
        }

首先,getState获取当前的sync对象里的state状态,即当前锁的状态。

  • state==0:表示当前锁还没有被任何线程获得。此时判断当前的等待队列中是否已经有线程正在等待(hasQueuedPredecessors())。根据公平锁的定义,线程会按照他们到来的顺序依次取得锁。因此如果有其他线程已经在等待锁,他们的优先级显然比当前这个线程要高。

    • hasQueuedPredecessors()==true:表示等待队列里有其他线程,此时tryRequire方法返回false。
    • hasQueuedPredecessors()==false:等待队列里没有线程。此时使用CAS的方式(compareAndSetState(0, acquires))尝试更改state。如果修改成功,就设置锁的拥有者为当前线程(setExclusiveOwnerThread(current));否则就退出方法,返回false。
  • state!=0 且锁的拥有者就是当前线程(current == getExclusiveOwnerThread()):说明当前是一个锁重入的情况,这样直接修改state即可。

  • 其他情况都认为锁申请失败,返回false。

2.1.1.2 addWaiter方法

addWaiter方法主要是创建Node节点并且尝试把这个节点添加到等待队列里。sync的等待队列是一个双向链表。每个Node保存的内容如下:

  • thread:保存绑定的线程
  • waitStatus:等待状态,用于判断后续节点是否需要被唤醒(unpark)
  • prev、next:当前节点的前驱、后驱节点
    在这里插入图片描述
/**
     * 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) {
    /**
    1、首先创建一个新的节点,设置好保存的线程
        Node(Node nextWaiter) {
            this.nextWaiter = nextWaiter;
            THREAD.set(this, Thread.currentThread());
        }
	*/
        Node node = new Node(mode);
		
		// 3、循环拿尾节点,直到成功位置
        for (;;) {
        /**
			2、拿到当前的尾节点。
		*/
            Node oldTail = tail;
            if (oldTail != null) {
            	/**
            	2.1 如果尾节点不为空,说明当前等待队列里已经有其他节点了。
            	将新生成的节点node的prev指向oldTail。
            	然后通过CAS的形式尝试把node设置为新的尾节点
            	【过程:比较当前的尾节点是否是oldTail,如果是表明尾节点没被修改,
            	自己可以把尾节点改为自己生成的node】。
            	*/
                node.setPrevRelaxed(oldTail);
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return node;
                }
            } else {
            /**
			2.2 如果尾节点为空,说明当前队列里并没有其他节点。
			执行队列初始化【创建一个节点并且把head和tail都指向该节点】。
            */
                initializeSyncQueue();
                /**
	                private final void initializeSyncQueue() {
	        			Node h;
	        			if (HEAD.compareAndSet(this, null, (h = new Node())))
	            			tail = h;
	    			}
                */
            }
        }
    }

Q:从上述过程可以发现,等待队列是通过双向链表实现的,并且头节点是一个冗余节点(dumy node),真正的第一个等待线程节点应该是head.next。为什么这么设计?
A:下面2.1.1.3节的acquireQueued方法是把新添加的节点里的线程给park掉。那么后续这个线程由谁来unpark?是它的前序节点来unpark的。即前序节点获得了锁,执行完毕后,它来unpark它的后续节点。

2.1.1.3 acquireQueued方法

该方法主要是将addWaiter方法中添加的Node节点中的线程给阻塞(通过LockSupport.park()方法)。

注意每个节点入队之后并不是立刻park。它首先要把前序节点的waitStatus设置为-1,然后再park。对于一个节点如果它获得了锁并且使用完毕,他会看自己的waitStatus,如果等于-1,表示后续有节点需要unpark,如果等于0说明后面没有节点要unpark。
在这里插入图片描述

  • 为什么不能直接unpark next节点?
    • 因为next节点有时候不一定要被unpark。比如节点在阻塞的过程中被中断了,它后面不再需要锁了,因此不需要给它unpark。
final boolean acquireQueued(final Node node, int arg) {
        boolean interrupted = false;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node))
                    interrupted |= parkAndCheckInterrupt();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            if (interrupted)
                selfInterrupt();
            throw t;
        }
    }

上述程序的解释:
首先拿到node的前驱节点。

  • 如果前驱节点为head,由于等待队列有一个dumy节点,因此前驱节点为head说明当前这个node节点就是第一个(唯一一个)线程节点。考虑到之前进行的节点构造、入队等操作消耗了一定时间,此时再次尝试去获取锁(tryAcquire(arg)。注意该方法是尝试让当前线程去获得锁。当前线程正是node节点里保存的线程)。如果获取成功,则把队列里之前添加的node节点删除。这里的删除操作是把node设置为头(setHead(node)方法会把node的thread和next都置为null),且把原先的头p的后继设为null。
  • 如果再次获取锁失败,则执行shouldParkAfterFailedAcquire方法。该方法本质就是修改前驱节点的waitStatus。
    • 如果ws已经是-1(SIGNAL)了,直接返回true。
    • 否则如果ws是一个大于0的数,说明前驱节点pred被取消了(不再需要锁了),把所有ws大于0的前驱节点都跳过,更新节点之间的连接关系。
    • 否则如果ws是一个小于0的数,尝试以CAS的方式将前驱节点的ws修改为-1。
  • 情况2和3最后都会返回false。之后又重新尝试让node里的线程去获取锁(如果它是第一个节点),并且再次执行shouldParkAfterFailedAcquire。直到前驱节点p的waitStatus已经设置为了-1为止。
  • 最后通过parkAndCheckInterrupt方法把当前的对象(sync)上锁。
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == 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.
             */
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        return false;
    }
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

3 总结

ReentrantLock底层上锁原理:

|- 首先尝试让当前线程获取锁【tryAcquire(arg)】。
	|- 当前锁的状态为0:没有线程使用锁,直接把设置该锁对象被当前线程拥有。
	|- 当前锁的状态>0:检查是否是重入锁。是重入锁就直接返回true,不是则返回false表示获取锁失败
|- 如果获取锁失败【tryAcquire(arg)返回了false】
	|- 构造Node节点保存当前线程,并且把node插入到等待队列尾。注意这个入队方法会循环执行保证入队一定成功。
	|- 把当前线程park掉。在这之前如果当前线程是等待队列里唯一一个线程,会先尝试让当前线程再次获取锁;如果获取失败,就把它的前驱节点的waitStatus置为-1,然后park掉该线程。
  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值