Lock锁机制详解(一)AQS的底层实现

本文深入探讨了Java Lock接口提供的锁机制,包括为何需要Lock而非仅依赖synchronized。Lock锁通过响应中断、支持超时和非阻塞获取锁来避免死锁,提供更细粒度的同步控制。文中还介绍了Lock的使用特性,如公平锁、等待可中断和锁绑定多个条件,并列举了如ReentrantLock、ReadWriteLock等应用场景。同时,详细解析了锁的实现原理,包括AbstractQueuedSynchronizer(AQS)的数据结构、节点状态以及获取和释放锁的流程。最后,展示了如何基于AQS自定义一个非重入锁。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Lock锁机制详解(一)


在详细探讨Lock锁机制之前,首先来思考一个问题:为什么有了Synchronized还要提供Lock接口

1、Lock锁机制存在的原因

1) 死锁方面

虽然Java对synchronized做了许多优化,大大提升了锁的性能,但是由于Synchronized锁是无法主动释放锁资源的,因此会涉及到死锁的问题

死锁发生的四个必要条件如下:

  • 互斥
  • 不可剥夺
  • 请求与保持
  • 循环等待

问题的关键在于Synchronized锁无法主动释放资源,但在大多数场景下,我们都希望不可剥夺这一条件能够被破坏,如果占用资源的线程申请不到其他资源,就可以主动释放自己占有的资源,因此Java提供了Lock包下的锁对象供开发者使用。

那么Lock包下的锁是如何解决这一问题的呢?其实主要是通过三方面来进行设计:响应中断、支持超时和非阻塞获锁

  • 响应中断(void lockInterruptibly() throws InterruptedException;方法)

线程使用synchronized锁,如果线程阻塞并且进入死锁状态,就没有机会来唤醒阻塞的线程。Lock锁提供了响应中断的机制,使得阻塞态的线程能够响应中断信号而被唤醒,从而有机会释放已占有的资源来破坏不可抢占的条件。

  • 支持超时(boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

如果线程在一段时间内没有获取到锁,线程不会进入阻塞态,而是返回一个错误。通过该机制,线程也有机会去释放持有的锁,破坏不可剥夺的条件。

  • 非阻塞(boolean tryLock();

线程使用该方法尝试获取锁,获取成功返回true,失败不会进行等待,而是直接返回false,同样使得线程可以释放锁。

总的来说,Lock锁机制通过破坏不可剥夺的条件,从而在一定程度上避免了死锁问题。

2)使用特性

Lock 接是 JUC 包的顶层接口,基于Lock 接口,用户能够以非块结构来实现互斥同步,摆脱了语言特性束缚,在类库层面实现同步。

在使用方面,Lock包增加的高级功能有:

  • 等待可中断: 持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待而处理其他事情。
  • 公平锁: 公平锁指多个线程在等待同一个锁时,必须按照申请锁的顺序来依次获得锁,而非公平锁不保证这一点,在锁被释放时,任何线程都有机会获得锁。synchronized 是非公平的,ReentrantLock 在默认情况下是非公平的,可以通过构造方法指定公平锁。一旦使用了公平锁,性能会急剧下降,影响吞吐量。
  • 锁绑定多个条件: 一个Lock 锁可以同时绑定多个 Conditionsynchronized 中锁对象的 waitnotify 可以实现一个隐含条件,如果要和多个条件关联就不得不额外添加锁,而 Lock 锁可以多次调用 newCondition 创建多个条件。

因此,在使用方面,Lock锁提供了一些更灵活的方法去控制同步动作。

3)应用场景

Lock锁机制提供了各式各样的锁供我们使用,每种锁可以适配不同的场景,例如:

  • ReentrantLock:与synchronized类似,但可以指定是公平锁还是非公平锁。
  • ReadWriteLock:读写锁,包含读共享锁和写互斥锁,在读多写少的场景下可以大大提升读写性能。
  • Semaphore:信号量,用于指定线程同步的个数
  • CountDownLatch:计数器(一次性),用于线程之间的等待执行(类比join()方法)
  • CyclicBarrier:回环屏障,可以满足计算器的重置操作。(与计数器有所区别,之后会讨论)

通过这些不同种类的锁实现,可以在不同场景下灵活使用。

2、锁实现原理

JUC的锁框架如下:

