并发编程(5)-深入理解ReentrantLock原理

并发编程学习目录

并发编程(1)-java中的6中线程状态
并发编程(2)-怎么中断线程?
并发编程(3)-synchronized的实现原理
并发编程(4)-深入理解volatile关键字

1、ReentrantLock代码实战
public class ReentrantLockDemo {
    private static volatile int count;
    
    static List<Thread> threads = new ArrayList<>();
	// 创建可重入锁
    private static Lock lock = new ReentrantLock();

     public static void inc() {
        // 获取锁
        lock.lock();
        count = count +1;
        System.out.println(count);
        dec();
        // 释放锁
        lock.unlock();
        
    }
    public static void dec() {
        // 在次获取锁(锁的重入)
        lock.lock();
        count = count -1;
        System.out.println(count);
        lock.unlock();
    }
	
    public static void main(String[] args) {
        // 创建3000个线程
        for (int i = 0; i <3000 ; i++) {
            Thread thread = new Thread(()->{
               // 对count变量进行修改
                inc();
            });
            threads.add(thread);
        }
        // 启动线程
        for (int i = 0; i <threads.size() ; i++) {
            threads.get(i).start();
        }
    }

}

例子中我们用重入锁ReentrantLock的lock和unlock解决了线程问题。
什么是重入锁?为什么这么设计?
就是获取到锁的线程可以在再次获取到锁,例子中线程进入到了inc方法,在没有释放锁的情况下,可以继续获取到锁进入到dec方法。这么设计是为了防止死锁。如果不这么设计,到了inc方法在进入到dec方法,就会产生线程A在等待线程A释放锁。
我们下面来分析一下它是怎么实现的

2、ReentrantLock源码解读
2.1 设计思想与猜想

平常我们会根据一些业务需求,去思考业务实现,而我们在看一些技术的源码的时候,也可以思考,如果这个技术让你实现,你会怎么去实现,带着这种问题和你猜想,看源码会有一种新的领悟。
技术需求

  • 锁是互斥的,就是对共享资源的抢占
  • 没有抢占到的线程需要阻塞唤醒
  • 阻塞的线程需要保存
  • 锁的公平性和非公平
  • 锁的重入性

技术猜想

  • 锁的互斥可以用一个变量进行存储,比如state来表示,0表示无锁,1表示有锁
  • 锁的阻塞唤醒可以用wait/notify来实现,但是无法将指定的线程唤醒,我们可以用LockSupport.park();来阻塞和LockSupport.unpark()来唤醒
  • 阻塞的线程可以用双向链表来保存
  • 公平性与否可以用逻辑实现
  • 用一个变量去存储当前抢占到锁的线程Id。
2.2 公平锁和非公锁的区别

公平锁:线程A已经获取到锁,线程B和线程C在等待,这个时候线程D在来访问,也会排队等待。公平锁符合FIFO(先进先出),下一个就是线程B获取到锁。
非公平锁:线程A已经获取到锁,线程B和线程C在等待,这个时候线程D在来访问,会尝试获取锁,如果线程A刚好结束,线程D就有可能先于线程B获取到锁。

2.3 非公平锁
2.3.1 获取锁

在这里插入图片描述
sync 实际上是一个抽象的静态内部类,它继承了 AQS 来实现重入锁的逻辑,全称 AbstractQueuedSynchronizer。它能够实现线程的阻塞以及唤醒,但它并不具备业务功能,所以在不同的同步场景中,会继承AQS 来实现对应场景的功能Sync 有两个具体的实现类,分别是:
NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁
FailSync: 表示所有线程严格按照FIFO 来获取锁
类的关系图
在这里插入图片描述
在ReentrantLock中我们也知道默认创建的就是非公平的sync

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

我们看NofairSync的lock方法

    final void lock() {
    		// cas 修改state的值
            if (compareAndSetState(0, 1))
                // 获取锁成功,设置线程id
                setExclusiveOwnerThread(Thread.currentThread());
            else
               // 获取锁失败
                acquire(1);
        }

