Lock锁机制原理(二)ReentrantLock和ReentrantReadWriteLock

本文深入探讨了Java中ReentrantLock和ReentrantReadWriteLock的实现原理及应用案例,包括锁的获取与释放流程、公平与非公平模式的区别,以及如何利用这两种锁提高多线程程序的效率。

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

Lock锁机制原理(二)

在前面的文章中,我们已经详细地了解了Lock锁的底层实现原理,本篇文章将着眼于Lock锁的具体实现,讨论Java提供了哪些锁对象供开发者使用。

一、独占锁ReentrantLock

概念ReentrantLock是可重入的独占锁,同一时刻只能有一个线程拿到锁资源,其他会进入AQS队列进行阻塞。

ReentrantLock可以有两种模式:公平和非公平,这两个模式区别不大,只是在获锁的逻辑上稍有不同(一个先加入AQS队列再根据情况尝试获锁;一个直接获锁,拿不到在加入队列)。

1)组成

ReentrantLock类图如下:

在这里插入图片描述

跟我们在(一)中的自定义锁是不是类似~ ^ ^

可以看出,ReentrantLock也是基于AQS来实现的。

静态内部类Sync:图中的辅助内部类Sync直接继承于AQS,为了实现公平和非公平,定义了它的两个子类FairSyncNonfairSync,这两个子类中只定义了一个tryAcquire()方法。

state变量AQS中实现同步的关键字段state的值表示线程获取该锁的可重入次数。如果state=0,表明没有线程持有锁;否则表明锁被某个线程占有。

2)方法实现

Lock包下的锁获取和释放逻辑都是类似的,本质上都是对state变量的修改。

获锁:当一个线程第一次获取RenntrantLock锁时,会尝试使用CASstate设置为1,如果设置成功表明成功拿到锁,记录该锁的持有者为当前线程;后续如果该线程再次尝试获取锁,state值会增加表明重入的次数。

解锁:线程释放锁时会通过CASstate值减少,当state值减少到0时,当前线程会释放锁,并唤醒后续线程。

下面关注实现上述逻辑的几个关键方法:

由上一篇文章我们知道,实现state状态值修改的关键逻辑都在tryAcquire()tryRelease()这两个方法中,因此下面我们以这个为基础展开讲解。

》获锁

  • tryAcquire(int acquires)方法(Sync类下定义,分为公平和非公平两种实现)

    非公平

    //NonFairSync类下
    protected final boolean tryAcquire(int acquires) {
        		//调用基类的方法
                return nonfairTryAcquire(acquires);
            }
    
    //Sync基类下(默认非公平)
    final boolean nonfairTryAcquire(int acquires) {
        		//获取当前线程和状态值
                final Thread current = Thread.currentThread();
                int c = getState();
        		//如果状态值为0,表明锁空闲,尝试获锁
                if (c == 0) {
                    //CAS获锁
                    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;
                }
        		//拿不到锁;返回false
                return false;
            }
    

    公平

    //FairSync类下
    protected final boolean tryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    //公平只在这里多个hasQueuedPredecessors()方法
                    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;
            }
    

    可以看到非公平下tryAcquire()方法只在获锁之前调用了hasQueuedPredecessors()方法,我们看看这个方法做了什么

    public final boolean hasQueuedPredecessors() {
            Node h, s;
        	//头节点不为null,表明AQS中有元素
            if ((h = head) != null) {
                //如果第一个AQS的元素为空或者该元素取消了同步,找到合法节点
                if ((s = h.next) == null || s.waitStatus > 0) {
                    s = null; // traverse in case of concurrent cancellation
                    for (Node p = tail; p != h && p != null; p = p.prev) {//从后往前遍历找到离头结点最近的合法节点,作为目标节点(可以尝试获锁的节点)
                        if (p.waitStatus <= 0)
                            s = p;
                    }
                }
                //如果存在合法的目标节点并且不是当前线程节点,获锁失败,会阻塞在AQS中
                if (s != null && s.thread != Thread.currentThread())
                    return true;
            }
        	//head为空或者AQS的目标节点就是当前线程,返回false尝试获锁
            return false;
        }
    

    简单理解:公平锁会先加入AQS队列排队,只有排到自己才能去尝试获锁;非公平先直接尝试获锁,拿不到在加入队列

  • lock方法

    该方法是ReentrantLock锁对外提供的获锁方法,其本质还是调用了上面的tryAcquire()方法,整个的调用链为lock()->acquire()->tryAcquire(),其中acquire()AQS实现,该方法会调用tryAcquire()方法,而这个方法由子类去具体实现。(跟第一篇文章关联起来了,是不是很清晰)

    public void lock() {
            sync.acquire(1);
        }
    
    //AQS类中
    public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
    

