多线程与高并发系列五(ReentrantLock)

欢迎大家观看我对于多线程与高并发这一系列的博客:
多线程与高并发系列一(多线程基本知识)
多线程与高并发系列二(Synchronized和volatile)
多线程与高并发系列三(ThreadLocal)
多线程与高并发系列四(线程池)
多线程与高并发系列五(ReentrantLock)
多线程与高并发系列六(并发工具)
多线程与高并发系列七(阻塞队列和Atomic 原子类)

Lock简介

在 Lock 接口出现之前,Java 中的应用程序对于多线程的并发安全处理只能基于synchronized 关键字来解决。但是 synchronized 在有些场景中会存在一些短板,也就是它并不适合于所有的并发场景。但是在 Java5 以后,Lock 的出现可以解决synchronized 在某些场景中的短板,它比 synchronized 更加灵活。

Lock实现

Lock 本质上是一个接口,它定义了释放锁和获得锁的抽象方法,定义成接口就意味着它定义了锁的一个标准规范,也同时意味着锁的不同实现。实现 Lock 接口的类有很多,以下为几个常见的锁实现:

  • ReentrantLock:表示重入锁,它是唯一一个实现了 Lock 接口的类。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数;
  • ReentrantReadWriteLock:重入读写锁,它实现了 ReadWriteLock 接口,在这个类中维护了两个锁,一个是 ReadLock,一个是 WriteLock,他们都分别实现了 Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则是: 读和读不互斥、读和写互斥、写和写互斥也就是说涉及到影响数据变化的操作都会存在互斥
  • StampedLock: stampedLock 是 JDK8 引入的新的锁机制,可以简单认为是读写锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全并发,但是读和写是有冲突的,如果大量的读线程存在,可能会引起写线程的饥饿。stampedLock 是一种乐观的读策略,使得乐观锁完全不会阻塞写线程。

接下来,我们来重点看一下ReentrantLock的底层原理

ReentrantLock

重入锁的设计目的

public class ReentrantDemo{
	public synchronized void demo(){
 		System.out.println("begin:demo");
 		demo2();
 	}
	public void demo2(){
 		System.out.println("begin:demo1");
 		synchronized (this){
 		}
 	}
 	public static void main(String[] args) {
 		ReentrantDemo rd=new ReentrantDemo();
 		new Thread(rd::demo).start();
 	} 
 }

比如调用 demo 方法获得了当前的对象锁,然后在这个方法中再去调用demo2,demo2 中的存在同一个实例锁,这个时候当前线程会因为无法获得demo2 的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死锁。

ReentrantLock 的实现原理

我们知道锁的基本原理是,基于将多线程并行任务通过某一种机制实现线程的串行执行,从而达到线程安全性的目的。在 synchronized 中,有偏向锁、轻量级锁、乐观锁。基于乐观锁以及自旋锁来优化了 synchronized 的加锁开销,同时在重量级锁阶段,通过线程的阻塞以及唤醒来达到线程竞争和同步的目的。那么在 ReentrantLock 中,也一定会存在这样的需要去解决的问题。就是在多线程竞争重入锁时,竞争失败的线程是如何实现阻塞以及被唤醒的呢?

AQS

AQS的全称为(AbstractQueuedSynchronizer),抽象的队列式的同步器。AQS是⼀个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

AQS 的内部实现

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要⼀套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,andHagersten)队列是⼀个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成⼀个CLH锁队列的⼀个结点(Node)来实现锁的分配。

在这里插入图片描述
AQS使用⼀个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原⼦操作实现对其值的修改。

private volatile int state;//共享变量,使⽤volatile修饰保证线程可⻅性

状态信息通过protected类型的getState,setState,compareAndSetState进行操作:

//返回同步状态的当前值
protected final int getState() { 
 return state; }
// 设置同步状态的值
protected final void setState(int newState) {
 state = newState; }
//原⼦地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
 return unsafe.compareAndSwapInt(this, stateOffset, expect,
update);
}
AQS定义两种资源共享方式
  • Exclusive(独占):只有⼀个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
    公平锁:按照线程在队列中的排队顺序,先到者先拿到锁;
    非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的;
  • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。

ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读锁允许多个线程同时对某⼀资源进行读。

ReentrantLock 加锁的源码分析

ReentrantLock有两个内部类

  1. new FairSync() ;
  2. new NonfairSync();
