AQS(AbstractQueuedSynchronizer)框架之ReentrantLock

822 阅读8分钟

park 与 unPark 使用

可以先了解下子路老师写的一些代码案例 blog.csdn.net/java_lyvee/…

ReentrantLock的实现使用的就是 park + 自旋的方式 ,下面举个例子来了解下 park 和 unpark 方法

public static void main(String[] args) throws InterruptedException {
    log.debug("1");
    Thread t1 = new Thread(()->{
        log.debug("2");
        LockSupport.park();//让当前线程阻塞
        log.debug("4");
    });
    //告诉cpu t1 当前可调度;具体什么时候调度是由操作系统决定的
    t1.start();
    //让主线程睡眠2秒
    TimeUnit.SECONDS.sleep(2);

    log.debug("3");
    LockSupport.unpark(t1);//唤醒t1线程
}

打印结果

image.png

手写原始方式实现锁

锁,其实就是一个标识,当这个标识改变成了某个状态我们理解为获得锁

public class CustomerLock {
    // status = 1;  不是原子性的,这是一条java代码 编译后会分为三条指令 gets=0  set1cache=1 set2
    // 执行set1时 只是在当前线程操作 将status改为1 此时只是在内存中操作,还未同步到主存当中。
    // 就是说线程t1修改status值还未同步写入主存时 t2也执行了相同操作,此时t2拿到的status还是0
    volatile int status = 0;

    //实例化这个类 ①为了调用cas方法 ②获取status变量的偏移量
    private static Unsafe unsafe = null;

    //CustomerLock当中status变量的内存偏移量
    private static long statusOffset;