上面的代码正是我们第一篇文章中分析过的方法,这里不在赘述。再次强调之前说过的结论:JUC中任何锁的实现都会去自定义tryAcquire()方法,该方法确切规定了锁对state变量的控制,而对于具体的阻塞和唤醒操作,都依靠底层的AQS类来实现!

其实上面两个方法已经包含了获锁的基本逻辑,但是ReentrantLock还提供了几个方法供用户灵活使用,这几个方式实现了非阻塞式的获锁,避免发生死锁问题。

  • tryLock()

    public boolean tryLock() {
            return sync.nonfairTryAcquire(1);
        }
    

    该方法尝试获取锁,获取成功返回true;否则false。可以看出底层还是基于非公平获锁的方法来实现(主动尝试获取锁本身就是一种非公平行为)。

  • tryLock(long timeout, TimeUnit unit)

    类似于上一个方法,但是加了超时时间,在一定时间内没拿到才返回false。

  • void lockInterruptibly()

    lock方法类似,但是会进行中断响应,底层还是使用了AQSacquireInterruptibly()方法,这里不做过多介绍,有兴趣自行研究。

》解锁

解锁的操作与获锁类似,调用链为unlock()->release(int args)->tryRelease(int args),其中的关键还是由ReentrantLock本身实现的tryRelease()方法。

public void unlock() {
        sync.release(1);
    }
public final boolean release(int arg) {//上一篇文章的内容..
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
//辅助内部类Sync下
protected final boolean tryRelease(int releases) {
            int c = getState() - releases;//更新state值
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();//当前线程没有锁肯定报异常
            boolean free = false;//锁释放成功标志
            if (c == 0) {//state等于0了就放锁
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;//返回成功标志
        }

锁释放比较简单,不做过多介绍,强调一点:释放锁的本质还是对state变量做判断修改

3)案例

最后来个简单的案例熟悉下ReentrantLock的使用。我们用ReentrantLock来实现一个线程安全的List

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockList {
    private volatile ReentrantLock lock;
    private List<Integer> list = new ArrayList<>();

    public ReentrantLockList() {
        this.lock = new ReentrantLock();
    }

    public void add(int num){
        lock.lock();
        try {
            list.add(num);
        }finally {
            lock.unlock();
        }
    }

    public void remove(int i){
        lock.lock();
        try {
            list.remove(i);
        }finally {
            lock.unlock();
        }
    }

    public int get(int i){
        lock.lock();
        try {
            return list.get(i);
        }finally {
            lock.unlock();
        }
    }

    @Override
    public String toString() {
        return "ReentrantLockList{" +
                "list=" + list +
                '}';
    }
	
    //模拟两个线程并发增加元素
    public static void main(String[] args) throws InterruptedException {
        ReentrantLockList lockList = new ReentrantLockList();
        new Thread(() -> {
            for(int i = 0; i < 10; ++i){
                try {
                    Thread.sleep(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lockList.add(i);
            }
        }).start();
        new Thread(() -> {
            for(int i = 10; i < 20; ++i){
                try {
                    Thread.sleep(i-9);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lockList.add(i);
            }
        }).start();
        Thread.sleep(1000);
        System.out.println(lockList);
    }
}

代码比较简单,第一个线程拿到锁后增加元素,此时第二个线程会加入AQS队列进行阻塞,之后第一个线程增加完后唤醒阻塞的线程2,并睡眠,第二个线程拿到锁后也开始增加元素,后续是一样的。

二、读写锁ReentrantReadWriteLock

概念:上面提出的ReentrantLock已经可以有效解决线程安全问题了,那么为什么还要提出ReentrantReadWriteLock? 其实正如它的名字一样,ReentrantReadWriteLock是为了解决在实际场景中的写少读多问题,该锁通过读写分离的策略,允许多个线程可以同时获取读锁,大大提高了多线程场景下读的性能。

1)组成

ReentrantReadWriteLock类图(只展示出了关键方法):

大体结构与ReentrantLock是类似的,但是ReentrantReadWriteLock内部构造了两把锁:读锁ReadLock和写锁WriteLock,这两把锁依赖于Sync来实现具体功能。

读写锁一个很巧妙地功能就是用一个state的高低位来表示读写两种的状态,这主要是因为CAS只能对一个int类型的变量进行原子操作。我们来看看代码中是如何来定义的:

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

		//偏移量	
        static final int SHARED_SHIFT   = 16;
    	//共享锁状态单位值65536
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    	//共享锁线程最大数65535
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
    	//排他锁掩码,低15位为1
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        /** Returns the number of shared holds represented in count. */
    	//返回读写线程数
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /** Returns the number of exclusive holds represented in count. */
    	//返回写锁可重入数
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
		
        static final class HoldCounter {
            int count;          // initially 0
            // Use id, not reference, to avoid garbage retention
            final long tid = LockSupport.getThreadId(Thread.currentThread());
        }

        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }
    	//存放除第一个获取读锁线程外的其他线程获取读锁的可重入次数
    	private transient ThreadLocalHoldCounter readHolds;
		//记录最后一个获取到读锁的线程获取到读锁的可重入次数
        private transient HoldCounter cachedHoldCounter;
		//记录第一个获取到读锁的线程
        private transient Thread firstReader;
    	//第一个获取到读锁的线程获取读锁的可重入次数
        private transient int firstReaderHoldCount;

        Sync() {
            readHolds = new ThreadLocalHoldCounter();
            setState(getState()); // ensures visibility of readHolds
        }
    
    /*
    * 其他代码
    **/
}

可以看出,ReentrantReadWriteLock针对读锁提供了一些额外的类和字段来记录其状态;而因为写锁是排他的,所以只需要通过state变量的低16位来判断可重入数即可。

这里获取到读锁的线程的可重入状态是通过ThreadLocal来持有HoldCounter对象来实现的(ThreadLocalHoldCounter),ThreadLocal是线程共享变量,可以存放线程独有的数据。

2)方法实现