从类图中可以看到,锁框架中锁的实现都是围绕一个最底层的数据结构来实现的,也就是AbstractQueuedSynchronizer抽象同步队列,该抽象类的结构如下:

在这里插入图片描述

1)组成

AQS是一个FIFO的双向队列,其中包含以下几个关键字段

  • headtail记录队首和队尾元素,元素类型为Node

  • 静态内部类Node,该类用于包装线程的同步信息

    thread:存放目标线程

    SHARED/EXCLUSIVE:标记线程是获取独占还是共享资源时被挂起的

    waitStatus:记录当前线程的等待状态(CANCELLED(线程被取消了)、SIGNAL(线程需要被唤醒)、CONDITION(线程在条件队列里面等待)、PROPAGATE(释放共享资源时需要通知其他节点)

    next:记录当前节点的后继节点

    pre:记录当前节点的前驱结点

  • 同步状态信息state,这是实现锁机制的关键变量,该字段使用volatile修饰,可以通过getState/setState/compareAndSetState等方法修改其值

    state的值在不同的锁中有不同的含义,比如在ReentrantLock中表示可重入次数;在ReadWriteLock中前后16位表示读写锁的重入次数;在Semaphore中表示信号量的个数;在CountdownLatch中表示计数器的值。

  • 内部类ConditionObject,该类用于结合锁来实现线程同步,称为条件变量。每一个条件变量对应一个条件队列(单向链表),用于存放调用条件变量的await后被阻塞的线程

  • 工具类LockSupport,该类的主要作用是挂起和唤醒线程,AQS的底层方法实现会使用该工具类的park/unpark相关方法去进行线程阻塞唤醒操作,如下面的unparkSuccessor方法

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)
            node.compareAndSetWaitStatus(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 p = tail; p != node && p != null; p = p.prev)
                if (p.waitStatus <= 0)
                    s = p;
        }
        if (s != null)
            //唤醒后续线程
            LockSupport.unpark(s.thread);
    }

LockSupport类与每个使用它的线程都会关联一个许可证,默认情况下线程不持有许可证。

  • 如果线程中调用了park方法并且该线程有许可证,方法会立刻返回;反之,线程不持有许可证,当前线程会被阻塞挂起
  • unpark(Thread thread)方法会为目标线程分配一个许可证,如果线程之前被park阻塞,会唤醒该线程

注意:如果其他线程调用了阻塞线程的interrupt方法,设置了中断标志或线程被虚假唤醒,该阻塞线程也会被唤醒。

2)操作

对AQS来说,线程同步的关键是对状态值state进行操作,根据state是否属于一个线程,操作state的方式分为独占方式和共享方式。

独占方式获取和释放资源的方法为:

  • acquire(int arg)(不响应中断)
  • void acquireInterruptibly(int arg),该方法可响应中断(抛异常)
  • boolean release(int arg)

共享方式下相关方法为:

  • acquireShared(int arg)
  • void acquireSharedInterruptibly(int arg)
  • boolean releaseShared(int arg)

下面以独占方式为例,介绍获取与释放锁的步骤。

》获锁:acquire()

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

当一个线程调用acquire方法获取独占资源时,会首先使用tryAcquire(int arg)方法获取资源,该方法实际上是一个空方法(如下),交由具体的子类去实现。

protected boolean tryAcquire(int arg) {
    	//直接报错,不支持的操作
        throw new UnsupportedOperationException();
    }

tryAcquire()方法的具体实现留到后面进行说明,这里只要知道其本质就是去设置state变量的值,如果修改成功该方法会直接返回true,因此acquire方法直接返回。