获取到锁
在AbstractQueuedSynchronizer和 AbstractOwnableSynchronizer中有2个非常重要的变量
state = 0 ; 0表示获取到锁
exclusiveOwnerThread = 保存获取到锁的线程Id

	 // 锁的状态 0 代表无锁 >0 代表有锁
	 private volatile int state;
 	// 保存抢到锁的线程id
    private transient Thread exclusiveOwnerThread;

cas的实现原理

protected final boolean compareAndSetState(int expect, int update) {

return unsafe.compareAndSwapInt(this,stateOffset, expect, update);
}

通过 cas 乐观锁的方式来做比较并替换,这段代码的意思是,如果当前内存中的state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返回false.这个操作是原子的,不会出现线程安全问题。

stateOffset
一个Java 对象可以看成是一段内存,每个字段都得按照一定的顺序放在这段内存里,通过这个方法可以准确地告诉你某个字段相对于对象的起始内存地址的字节偏移。用于在后面的 compareAndSwapInt 中,去根据偏移量找到对象在内存中的具体位置所以 stateOffset 表示 state 这个字段在 AQS 类的内存中相对于该类首地址的偏移量

没有获取到锁

  1. 通过tryAcquire尝试获取独占锁,如果成功返回true,失败返回 false
  2. 如果tryAcquire 失败,则会通过addWaiter 方法将当前线程封装成Node 添加到 AQS 队列尾部
  3. acquireQueued 将 Node 作为参数,通过自旋去尝试获取锁。失败将挂起
  4. 如果acquireQueued返回为线程中断 ,selfInterrupt 将继续中断线程
public final void acquire(int arg) {
			// tryAcquire判断是否是重入,不是将尝试再去获取锁
			// addWaiter 将没有获取的线程放入到阻塞队列
			// acquireQueued 将线程挂起
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

AQS 中 tryAcquire 方法的定义,并没有实现,而是抛出异常,我们找实现

   protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

NonfairSync.tryAcquire

 protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

ReentrantLock.nofairTryAcquire

  1. 获取当前线程,判断当前的锁的状态
  2. 如果 state=0 表示当前是无锁状态,通过 cas 更新 state 状态的值
  3. 当前线程是属于重入,则增加重入次数
 final boolean nonfairTryAcquire(int acquires) {
          
            final Thread current = Thread.currentThread();  // 获取线程id 
            int c = getState();  // 获取state
            if (c == 0) { // 表示无锁
                if (compareAndSetState(0, acquires)) { //获取锁,cas 修改state
                    setExclusiveOwnerThread(current); // 设置当前线程
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) { //同一线程来获取锁。增加重入次数
                int nextc = c + acquires; // State的值+1
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc); // 设置state值,不要cas修改,因为当前线程已经获得了锁
                return true;
            }
            return false;
        }

AQS.addWaiter

当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成Node.入参 mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状态。意味着重入锁用到了 AQS 的独占锁功能

  1. 将当前线程封装成Node
  2. 当前链表中的 tail 节点是否为空,如果不为空,则通过 cas 操作把当前线程的node 添加到 AQS 队列
  3. 如果为空或者 cas 失败,调用enq 将节点添加到 AQS 队列
 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; //tail 是aqs中表示队列中队尾的一个属性默认为null
        if (pred != null) { // 不为空,说明队列中有值
            node.prev = pred; //把当前线程的 Node 的 prev 指向 tail
            if (compareAndSetTail(pred, node)) { // 用cas将尾节点提成node
                pred.next = node; //替换成后将原tail 节点执行node
                return node;
            }
        }
        enq(node);//tail=null,或者cas 失败
        return node;
    }

enq
enq 就是通过自旋操作把当前节点加入到队列中

private Node enq(final Node node) {
        for (;;) {
            Node t = tail; 
            if (t == null) { // Must initialize 如果尾节点为空
                if (compareAndSetHead(new Node())) //将head指向一个空的节点
                    tail = head;// 将tail指向一个空的节点
            } else {
                node.prev = t;  // 当前节点的前一个节点指向尾节点
                if (compareAndSetTail(t, node)) { //将node替换成尾节点
                    t.next = node; //原尾节点下个一个节点指向node
                    return t;
                }
            }
        }
    }