这里以非公平锁为例

》获写锁

  • tryAcquire(int acquires)方法

    protected final boolean tryAcquire(int acquires) {
                /*
                 * Walkthrough:
                 * 1. If read count nonzero or write count nonzero
                 *    and owner is a different thread, fail.
                 * 2. If count would saturate, fail. (This can only
                 *    happen if count is already nonzero.)
                 * 3. Otherwise, this thread is eligible for lock if
                 *    it is either a reentrant acquire or
                 *    queue policy allows it. If so, update state
                 *    and set owner.
                 */
                Thread current = Thread.currentThread();
                int c = getState();
                int w = exclusiveCount(c);
                if (c != 0) {//读锁或写锁已被某线程获取
                    // (Note: if c != 0 and w == 0 then shared count != 0)
                    if (w == 0 || current != getExclusiveOwnerThread())
                        return false;//如果有其他线程获取写锁,返回false表明拿不到锁
                    if (w + exclusiveCount(acquires) > MAX_COUNT)//当前线程拿到写锁,更新可重入次数
                        throw new Error("Maximum lock count exceeded");
                    // Reentrant acquire
                    setState(c + acquires);
                    return true;
                }
        		//如果是第一个写线程获取写锁,设置状态并更新持有者
                if (writerShouldBlock() ||
                    !compareAndSetState(c, c + acquires))
                    return false;
                setExclusiveOwnerThread(current);
                return true;
            }
    

    可以看出,这里通过state的值判断锁的获取状态,然后根据不同情况对锁状态进行更新。需要注意的是,这里writerShouldBlock()在公平锁和非公平锁下实现了不同的逻辑,简单来说就是在非公平条件下那么直接返回false,即直接去尝试修改state值;而公平条件下如果该写锁不是头节点,那么返回true,上面if条件直接成立,线程不会尝试修改state值,而是返回false。

    //FairSync公平实现
    final boolean writerShouldBlock() {
                return hasQueuedPredecessors();
            }
    //NoFairSync非公平实现
    final boolean writerShouldBlock() {
                return false; // writers can always barge
            }
    
  • lock()方法

    ReentrantLock类似,ReentrantReadWriteLock也提供了相关的接口供我们获锁和释放锁。其本质还是使用了Sync提供的tryAcquire()/tryRelease()方法

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

    ReentrantLock完全相同,不多讲了。

    WriteLock也提供了相应的非阻塞获读锁的方法,与ReentrantLock是类似的,这里不做过多介绍。