否则可以看到调用了acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法,其中

  • addWaiter(Node.EXCLUSIVE)方法将当前线程封装成一个类型为Node.EXCLUSIVENode节点,然后插入到AQS阻塞队列的尾部

    private Node addWaiter(Node mode) {
        	//以传入类型来构造新节点
            Node node = new Node(mode);
    		//CAS循环设置该节点到阻塞队列尾部
            for (;;) {
                Node oldTail = tail;
                if (oldTail != null) {
                    node.setPrevRelaxed(oldTail);
                    //CAS设置节点到队尾,成功则返回
                    if (compareAndSetTail(oldTail, node)) {
                        oldTail.next = node;
                        return node;
                    }
                } else {
                    //队列为空先进行初始化
                    initializeSyncQueue();
                }
            }
        }
    
  • acquireQueued方法实现如下:

    final boolean acquireQueued(final Node node, int arg) {
        	//设置中断标志(不允许中断)
            boolean interrupted = false;
            try {
                for (;;) {
                    //获取前驱节点
                    final Node p = node.predecessor();
                    //前驱节点为头节点,会再次尝试获取锁资源,成功把当前节点设置成新的头节点,返回中断标志
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null; // help GC
                        return interrupted;
                    }
                    //请求失败后调用,根据节点的不同状态进行相应的操作
                    if (shouldParkAfterFailedAcquire(p, node))
                        //会调用park方法阻塞线程,捕捉中断信号,但不响应(抛异常)
                        interrupted |= parkAndCheckInterrupt();
                }
            } catch (Throwable t) {
                //如果异常会取消该节点的同步动作,如果发生过中断会进行中断处理
                cancelAcquire(node);
                if (interrupted)
                    selfInterrupt();
                throw t;
            }
        }
    
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
            int ws = pred.waitStatus;
            if (ws == Node.SIGNAL)
                /*
                 * This node has already set status asking a release
                 * to signal it, so it can safely park.
                 */
                //当前节点是signal状态,返回true,表明需要阻塞并等待后续唤醒
                return true;
            if (ws > 0) {
                /*
                 * Predecessor was cancelled. Skip over predecessors and
                 * indicate retry.
                 */
                //如果当前节点的前驱节点是取消同步的节点,需要找到合法的前驱节点
                do {
                    node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                //将当前节点连接到合法的前驱后面
                pred.next = node;
            } else {
                /*
                 * 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.
                 */
                //否则(要么没设置状态;要么要进行唤醒传播)重新设置状态
                pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
            }
        	//返回false,表明需要重新争用资源
            return false;
    
    private final boolean parkAndCheckInterrupt() {
        	//阻塞线程
            LockSupport.park(this);
            return Thread.interrupted();
        }
    

总流程图如下:

在这里插入图片描述

》释放锁:release()

public final boolean release(int arg) {
    	//tryRelease释放资源
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                //唤醒后继节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

与获锁时类似,当一个线程调用release方法时,会首先使用tryRelease(int arg)方法释放资源,该方法也是一个空方法(如下),交由具体的子类去实现。其本质还是通过CAS设置state变量的值。

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)
            node.compareAndSetWaitStatus(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 p = tail; p != node && p != null; p = p.prev)
                if (p.waitStatus <= 0)
                    s = p;
        }
        if (s != null)
            //唤醒后继节点
            LockSupport.unpark(s.thread);
    }

在成功修改state值后,会调用unparkSuccessor(thread)方法激活AQS队列里面被阻塞的一个线程,这个被激活的线程会使用tryAcquire(arg)方法再次尝试获取资源(就是上面aquireQueue中获锁的内容)。

  • 共享方式下的资源获取释放流程与上面类似,这里不做讨论
  • 使用可响应中断方法获取资源的方式会增加响应中断的逻辑(有中断会抛异常),有兴趣可以去自行研究

》条件变量的使用

