揭开ReentrantLock和AQS的神秘面纱

1 篇文章 0 订阅
1 篇文章 0 订阅

**在这里插入图片描述

一.JUC自我介绍:

java.util.concurrent是在并发编程中比较常用的工具类,里面包含很多用来在并发场景中使用的组件。比如线程池、阻塞队列、计时器、同步器、并发集合等等。并发包的作者是大名鼎鼎的DougLea.

二.核心Lock

Lock本质上是一个接口,它定义了释放锁和获得锁的抽象方法,定义成接口就意味着它定义了锁的一个标准规范,也同时意味着锁的不同实现。实现Lock接口的,比如大名鼎鼎的ReentrantLock、ReentrantReadWriteLock、StampedLock

ReentrantLock:

表示重入锁,它是唯一一个实现了Lock接口的类。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数

ReentrantReadWriteLock:

重入读写锁,它实现了ReadWriteLock接口,在这个类中维护了两个锁,一个是ReadLock,一个是WriteLock,他们都分别实现了Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则是:读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥。

StampedLock:

stampedLock是JDK8引入的新的锁机制,可以简单认为是读写锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全并发,但是读和写是有冲突的,如果大量的读线程存在,可能会引起写线程的饥饿。stampedLock是一种乐观的读策略,使得乐观锁完全不会阻塞写线程

@ReservedStackAccess
    private long tryAcquireWrite() {
        long s, nextState;
        if (((s = state) & ABITS) == 0L && casState(s, nextState = s | WBIT)) {
            U.storeStoreFence();
            return nextState;
        }
        return 0L;
    }

    @ReservedStackAccess
    private long tryAcquireRead() {
        for (long s, m, nextState;;) {
            if ((m = (s = state) & ABITS) < RFULL) {
                if (casState(s, nextState = s + RUNIT)) //cas自旋
                    return nextState;
            }
            else if (m == WBIT)
                return 0L;
            else if ((nextState = tryIncReaderOverflow(s)) != 0L)
                return nextState;
        }
    }

ReentrantLock重入锁

重入锁,表示支持重新进入的锁,也就是说,如果当前线程t1通过调用lock方法获取了锁之后,再次调用lock,是不会再阻塞去获取锁的,直接增加重试次数就行了。synchronized和ReentrantLock都是可重入锁,他的设计目的就是为了解决死锁,比如在一个方法中获得了当前锁资源,然后在这个方法中再去调用另外一个加锁的方法,这个时候存在同一个实例锁,当前资源锁无法获得,将会导致死锁,这个时候ReentrantLock将会根据state判断当前线程是否持有锁资源,进行重入

final boolean tryLock() {
            Thread current = Thread.currentThread();  //获取当前线程
            int c = getState(); //获取状态
            if (c == 0) {
                if (compareAndSetState(0, 1)) {  //cas自旋比较  期望值与内存旧值比较,设置新值1
                    setExclusiveOwnerThread(current);  //将当前线程设置为独占线程
                    return true;
                }
            } else if (getExclusiveOwnerThread() == current) { //代表当前资源有线程持有,获取该线程
                if (++c < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(c);  // 设置state  并且state为volatile修饰
                return true;
            }
            return false;
        }

在这里插入图片描述

什么是CAS机制

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

三、ReentrantLock的实现原理

锁的基本原理就是将多线程并行任务通过某种机制实现线程的串行执行,从而达到线程的安全性目的,在synchronized种,通过线程的阻塞以及唤醒来达到线程竞争和同步的目的,ReentrantLock中主要通过AQS队列来实现的

AQS自我介绍:

在Lock中,用到了一个同步队列AQS,全称AbstractQueuedSynchronizer,它是一个同步工具也是Lock用来实现线程同步的核心组件

AQS的两种功能

  1. 独占锁,每次只能有一个线程持有锁,比如前面给大家演示的ReentrantLock就是以独占方式实现的互斥锁
  2. 共享锁,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock

AQS的内部实现

AQS队列内部维护的是一个FIFO的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个Node其实是由线程封装,当线程争抢锁失败后会封装成Node加入到ASQ队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
在这里插入图片描述

 abstract static class Node {
        volatile Node prev;       // initially attached via casTail
        volatile Node next;       // visibly nonnull when signallable
        Thread waiter;            // visibly nonnull when enqueued
        volatile int status;      // written by owner, atomic bit ops by others

        // methods for atomic operations  
        final boolean casPrev(Node c, Node v) {  // for cleanQueue
            return U.weakCompareAndSetReference(this, PREV, c, v);
        }
        final boolean casNext(Node c, Node v) {  // for cleanQueue
            return U.weakCompareAndSetReference(this, NEXT, c, v);
        }
        final int getAndUnsetStatus(int v) {     // for signalling
            return U.getAndBitwiseAndInt(this, STATUS, ~v);
        }
        final void setPrevRelaxed(Node p) {      // for off-queue assignment
            U.putReference(this, PREV, p);
        }
        final void setStatusRelaxed(int s) {     // for off-queue assignment
            U.putInt(this, STATUS, s);
        }
        final void clearStatus() {               // for reducing unneeded signals
            U.putIntOpaque(this, STATUS, 0);
        }
        //这里相当于JDK1.9的valHandle类,可以将属性设置为原子性操作,
        //很NB的一个类,相当于一个属性变量的引用
        private static final long STATUS
            = U.objectFieldOffset(Node.class, "status");
        private static final long NEXT
            = U.objectFieldOffset(Node.class, "next");
        private static final long PREV
            = U.objectFieldOffset(Node.class, "prev");
    }

释放锁以及添加线程对于队列的变化
1.添加节点:
在这里插入图片描述

  final void enqueue(Node node) {
        if (node != null) {
            for (;;) {
                Node t = tail;
                node.setPrevRelaxed(t);        // avoid unnecessary fence
                if (t == null)                 // initialize
                    tryInitializeHead();   //设置虚拟的头结点 null
                else if (casTail(t, node)) {
                    t.next = node;
                    if (t.status < 0)          // wake up to clean link
                        LockSupport.unpark(node.waiter);
                    break;
                }
            }
        }
    }

1.新的线程封装成Node节点追加到同步队列中,设置prev节点以及修改当前节点的前置节点的next节点指向自己
2.通过CAS讲tail重新指向新的尾部节点
3.head节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下
在这里插入图片描述
1.修改head节点指向下一个获得锁的节点
2.新的获得锁的节点,将prev的指针指向null

设置head节点不需要用CAS,原因是设置head节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要CAS保证,只需要把head节点设置为原首节点的后继节点,并且断开原head节点的next引用即可

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值