》释放写锁

  • tryRelease()方法

    protected final boolean tryRelease(int releases) {
        		//判断是否是写锁持有者调用的解锁方法
                if (!isHeldExclusively())
                    throw new IllegalMonitorStateException();
        		//获取可重入值(此时高16位一定为0)
                int nextc = getState() - releases;
                boolean free = exclusiveCount(nextc) == 0;
        		//如果可重入值为0则释放锁
                if (free)
                    setExclusiveOwnerThread(null);
        		//更新状态值
                setState(nextc);
                return free;
            }
    

    比较直观,根据锁的重入状态更新state值

  • unlock()方法

    public void unlock() {
                sync.release(1);
            }
    
    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;
        }
    

    可以看出,lock()方法主要做了两件事:修改state值以及唤醒后续线程

》获取读锁

  • tryAcquireShared(int arg)方法

    protected final int tryAcquireShared(int unused) {
                /*
                 * Walkthrough:
                 * 1. If write lock held by another thread, fail.
                 * 2. Otherwise, this thread is eligible for
                 *    lock wrt state, so ask if it should block
                 *    because of queue policy. If not, try
                 *    to grant by CASing state and updating count.
                 *    Note that step does not check for reentrant
                 *    acquires, which is postponed to full version
                 *    to avoid having to check hold count in
                 *    the more typical non-reentrant case.
                 * 3. If step 2 fails either because thread
                 *    apparently not eligible or CAS fails or count
                 *    saturated, chain to version with full retry loop.
                 */
                Thread current = Thread.currentThread();
                int c = getState();
        		//判断写锁是否被占用(锁降级)
                if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)
                    return -1;
        		//获取读锁计数
                int r = sharedCount(c);
        		//尝试获取读锁,成功则更新信息,否则(多个在获取)调用fullTryAcquireShared方法进行重试
                if (!readerShouldBlock() &&
                    r < MAX_COUNT &&
                    compareAndSetState(c, c + SHARED_UNIT)) {
                    if (r == 0) {//如果是一个获取读锁的线程
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {//如果当前线程是第一个获取读锁的线程,更新可重入值
                        firstReaderHoldCount++;
                    } else {
                        HoldCounter rh = cachedHoldCounter;
                        //判断是否是最后一个获取读锁的线程
                        if (rh == null ||
                            rh.tid != LockSupport.getThreadId(current))
                            cachedHoldCounter = rh = readHolds.get();//更新最后一个获取读锁的线程
                        else if (rh.count == 0)//如果是最后一个获取读锁的线程并且是第一次获取,记录在ThreadLocal结构中
                            readHolds.set(rh);
                        rh.count++;//增加当前线程的读锁可重入数
                    }
                    return 1;
                }
                return fullTryAcquireShared(current);
            }
    

    上面的代码主要逻辑是获取state状态值判断写锁是否被占用,如果是,那么表明当前线程无法获取读锁,直接返回-1(阻塞);这里要注意如果是当前线程占用了写锁,那么依旧可以继续获取读锁(但没有释放写锁),这里可以进行锁降级操作

    还有一个要关注的点就是readerShouldBlock()方法,其非公平的实现如下:

    final boolean readerShouldBlock() {
                /* As a heuristic to avoid indefinite writer starvation,
                 * block if the thread that momentarily appears to be head
                 * of queue, if one exists, is a waiting writer.  This is
                 * only a probabilistic effect since a new reader will not
                 * block if there is a waiting writer behind other enabled
                 * readers that have not yet drained from the queue.
                 */
                return apparentlyFirstQueuedIsExclusive();
            }
    

    该方法调用了AQS底层定义的apparentlyFirstQueuedIsExclusive()

    final boolean apparentlyFirstQueuedIsExclusive() {
            Node h, s;
            return (h = head) != null &&
                (s = h.next)  != null &&
                !s.isShared()         &&
                s.thread != null;
        }
    

    这个方法会去判断AQS队列中的第一个阻塞线程是否在获取写锁,如果是,则让出竞争权,返回fullTryAcquireShared(current)去自旋获取锁(如果写锁被拿了还是会阻塞)。这些操作的目的其实是为了防止获取读锁的线程长时间饥饿,所以也可以看成是一种写优先的思想。

    注:在公平条件下不会进行这个判断,还是直接进行排队,只有轮到自己才能去获取修改状态。

  • lock()方法

    public void lock() {
                sync.acquireShared(1);
            }
    public final void acquireShared(int arg) {
            if (tryAcquireShared(arg) < 0)
                //类似于acquireQueued,提供共享模式下阻塞线程的方法
                doAcquireShared(arg);
        }
    

    熟悉的代码模式