首先通过一个简单的例子来理解Condition的使用

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionTest {
    public static void main(String[] args) {
        //创建一个独占锁
        ReentrantLock lock = new ReentrantLock();
        //创建一个条件变量
        Condition condition = lock.newCondition();
        new Thread(() -> {
            lock.lock();
            try{
                System.out.println("已上锁,加入条件队列等待");
                /*try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/
 				//挂起当前线程(类似于Object.wait()方法,只能在获取到锁后调用)
                condition.await();
                System.out.println("等待结束");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }).start();
        new Thread(() -> {
            /*try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
            lock.lock();
            try{
                System.out.println("已上锁,唤醒条件队列中的线程");
                //唤醒挂起的线程(类似于Object.notify()方法,只能在获取到锁后调用)
                condition.signal();
                System.out.println("唤醒完毕");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }).start();
    }
}

可以看出,类似于配合synchronized使用的waitnotifyCondition条件变量也可以配合Lock锁来实现线程间的同步动作。但两者的不同之处在于AQS的锁可以对应多个条件变量进行同步控制。

接下来我们看看条件变量的await()方法和signal()方法具体做了那些工作

public final void await() throws InterruptedException {
    		//有中断抛异常
            if (Thread.interrupted())
                throw new InterruptedException();
    		//创建新的node节点,插到条件队列尾部
            Node node = addConditionWaiter();
    		//释放当前线程获取的锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
    		//如果线程处于正常状态,调用park方法挂起线程
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
    		//跳出上面的循环(被唤醒)会调用acquireQueued方法进行当前节点在AQS队列中的动作(signal方法会唤醒线程并加入AQS队列)
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled 删除取消同步的节点
                unlinkCancelledWaiters();
            if (interruptMode != 0) //有中断处理中断
                reportInterruptAfterWait(interruptMode);
        }

可以看出,调用await会在内部构造一个类型为Node.CONDITIONnode节点,并加入条件队列末尾,然后释放锁进行阻塞,等待后续被唤醒。

public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
    		//获取队头节点
            Node first = firstWaiter;
            if (first != null)
                //将队头节点移到AQS队列
                doSignal(first);
        }
private void doSignal(Node first) {
            do {
                //修改条件队列的队头队尾
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
    	//已经取消同步的节点不做处理
        if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
            return false;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * indicate that thread is (probably) waiting. If cancelled or
         * attempt to set waitStatus fails, wake up to resync (in which
         * case the waitStatus can be transiently and harmlessly wrong).
         */
    	//enq方法将节点加入AQS队尾
        Node p = enq(node);
        int ws = p.waitStatus;
    	//设置该节点的状态,并唤醒节点
        if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

可以看出,signal()方法会将条件队列队头节点移入AQS,并且修改状态,唤醒该线程以进行后续动作(await()方法里while语句后面的逻辑)。

signalAll()方法与signal类似,只不过针对的是条件队列中的所有节点。

最后做一个小结:

在这里插入图片描述

一个锁对应一个AQS阻塞队列,当线程拿不到锁时会加入队尾调用park方法阻塞自己,直到被唤醒,再调用tryAcquire方法获取锁。一个锁可对应多个条件变量,每个条件变量维护自己的一个队列,当一个线程调用了条件变量的await方法,便会把当前线程加入队列进行等待,并释放锁。当其他线程调用了该条件变量的sign方法,会唤醒条件队列中的队头元素,加入AQS中等待获取锁。

3、实现自定义锁

了解了锁实现的原理之后,我们尝试基于AQS自定义一个同步锁,深化我们的理解。

自定义不可重入排它锁NonReetrantLock

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class NonReentrantLock implements Lock, java.io.Serializable{
    
    //定义一个内部辅助类
    private static class Sync extends AbstractQueuedSynchronizer{
        //判断锁是否被获取
        protected boolean isHeldExclusively(){
            //state为1表明有其他线程已经占用了锁
            return getState() == 1;
        }

        //尝试获取锁
        protected boolean tryAcquire(int acquires){
            assert acquires == 1;//判断是否真的有请求
            if(compareAndSetState(0, 1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        
        //尝试释放锁
        protected boolean tryRelease(int releases){
            assert releases == 1;
            if(getState() == 0){//若state为0,表明锁没有被占有,报错
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        //提供条件变量的生成接口
        Condition newCondition(){
            return new ConditionObject();
        }
    }
    
    //创建一个辅助类进行具体工作
    private final Sync sync = new Sync();
    
    /*
    * 下面是锁提供的一些外部接口
    * */
    
    @Override
    public void lock() {
        sync.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
    
    public boolean isLock(){
        return sync.isHeldExclusively();
    }
}

测试类:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;

public class NonReentrantLockTest {
    public static void main(String[] args) {
        NonReentrantLock lock = new NonReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(() -> {
            lock.lock();
            try{
                if(lock.isLock()){
                    System.out.println("已上锁,加入条件队列等待");
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                condition.await();
                System.out.println("等待结束");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }).start();
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                if(lock.tryLock(1, TimeUnit.SECONDS)){
                    System.out.println("已上锁,唤醒条件队列中的线程");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try{
                condition.signal();
                System.out.println("唤醒完毕");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }).start();
    }
}

执行结果:

在这里插入图片描述

小结

本篇文章详细探讨了Lock包下的锁底层机制,主要从锁的组成、锁的底层操作逻辑两个方面进行了讨论,但是没有涉及具体的锁实现,后续将对该包下的各种锁实现进行分析。

参考资料
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leo木

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值