java之ReentrantLock

在讲RentrantLock之前需要先讲一下AQS和LockSupport,因为rentrantLock底层是用AQS实现的,而AQS中获取阻塞和唤醒底使用LockSupport实现的。

1、LockSupport实现

下面代码中,LockSupport.park方法是当前线程等待,直到获得许可,LockSupport.unpark方法则将线程作为参数传递,释放许可,让myThread能继续运行。

    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread() +": start park");
            LockSupport.park();
            System.out.println(Thread.currentThread() +": end park");
        }
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        System.out.println(Thread.currentThread() +": start unpark");
        LockSupport.unpark(myThread);
        System.out.println(Thread.currentThread() +": end unpark");
    }

park和unpark方法类似于object默认的wait和notify,区别在于:

1、Object.wait需要在同步代码块中执行,而park则不需要

2、wait当中断时,则会响应中断抛出中断异常,park不需要

3、object.wait是对象才能调用,而park则任何线程中都能调用

正因为park和unpark是对线程的阻塞和唤醒。适合运用到 ReentrantLock的并发线程中,其他线程阻塞和唤醒中。

2、AQS

网上有很多讲解AQS机制的,都列出一大堆源码,这里都不在多说源码了,简单说一下基本原理,至于细节部分,可以直接看源码后细扣。AQS核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

2.1 AQS的数据结构

AQS底层数据结构是一个CLH的虚拟双向队列,实际上是一个双向链表,而请求共享需要等待的线程则会加入同步队列中,如果使用condition,则会加入到等待队列中,每一个线程使用以下图中Node节点表示。Node节点包含pre,next,当前线程,waitStatus。

其中waitStatus包含四种状态:

// CANCELLED,值为1,表示当前的线程被取消
static final int CANCELLED =  1;
// SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark 
static final int SIGNAL    = -1;
// CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中
static final int CONDITION = -2;
// PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行 
static final int PROPAGATE = -3;
值为0,表示当前节点在sync队列中,等待着获取锁

java之ReentrantLock_第1张图片

3、 ReentrantLock实现原理

知道了上述两个知识点之后,lock.lock和lock.unlock方法内部到底是怎么实现的呢?

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        MyThread t1 = new MyThread("t1", lock);
        MyThread t2 = new MyThread("t2", lock);
        t1.start();
        t2.start();
    }
   static class MyThread extends Thread {
        private Lock lock;
        public MyThread(String name, Lock lock) {
            super(name);
            this.lock = lock;
        }

        @Override
        public void run() {
            lock.lock();
            try {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("thread ==== " + Thread.currentThread() + " running");
            } finally {
                lock.unlock();
            }
        }
    }

上述代码中t1 和t2同时执行,当t1执行lock之后,t2则等待lock释放锁,等到t1执行unlock之后,t2则被唤醒执行下面操作。内部具体流程是怎么样的?

java之ReentrantLock_第2张图片

1、当t1执行lock之后,由于此时只有t1这一个线程,则直接将此线程设置为独占锁

2、当t2执行lock之后, 由于t1已经是独占锁了。所以此时t2则需要加入到同步队列中

(1)先初始化同步队列,创建一个head节点,

(2)将head指向t2线程

(3)将head节点状态设置为SIGAL,当前节点的后继节点包含的线程需要运行,可以执行unpark

(4)对t2执行LockSupport.park(),阻塞t2线程

java之ReentrantLock_第3张图片

3、t1执行unlock,除了释放自身的独占锁之后,后续对同步队列中的t2线程也有相关操作

(1)从后往前找,找到状态设置为SIGAL的节点,是head节点,然后对后续节点执行unpark操作,上述我们知道unpark,则是唤醒t2线程

(2)将head节点状态设置为0

java之ReentrantLock_第4张图片

4、由于之前t2在lock过程中,底层AQS代码是一个自旋,不断获取资源,t2线程被唤醒执行后,将t2自身将清空,head指向t2, next为null;最终达到的状态是sync queue中只剩下了一个结点,并且该节点除了状态为0外,其余均为null。

java之ReentrantLock_第5张图片

5、t2执行unlock,最后的状态和之前的状态是一样的,队列中有一个空节点,头节点为尾节点均指向它。

java之ReentrantLock_第6张图片

你可能感兴趣的:(java,java,开发语言)