ReadLock也提供了相应的非阻塞获读锁的方法,与ReentrantLock是类似的,这里不做过多介绍。

》释放读锁

  • tryreleaseShared(int arg) 方法

    protected final boolean tryReleaseShared(int unused) {
                Thread current = Thread.currentThread();
                /*
                * 获取读锁的线程信息更新(第一个获取读锁的线程,最后一个获取读锁的线程,以及它们的可重入值..)
                **/
                for (;;) {//循环直到自己读状态的计数值减一
                    int c = getState();
                    int nextc = c - SHARED_UNIT;
                    if (compareAndSetState(c, nextc))
                        // 为0表明没有其他线程持有读锁(写锁),方法返回后会尝试唤醒后续阻塞线程(因为是共享锁,因此会进行传播唤醒)
                        return nextc == 0;
                }
            }
    

    这里的逻辑比较明确,但要注意一个问题,如果是发生了刚刚提出的锁降级操作,即当前线程把持住写锁,也获取读锁进行操作,即使后续读锁被释放,这里的nextc==0也是不成立的,只有读锁在写锁释放后释放才会满足这一条件。

  • unlock()方法

    public void unlock() {
                sync.releaseShared(1);
            }
    public final boolean releaseShared(int arg) {
            if (tryReleaseShared(arg)) {
                //传播唤醒后续线程
                doReleaseShared();
                return true;
            }
            return false;
        }
    

    底层还是tryReleaseShared()方法。

3)案例

最后同样来个简单的案例理解一下ReentrantReadWriteLock的使用。

import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantReadWriteLockList {
    private ArrayList<Integer> arrayList = new ArrayList<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    public void add(int num){
        writeLock.lock();
        try{
            arrayList.add(num);
        }finally {
            writeLock.unlock();
        }
    }

    public void remove(int index){
        writeLock.lock();
        try{
            arrayList.remove(index);
        }finally {
            writeLock.unlock();
        }
    }

    public void get(int index){
        readLock.lock();
        try{
            arrayList.get(index);
        }finally {
            readLock.unlock();
        }
    }
}

上面的自定义List在修改元素时使用写锁,在访问元素时使用读锁,在读多写少的场景下性能会更好。

下面代码中的lockDown()方法演示了锁降级的操作,这保证了数据的一致性。

public void lockDownShow(){
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        readLock.lock();
        System.out.println(list.get(list.size() - 1));
        readLock.unlock();
        writeLock.lock();
        try {
            list.add(123456);
            readLock.lock();//如果不先获取读锁,而是先释放写锁,那么就不是锁降级,因为其他线程可能会拿到写锁进行数据的修改
        }finally {
            writeLock.lock();
        }
        try {
            System.out.println(list.get(list.size() - 1));
        }finally {
            readLock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantReadWriteLockList list = new ReentrantReadWriteLockList();
        list.lockDownShow();
    }

结果:

如果没有锁降级,上面的123456可能是一个不确定的值(被其他线程修改)。

小结

本篇文章主要讲了lock包下的两种锁实现ReentrantLockReentrantReadWriteLock,由于篇幅较长,将在后续文章中进一步探讨该包下一些线程同步器的实现原理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Leo木

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

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

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

打赏作者

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

抵扣说明:

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

余额充值