图解分析
假设 3 个线程来争抢锁,那么截止到 enq 方法运行结束之后,或者调用 addwaiter方法结束后,AQS 中的链表结构图
在这里插入图片描述

AQS.acquireQueued

通过 addWaiter 方法把线程添加到链表后,会接着把 Node 作为参数传递给acquireQueued 方法,去竞争锁

  1. 获取当前节点的prev 节点
  2. 如果prev 节点为 head 节点,那么它就有资格去争抢锁,调用 tryAcquire 抢占锁
  3. 抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head节点
  4. 如果获得锁失败,则根据waitStatus 决定是否需要挂起线程
  5. 最后,通过cancelAcquire 取消获得锁的操作
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)) { // 前一个节点是头节点就有资格去争抢锁
                    setHead(node); //获取锁成功,也就是ThreadA 已经释放了锁,然后设置head 为 ThreadB 获得执行权限
                    p.next = null; // help GC //把原 head 节点从链表中移除
                    failed = false;
                    return interrupted;
                }
                //ThreadA 可能还没释放锁,使得 ThreadB 在执行 tryAcquire 时会返回 false
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true; //并且返回当前线程在等待过程中有没有中断过。
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire(p, node)

如果 ThreadA 的锁还没有释放的情况下,ThreadB 和 ThreadC 来争抢锁肯定是会失败,那么失败以后会调用shouldParkAfterFailedAcquire 方法Node 有 5 中状态,分别是:CANCELLED(1),SIGNAL(-1),CONDITION(- 2)、PROPAGATE(-3)、默认状态(0)

