2021-05-13

1 篇文章 0 订阅

Java多线程(7)

Lock锁和AQS

1.Lock锁

定义:Lock implementations provide more extensive locking operations than can be obtained using synchronized methods and statements. They allow more flexible structuring, may have quite different properties, and may support multiple associated Condition objects.这是官方定义,就是说lock比synchronized提供了加多的操作和更灵活的使用方式。

常见实现类:ReentrantLockReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock

使用说明:官方推荐

 Lock l =new ReentrantLock();//也可以是其他实现Lock的类
 l.lock();
 try {
   // access the resource protected by this lock
 } finally {
   l.unlock();
 }

 2.首先来说一下第一个实现类可重入锁(ReentrantLock)

可重入锁顾名思义就是对于同一个对象可以多次获取锁。如下所示:

对于上述锁我们可以重复的去获取锁的使用权,但是获取多少次同样也要释放多少次。

3.ReentrantLock的底层实现

首先我们看看源码,声明一个锁:如下所示:

ReentrantLock reentrantLock=new ReentrantLock();

然后进去ReentrantLocak,如下所示:他的加锁的方法是如下所示:

我们从上面可以看出他调用了一个sync的一个变量去调用了lock方法:下面看看sync

从上面我们可以看出在ReentrantLock类里面声明了一个内部内Sync,然后继承AbstractQueuedSynchronizer这个类,这就说明ReentrantLock的所有方法都是基于AbstractQueuedSynchronizer这个类实现的,因为在每个ReentrantLock的方法内都是sync调用自己的方法,而sync此类又继承AbstractQueuedSynchronizer这个类,所以ReentrantLock的底层实现就是AbstractQueuedSynchronizer的底层实现,那么我们就看看AbstractQueuedSynchronizer的底层实现。

4 AQS(AbstractQueuedSynchronizer)的底层实现

AQS全称为抽象同步队列器,他的底层实际就维护了两个队列一个是等待对列(CLH),一个是条件队列。AQS定义两种资源共享方式:Exclusive(独占,在特定时间内,只有一个线程能够执行,如ReentrantLock)和share(共享,多个线程可以同时执行,如ReadLock、Semaphore、CountDownLatch),可见不同的实现方式征用共享资源的方式不同。

对于同步队列(CLH):,每当有新的线程请求资源时,该线程会进入一个等待队列只有当持有锁的线程释放资源后,该线程才能持有资源。该等待队列的实现方式是双向链表,线程会被包裹在链表节点Node中。Node即队列的节点对象,它封装了各种等待状态(典型的状态机模式),前驱和后继节点信息,以及它对应的线程。

同步队列中的节点的状态总共有四种:

当调用了lock方法时如下图所示:默认使用非公平锁,非公平锁可以插队去获取锁

这里说明一下“1”的含义,它是设置“锁的状态”的参数。对于“独占锁”而言,锁处于可获取状态时,它的状态值是0;锁被线程初次获取到了,它的状态值就变成了1。
由于ReentrantLock(公平锁/非公平锁)是可重入锁。

lock方法先通过CAS尝试将同步状态(AQS的state属性)从0修改为1。若直接修改成功了,则将占用锁的线程设置为当前线程。这说明当前只有一个线程也即是他自己来获取锁,所以直接把锁的状态改为独占状态并且保存当前线程的状态。

如果当前锁被其他线程使用,则尝试去竞争获取锁(因为是非公平锁,所以大家都可插队获取锁的使用权),调用acquire方法:如下图所示

如上图所示,如果尝试成功就的到锁,如果失败就放入等待队列末尾,然后为同步队列中的当前节点循环等待获取锁,直到成功。

然后我们再去看看tryAcquire方法:

从上图来看调用的是nonfairTryAcquire方法,我们点进去看看:

这里的acquires的值为1,

从上图中我们可以看出,首先获取AQS的同步状态(state),就是锁的状态,如果状态为0,则尝试设置状态为arg(这里为1), 若设置成功则表示当前线程获取锁,返回true。这个操作外部方法lock()就做过一次,这里再做只是为了再尝试一次,尽量以最简单的方式获取锁。

如果状态不为0,再判断当前线程是否是锁的owner(即当前线程在之前已经获取锁,这里又来获取),如果是owner, 则尝试将状态值增加acquires,如果这个状态值越界,抛出异常;如果没有越界,则设置后返回true。这里可以看非公平锁的涵义,即获取锁并不会严格根据争用锁的先后顺序决定。这里的实现逻辑类似synchroized关键字的偏向锁的做法,即可重入而不用进一步进行锁的竞争,也解释了ReentrantLock中Reentrant的意义。

如果状态不为0,且当前线程不是owner,则返回false。就说明获取锁失败,去自旋吧

如果返回false,那么就要把此线程加入等待队列,如下图所示:

首先创建一个Node对象,Node中包含了当前线程和Node模式(这时是排他模式)。tail是AQS的中表示同步队列队尾的属性,刚开始为null,所以进行enq(node)方法,从字面可以看出这是一个入队操作,接来下看看enq方法:

我们看到入队的操作是一个死循环,本身没有锁,可以多个线程并发访问,假如某个线程进入方法,此时head, tail都为null, 进入if(t==null)区域,从方法名可以看出这里是用CAS的方式创建一个空的Node作为头结点,因为此时队列中只一个头结点,所以tail也指向它,第一次循环执行结束。注意这里使用CAS是防止多个线程并发执行到这儿时,只有一个线程能够执行成功,防止创建多个同步队列。

进行第二次循环时(或者是其他线程enq时),tail不为null,进入else区域。将当前线程的Node结点(简称CNode)的prev指向tail,然后使用CAS将tail指向CNode。看下这里的实现:

expect为t, t此时指向tail,所以可以CAS成功,将tail重新指向CNode。此时t为更新前的tail的值,即指向空的头结点,t.next=node,就将头结点的后续结点指向CNode,返回头结点。

其他线程再插入节点以此类推,都是在追加到链表尾部,并且通过CAS(CAS可以保证原子性、有序性、可见性)操作保证线程安全。

下面是释放锁的过程,如下所示:

通过ReentrantLock的unlock方法来看下AQS的锁释放过程。

从上图可以看出首先调用了release方法,release方法如下所示:

unlock调用AQS的release()来完成, AQS的如果tryRelease方法由具体子类实现。tryRelease返回true,则会将head传入到unparkSuccessor(Node)方法中并返回true,否则返回false。

接下来看看tryRelease方法:

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

接下来看看unparkSuccessor(Node)

 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;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 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;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.

从上面我们可以看出此方法是真正的释放了锁。

内部首先会发生的动作是获取head节点的next节点,如果获取到的节点不为空,则直接通过:“LockSupport.unpark()”方法来释放对应的被挂起的线程,这样一来将会有一个节点唤醒后继续进入循环进一步尝试tryAcquire()方法来获取锁。

以上就是ReentrantLock加锁和解锁的过程,里面全是依靠AQS的方法,所以可重入锁的的底层就是AQS。

(以上参考:https://segmentfault.com/a/1190000008471362

https://www.cnblogs.com/guanghe/p/13462079.html

https://www.cnblogs.com/wyq1995/p/12253792.html

等待队列(CLH)

如下所示:

如上图所示,我们可以看出等待队列实际就是个双向链表,每次新的阻塞线程都会加入链表并加入到尾部,唤醒阻塞队列都是从头部唤醒。

其中signal代表等待被唤醒的队列,cancel代表此节点已经没有用了被垃圾回收器回收了。

条件队列

是一个单链表如下图所示:

条件队列的使用:

 // 创建锁对象
        ReentrantLock lock = new ReentrantLock();
        // 创建条件变量
        Condition condition = lock.newCondition();
        // 以下创建两个线程,里面都会获取锁和释放锁
        Thread thread1 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("await begin");
      // 注意,这里调用条件变量的await方法,当前线程就会丢到condition条件变量中的条件队列中阻塞
                condition.await();
                System.out.println("await end");
            } catch (InterruptedException e) {
                //
            } finally {
                lock.unlock();
            }

        });

        Thread thread2 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("signal begin");
                // 唤醒被condition变量内部队列中的某个线程
                condition.signal();
                System.out.println("signal end");
            } finally {
                lock.unlock();
            }
        });
        thread1.start();
        Thread.sleep(500);
        thread2.start();

接下来我们看看Condition这个类:

这个类是AQS的内部类,通过这个类可以访问AQS内部的属性和方法;

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //新建一个Node.CONDITION节点放到条件队列最后面
    Node node = addConditionWaiter();
    //释放当前线程获取的锁
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //调用park()方法阻塞挂起当前线程,因为是加入到同步队列中去,所以要阻塞当前线程
    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);
}
private Node addConditionWaiter() {
   //条件队列是个单链表,不是双向链表
    Node t = lastWaiter;
    //第一次进来,这个lastWaiter是null,即t = null,不会进入到这个if语句
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //创建一个Node.CONDITION类型的节点,然后下面这个if中就是将第一个节点firstWaiter和最后一个节点都指向这个新创建的节点
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}
public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //条件队列移除第一个节点,然后把这个节点丢到阻塞队列中,然后激活这个线程
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

AQS中的等待队列(CLH)和条件队列的关系

1.当多个线程调用lock.lock()方法的时候,只有一个线程获取到可锁,其他的线程都会被转为Node节点丢到AQS的等待队列中,并做CAS自旋获取锁;

2.当获取到锁的线程对应的条件变量的await()(对应于Object中的wait()方法)方法被调用的时候,该线程就会释放锁,并把当前线程转为Node节点放到条件变量对应的条件队列中;

3.这个时候AQS的等待队列中又会有一个节点中的线程能得到锁了,如果这个线程又恰巧调用了对应条件变量的await()方法时,又会重复2的步骤,然后等待队列中又会有一个节点中的线程获得锁。

4.然后,又有一个线程调用了条件变量的signal()(相当于Object中1的notify()方法)或者signalAll()方法,就会把条件队列中一个或者所有的节点都移动到AQS等待队列中,然后调用unpark方法进行授权,就等着获得锁了;

5.一个锁对应一个等待队列,但是对应多个条件变量,每一个条件变量对应一个条件队列;其中,这两种队列中存放的都是Node节点,Node节点中封装了线程及其状态,下图所示:

参考:https://www.cnblogs.com/wyq1995/p/12253792.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值