/**
     * 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();
    }

公平锁和非公平锁都是继承自Sync,而Sync继承自AbstractQueuedSynchronizer,也就是AQS。

我们看一下lock()方法:

lock

public void lock() {
        acquire(1);
    }

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

函数流程如下:

  1. tryAcquire()尝试直接去获取资源,如果成功则直接返回;
  2. addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

接下来,我们看一下tryAcquire()方法

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;
}

获取锁成功分为两种情况,第一个if判断AQS的state是否等于0,表示锁没有人占有。接着,hasQueuedPredecessors判断队列是否有排在前面的线程在等待锁,没有的话调用compareAndSetState使用cas的方式修改state,传入的acquires写死是1。最后线程获取锁成功,setExclusiveOwnerThread将线程记录为独占锁的线程。

第二个if判断当前线程是否为独占锁的线程,因为ReentrantLock是可重入的,线程可以不停地lock来增加state的值,对应地需要unlock来解锁,直到state为零。

如果最后获取锁失败,下一步需要将线程加入到等待队列。

nonfairTryAcquire:

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//获取当前执行的线程
            int c = getState();//获得 state 的值
            if (c == 0) {//表示无锁状态
                if (compareAndSetState(0, acquires)) {//cas 替换 state 的值,cas 成功表示获取锁成功
                    setExclusiveOwnerThread(current);//保存当前获得锁的线程,下次再来的时候不要再尝试竞争锁
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//如果同一个线程来获得锁,直接增加重入次数
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

addWaiter:

当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成Node.

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);//把当前线程封装为 Node
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;//tail 是 AQS 中表示同比队列队尾的属性,默认是 null
        if (pred != null) { //tail 不为空的情况下,说明队列中存在节点
            node.prev = pred;//把当前线程的 Node 的 prev 指向 tail
            if (compareAndSetTail(pred, node)) {//通过 cas 把 node加入到 AQS 队列,也就是设置为 tail
                pred.next = node;//设置成功以后,把原 tail 节点的 next指向当前 node
                return node;
            }
        }
        // 列队尾部为空 或者  CAS 操作失败
        enq(node);//tail=null,把 node 添加到同步队列
        return node;
    }

enq:

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

private Node enq(final Node node) {
 	for (;;) {
 		Node t = tail;
 		if (t == null) { // Must initialize
 			if (compareAndSetHead(new Node()))
 				tail = head;
 			} else {
 				node.prev = t;
 				if (compareAndSetTail(t, node)) {
 				t.next = node;
				 return t;
 				}
 			}
 } }

图解分析

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

acquireQueued

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

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();//获取当前节点的 prev 节点
                if (p == head && tryAcquire(arg)) { //如果是 head 节点,说明有资格去争抢锁
                    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

如果 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.
    返回 false 时,也就是不需要挂起,返回 true,则需要调用 parkAndCheckInterrupt挂起当前线程;
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//前置节点的waitStatus
        if (ws == Node.SIGNAL)
            //如果前置节点为 SIGNAL,意味着只需要等待其他前置节点的线程被释放
            return true;//返回 true,意味着可以直接放心的挂起了
        if (ws > 0) {  //ws 大于 0,意味着 prev 节点取消了排队,直接移除这个节点就行
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);//这里采用循环,从双向列表中移除 CANCELLED 的节点
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//利用 cas 设置 prev 节点的状态为 SIGNAL(-1)
        }
        return false;
    }

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); // 又是一个底层类 实现线程等待
        return Thread.interrupted(); // 返回并 取消等待状态
    }

selfInterrupt

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

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

图解分析

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

LockSupport

LockSupport类是 Java6引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了 Unsafe 类里的函数,归结到 Unsafe 里,只有两个函数unpark()和park()
unpark 函数为线程提供“许可(permit)”,线程调用 park 函数则等待“许可”。这个有点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。
permit 相当于 0/1 的开关,默认是 0,调用一次 unpark 就加 1 变成了 1.调用一次park 会消费 permit,又会变成 0。 如果再调用一次 park 会阻塞,因为 permit 已经是 0 了。直到 permit 变成 1.这时调用 unpark 会把 permit 设置为 1.每个线程都有一个相关的 permit,permit 最多只有一个,重复调用 unpark 不会累积。

ReentrantLock释放锁的源码分析

在 unlock 中,会调用 release 方法来释放锁

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; 
 }

tryRelease

这个方法可以认为是一个设置锁状态的操作,通过将 state 状态减掉传入的参数值(参数是 1),如果结果状态为 0,就将排它锁的 Owner 设置为 null,以使得其它的线程有机会进行执行。
在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时候减掉 1,同一个锁,在可以重入后,可能会被叠加为 2、3、4 这些值,只有 unlock()的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回 true。

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;
}

unparkSuccessor

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;//获得 head 节点的状态
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);// 设置 head 节点状态为 0
    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那个方法,来看一个新的节点是如何加入到链表中的

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { 
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else { 
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值