CANCELLED: 在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node 的结点, 其结点的waitStatus 为CANCELLED,即结束状态,进入该状态后的结点将不会再变化
SIGNAL: 只要前置节点释放锁,就会通知标识为SIGNAL 状态的后续节点的线程
CONDITION: 和Condition 有关系,后续会讲解
PROPAGATE:共享模式下,PROPAGATE 状态的线程处于可运行状态
0:初始状态
这个方法的主要作用是,通过 Node 的状态来判断,ThreadA 竞争锁失败以后是否应该被挂起。

  1. 如果 ThreadA 的pred 节点状态为SIGNAL,那就表示可以放心挂起当前线程
  2. 通过循环扫描链表把CANCELLED 状态的节点移除
  3. 修改pred 节点的状态为SIGNAL,返回false.
 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; // 前置的节点的waitStatus
        if (ws == Node.SIGNAL) // 如果前置节点为 SIGNAL,意味着只需要等待其他前置节点的线程被释放
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true; //返回 true,意味着可以直接放心的挂起了
        if (ws > 0) {//ws 大于 0,意味着 prev 节点取消了排队,直接移除这个节点就行
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);//这里采用循环,从双向列表中移除CANCELLED 的节点
            pred.next = node;
        } else { //利用 cas 设置 prev 节点的状态为 SIGNAL(-1) 
              /*
             * 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);
        }
        return false;
    }

parkAndCheckInterrupt()

使用LockSupport.park 挂起当前线程编程 WATING 状态Thread.interrupted,返回当前线程是否被其他线程触发过中断请求,也就是thread.interrupt(); 如果有触发过中断请求,那么这个方法会返回当前的中断标识true,并且对中断标识进行复位标识已经响应过了中断请求。如果返回 true,意味着在acquire 方法中会执行 selfInterrupt()。

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

selfInterrupt: 标识如果当前线程在 acquireQueued 中被中断过,则需要产生一个中断请求,原因是线程在调用 acquireQueued 方法的时候是不会响应中断请求的

static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

图解分析

通过 acquireQueued 方法来竞争锁,如果 ThreadA 还在执行中没有释放锁的话, 意味着ThreadB 和 ThreadC 只能挂起了。
在这里插入图片描述

2.3.2 释放锁

如果这个时候 ThreadA 释放锁了,那么我们来看锁被释放后会产生什么效果
在 unlock 中,会调用release 方法来释放锁

    public final boolean release(int arg) {
        if (tryRelease(arg)) { //释放锁成功
            Node h = head; //得到 aqs 中 head 节点
            if (h != null && h.waitStatus != 0) //如果head节点不为空并且状态!=0.调用unparkSuccessor(h)唤醒后续节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    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;//获得 head 节点的状态
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);// 设置 head 节点状态为 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; //得到 head 节点的下一个节点
        if (s == null || s.waitStatus > 0) {
       		 //如果下一个节点为 null 或者 status>0 表示 cancelled 状态.
			 //通过从尾部节点开始扫描,找到距离 head 最近的一个waitStatus<=0 的节点
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null) //next 节点不为空,直接唤醒这个线程即可
            LockSupport.unpark(s.thread);
    }

为什么在释放锁的时候是从 tail 进行扫描?
我们再回到 enq
那个方法、。在标注为红色部分的代码来看一个新的节点是如何加入到链表中的

  1. 将新的节点的prev 指向tail
  2. 通过 cas 将tail 设置为新的节点,因为 cas 是原子操作所以能够保证线程安全性
  3. t.next=node;设置原 tail 的 next 节点指向新的节点
private Node enq(final Node node) {
        for (;;) {
            Node t = tail; 
            if (t == null) { // Must initialize 如果尾节点为空
                if (compareAndSetHead(new Node())) //将head指向一个空的节点
                    tail = head;// 将tail指向一个空的节点
            } else {
                node.prev = t;  // 当前节点的前一个节点指向尾节点
                if (compareAndSetTail(t, node)) { //将node替换成尾节点
                    t.next = node; //原尾节点下个一个节点指向node
                    return t;
                }
            }
        }
    }

在这里插入图片描述
在 cas 操作之后,t.next=node 操作之前。存在其他线程调用 unlock 方法从 head 开始往后遍历,由于 t.next=node 还没执行意味着链表的关系还没有建立完整。就会导致遍历到 t 节点的时候被中断。所以从后往前遍历,一定不会存在这个问题。

图解分析
通过锁的释放,原本的结构就发生了一些变化。head 节点的waitStatus 变成了 0, ThreadB 被唤醒

在这里插入图片描述
通过ReentrantLock.unlock,原本挂起的线程被唤醒以后继续执行,应该从哪里执行大家还有印象吧。 原来被挂起的线程是在 acquireQueued 方法中,所以被唤醒以后继续从这个方法开始执行

   private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted(); //唤醒的线程从这里开始执行
    }

AQS.acquireQueued
这个方法前面已经完整分析过了,我们只关注一下 ThreadB 被唤醒以后的执行流程。由于 ThreadB 的prev 节点指向的是 head,并且 ThreadA 已经释放了锁。所以这个时候调用tryAcquire 方法时,可以顺利获取到锁

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)) { // 前一个节点是头节点就有资格去争抢锁
                   setHead(node); //获取锁成功,也就是ThreadA 已经释放了锁,然后设置head 为 ThreadB 获得执行权限
                   p.next = null; // help GC //把原 head 节点从链表中移除
                   failed = false;
                   return interrupted;
               }
               //ThreadA 可能还没释放锁,使得 ThreadB 在执行 tryAcquire 时会返回 false
               if (shouldParkAfterFailedAcquire(p, node) &&
                   parkAndCheckInterrupt())
                   interrupted = true; //并且返回当前线程在等待过程中有没有中断过。
           }
       } finally {
           if (failed)
               cancelAcquire(node);
       }
   }
  1. 把 ThreadB 节点当成 head
  2. 把原 head 节点的 next 节点指向为 null

图解分析

  1. 设置新 head 节点的prev=null
  2. 设置原 head 节点的 next 节点为 null
    在这里插入图片描述
2.4 公平锁
2.4.1 获取锁
 		final void lock() {
            acquire(1);
        }

和非公平锁的区别在这:
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;
        }

这个方法与 nonfairTryAcquire(int acquires)比较,不同的地方在于判断条件多了hasQueuedPredecessors()方法,也就是加入了[同步队列中当前节点是否有前驱节点]的判断,如果该方法返回 true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值