    //获取 Unsafe对象
    static {
        Field singleOneInstanceField = null;
        try{
            singleOneInstanceField = Unsafe.class.getDeclaredField("theUnsafe");
            singleOneInstanceField.setAccessible(true);
            unsafe = (Unsafe) singleOneInstanceField.get(null);

            statusOffset = unsafe.objectFieldOffset(
                    com.zld.cloud.CustomerLock.class.getDeclaredField("status"));
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    void lock() throws InterruptedException{
        while (!compareAndSet(0,1)){
            TimeUnit.SECONDS.sleep(5);
        }
    }

    void unLock(){
        status = 0;
    }

    private boolean compareAndSet(int oldVal, int newVal) {
        return unsafe.compareAndSwapInt(this,statusOffset,0,1);
    }
}
public static void main(String[] args) throws InterruptedException {
    CustomerLock customerLock = new CustomerLock();
    Thread t1 = new Thread(()->{
        try {
            customerLock.lock();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("1");
        log.debug("1");
        log.debug("1");
        log.debug("1");
        log.debug("1");
        customerLock.unLock();
    },"t1");

    Thread t2 = new Thread(()->{
        try {
            customerLock.lock();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.debug("2");
        log.debug("2");
        log.debug("2");
        log.debug("2");
        log.debug("2");
        customerLock.unLock();
    },"t2");

    t1.start();
    t2.start();
}

测试结果截图

image.png

ReentrantLock 源码分析

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;
    ...
   }

ReentrantLock 中有个 抽象类Sync,这个类有两个实现 FairSync 公平锁 和 NonfairSync 非公平锁

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

    /**
     * Performs {@link Lock#lock}. The main reason for subclassing
     * is to allow fast path for nonfair version.
     */
    abstract void lock();

    /**
     * Performs non-fair tryLock.  tryAcquire is implemented in
     * subclasses, but both need nonfair try for trylock method.
     */
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                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;
    }

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

    protected final boolean isHeldExclusively() {
        // While we must in general read state before owner,
        // we don't need to do so to check if current thread is owner
        return getExclusiveOwnerThread() == Thread.currentThread();
    }

    final ConditionObject newCondition() {
        return new ConditionObject();
    }

    // Methods relayed from outer class

    final Thread getOwner() {
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }

    final int getHoldCount() {
        return isHeldExclusively() ? getState() : 0;
    }

    final boolean isLocked() {
        return getState() != 0;
    }

    /**
     * Reconstitutes the instance from a stream (that is, deserializes it).
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }
}

ReentrantLock 是可重入锁,当state > 0 说明锁被持有

image.png

公平锁的实现

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1); //标识加锁成功后需要改变的状态
    }
}

aquire方法

public final void acquire(int arg) {
    //尝试加锁 如果加锁失败则 调用 acquireQueued 方法加入队列排队
    //加入队列后会立刻park,直到解锁调用unpark醒来判断自己是否被打断
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire 方法

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();//拿到当前线程
    int c = getState();//获取lock对象的锁状态  0为自由状态 1为上锁 大于1表示重入
    if (c == 0) { // 如果锁是自由状态
        //判断当前是否需要排队,如果不需要则进行cas操作尝试加锁
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);//如果加锁成功,将当前线程设置为拥有锁的线程
            return true;
        }
    }
    //锁不是自由状态 但当前线程是持锁线程 表示重入则 状态值 +1
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

判断是否需要排队

public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

这里会有不需要排队的两种情况

  • 队列没有初始化不需要排队,直接去加锁但可能会失败
    • 假设 t1 t2同时lock,t1 t2均查询到此时队列没有初始化认为不需要排队,同时进行cas修改操作,原子性操作必定有一个会失败,失败的将去排队
  • 队列已初始化,队列中的node大于1,假设tc加锁时发现队列中第一个就是自身,例如重入
    • 队列中第一个排队的线程进入队列后会尝试获取锁,如果此时持锁线程释放了锁则直接获取锁执行,如果没有释放那就排队 这里的判断由两部分组成
  • 队首不等于队尾三种情况
    • 队列没有初始化 直接上锁
    • 队列已经初始化了 假设h!=t成立(队首不等于队尾的情况一定是队列node个数大于1的情况,队列中只有一个node时不算排队 head节点不参与排队,它要么时持锁node,要么时虚拟头节点),队列中线程大于1个 s==null不成立 那么判断 s.thread != Thread.currentThread() 分两种情况,此时head持锁线程正在做事情,不知道有没有做完
      • 当前参与竞争锁的线程不是第一个排队线程,返回true需要排队
      • 当前参与竞争锁的线程就是第一个排队的线程,返回false不需排队,尝试加锁
        • 尝试加锁时,head节点 也就是持锁线程释放锁 则 加锁成功 返回最顶层aquire成功
        • 尝试加锁失败,head节点还没有释放锁
  • 队列已初始化但是里面只有一个数据
    • 队列初始化时会虚拟一个头节点h,AQS默认 h 是不参与排队的,当队列中的第一个排队node得到锁会将自己设置为h 头节点,将自身线程设置为持锁线程。因此队列已初始化但只有一个节点并且这个节点就是持锁线程 那么h!=t 就不成立直接返回false不需要排队。

acquireQueued(addWaiter(Node.exclusive),arg))方法解析,两种情况

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
- first节点持有了锁(将自己改成head)未释放,curr节点(此时是first)尝试加锁失败仍需要排队
- 其他线程抢占了锁,队列中有节点排队,curr(不是first,它前面还有其他node)跟着排队

入队

private Node addWaiter(Node mode) {
    //将当前线程封装成node对象
    Node node = new Node(Thread.currentThread(), mode);
    //将尾节点赋值给pred
    Node pred = tail;
    if (pred != null) { //如果队尾不为null 说明队列已初始化
        node.prev = pred;//设置当前节点的上个节点是原队列的尾节点
        if (compareAndSetTail(pred, node)) {//cas操作防止多线程加锁,确保当前节点入队时的原子操作
            pred.next = node;//将当前线程node设置为原队列尾节点的下一个节点
            return node;//返回当前线程节点对象
        }
    }
    enq(node);//队列还没初始化
    return node;//返回当前线程节点对象
}

初始化队列

private Node enq(final Node node) {
    //死循环
    for (;;) {
        Node t = tail;//每次循环队尾节点赋值给t
        if (t == null) { //第一次循环时队尾肯定为null
            //调用无参构造方法实例化node对象  属性都为null
            if (compareAndSetHead(new Node()))
                //此时队列中只有一个新的虚拟node 使队列(链表)成立设置头尾相等
                tail = head;
        } else {//第二次循环t一定不为null
            node.prev = t; // 将当前线程节点对象 放到尾节点(第二次循环尾节点就是头节点)之后
            if (compareAndSetTail(t, node)) {//将当前线程对象node入队 并设置为队尾 
                t.next = node;//维护好链表设置头节 原来队尾下一个节点尾当前线程节点
                return t;//返回队尾节点,即终止循环
            }
        }
    }
}

acquireQueued方法的源码分析

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {//死循环,走进这个方法node已经完成了入队
            //获得当前线程node的上个node有两种情况,1、p为head节点。2、p不为head
            final Node p = node.predecessor();
            //如果p是head,那么当前线程对象node 为first, 尝试加锁,其实就是想看下head节点释放锁了没
            if (p == head && tryAcquire(arg)) {
                setHead(node);//原head释放了锁,加锁成功将自身设置为head
                p.next = null; //原head的下一节点置空 因为当前node成为head
                failed = false;//标识 当前node加锁成功时为false
                return interrupted;//返回默认值
            }
            //这里分两种情况 1、上个节点不是head 2、上个节点是head但未释放锁 修改上个节点状态park
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())//自身park
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

Synchronized 1.6 之前对任何情况的处理相差不多,效率比较低下 ---重量级锁 并发编程之父Doug Lea 写了ReentrantLock

ReentrantLock的四种情况

ReentrantLock的性能比 synchronized 高

  • 从头到尾第一个线程t0获取锁的时候代价基本为0
    ReentrantLock加锁最大的开销在于 compareAndSetState(0,acquires) cas操作指令,cpu底层提供的硬件指令,跟语言无关
    • synchronize 只有一个线程时是偏向锁
  • 多线程之间顺序执行(t0执行完,t1执行)没有竞争执行,代价几乎为0
    • synchronize 此时升级为轻量级锁
  • 如果有线程竞争执行
    • 直到第二个线程自旋完 当前线程还没有释放锁
    • 第二个线程来加锁时没释放锁,但是入队自旋之后第一个线程释放了锁
  • t0持有锁不释放,t1尝试加锁失败,t2来加锁

公平锁与非公平锁区别

image.png

image.png

当锁是自由状态是 公平锁需要判断自己是否要排队,而非公平锁直接cas操作,当锁不是自由状态非公平锁和公平锁没有区别,非公平锁的效率比公平锁高
sychronized 只支持公平锁
有的人认为公平锁就是排队非公平锁是插队,这种说法是不完全正确的。 实际上非公平锁入队之后是无法插队的 只能在入队之前的两次抢锁过程种可以算插队。