并发编程的艺术之读书笔记(八)

前言:

上一部分,我们学习了同步器的概念及用法,分析了相关源码,这一部分,我们来一起学习可重入锁和读写锁

1. 可重入锁

可重入锁的意思是同一个线程获得锁之后可以再后续执行过程中再次获得锁而不会造成死锁或阻塞,java中synchronized和ReentrantLock都是可重入锁,这里我们来讨论Lock接口下的ReentrantLock。

ReentrantLock重入锁使用lock()方法来手动为资源上锁,已经获取锁的线程,可以再次调用lock()方法获得锁而不被阻塞。同时ReentrantLock还支持在构造时候指定公平锁还是非公平锁,公平锁的概念是多个线程按照请求锁的顺序获取锁,而非公平锁则是允许插队来获得锁。公平锁的效率不如非公平锁高。ReentrantLock本身没有实现锁,核心逻辑都由AQS同步器来完成,下面通过源码来分析ReentrantLock如何实现重入锁和公平性,ReentrantLock的默认实现是非公平实现。

//非公平的获取锁(可重入)
final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {                                    //状态是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;                              //当同步状态减少到0时,释放成功
        }

那么公平锁的实现和非公平锁的实现有什么区别呢,来看源码

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                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;
        }

//判断当前节点是否有前驱节点
public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

这个方法相比非公平锁的实现多了一个hasQueuedPredecessors()方法,如果这个方法返回true就表示有线程比当前线程更早请求获取锁,那就要等前一个线程释放锁之后才可以获取锁。

2.读写锁

之前我们所说的锁基本都是排它锁,也就是这些锁在同一时刻只能有一个线程访问,而读写锁在同一时刻允许多个读线程访问,但是只有一个写线程可以访问,读写锁中维护了一对锁,分别是读锁和写锁,通过分离读锁和写锁,使得并发性相比一般的排它锁有了很大的提升。一般情况下,读写锁的性能会比排它锁好,因为大多数情况下读场景是多于写的,java并发包提供的读写锁实现是ReentrantReadWriteLock,它的特性如下所示

特性说明
公平性选择支持非公平(默认)和公平的获取锁方式
重入支持锁重入,读线程在获取了读锁后,可以再次获取读锁,写线程在获取写锁后可以再次获取写锁,同时也可以获取读锁
锁降级遵循获取写锁,获取读锁再释放写锁的次序,写锁可以降级为读锁

接下来我们来看一下ReadWriteLock中便于外界监控其工作状态的方法,如下所示

方法名称描述
int getReadLockCount()返回当前读锁被获取的次数。
int getReadHoldCount()返回当前线程获取读锁的次数。
boolean isWriteLocked()判断写锁是否非获取
int getWriteHoldCount()返回当前写锁被获取的次数

下面用一个例子来说明一下读写锁的用法

public class Cache {
    static Map<String, Object> map = new HashMap<>();
    static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    static Lock r = readWriteLock.readLock();
    static Lock w = readWriteLock.writeLock();

    /**
     * 获取一个key对应的value
     */
    public static Object getValue(String key) {
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }

    /**
     * 设置value
     */
    public void setValue(String key, Object value) {
        w.lock();
        try {
            map.put(key, value);
        } finally {
            w.unlock();
        }
    }
}

上面这个例子使用读写锁在保证hashmap线程安全的读取和写入,在读取时,加上读锁,使得并发访问该方法时不会被阻塞,在写入时,加上写锁,使得其他线程对它的读写都被阻塞,当写锁释放时,其他读写操作才能继续。

现在我们知道了怎么用读写锁,接下来我们来分析读写锁的读写状态实现

想一想ReentrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义状态要在同步状态上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。

如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁是将变量切分成了两个部分,高16位表示读,低16位表示写,如下图所示

如上图所示,当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁。读写锁是如何迅速的确定读和写各自的状态呢? 答案是通过位运算。假设当前同步状态值为S,写状态等于 S & 0x0000FFFF(将高16位全部抹去),读状态等于 S >>> 16(无符号补0右移16位)。当写状态增加1时,等于S + 1,当读状态增加1时,等于S + (1 << 16),也就是S + 0×00010000。根据状态的划分能得出一个推论:S不等于0时,当写状态(S & 0x0000FFFF)等于0时,则读状态(S >>> 16)大于0,即读锁已被获取。

我们现在已经知道,写锁是一个支持重入的排它锁,如果当前线程已经获取了写锁,就增加写状态,如果在获取写锁时,读锁已经被获取,或者该线程不是已经获取写锁的线程,则进入等待状态。获取写锁的源码如下

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

该方法除了重入条件(当前线程为获取了写锁的线程)外,还增加了一个读锁是否存在的判断,如果读锁存在,则写锁不能被获取,而写锁一旦被获取,则后续读写线程访问都会被阻塞。

接着看读锁,读锁是支持重入的共享锁,可以被多个线程同时获取,在没有其他写线程访问或者写状态为0的时候,读锁总是可以被成功获取并增加读状态。如果当前线程在获取读锁时,写锁已被其他线程读取,增当前线程进入等待状态。读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数保存在ThreadLocal中,这使得读锁的实现变得更加复杂。现在来看看获取读锁的源码

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);
            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 != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

final int fullTryAcquireShared(Thread current) {
            /*
             * This code is in part redundant with that in
             * tryAcquireShared but is simpler overall by not
             * complicating tryAcquireShared with interactions between
             * retries and lazily reading hold counts.
             */
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                    // else we hold the exclusive lock; blocking here
                    // would cause deadlock.
                } else if (readerShouldBlock()) {
                    // Make sure we're not acquiring read lock reentrantly
                    if (firstReader == current) {
                        // assert firstReaderHoldCount > 0;
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            return -1;
                    }
                }
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

在上面的方法中,如果其他线程已经获取了写锁,那么当前线程获取锁失败,进入等待状态,如果当前线程成功获取了写锁或者写锁未被获取,则当前线程增加读状态(CAS实现原子操作,线程安全),成功获取读锁。

最后我们来看锁降级,锁降级指的是写锁降级成读锁。如果当前线程有写锁,然后释放,最后再获取读锁,这种完成的过程不是锁降级,锁降级是指持有写锁,再获取到读锁,然后释放之前那个写锁的过程。下面看一个锁降级的示例

// 读写锁对象
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void processData() {
        Lock readLock = readWriteLock.readLock();
        Lock writeLock = readWriteLock.writeLock();
        readLock.lock();
        boolean update = false;
        if (!update) {
            //先释放读锁
            readLock.unlock();
            //锁降级从写锁获取到开始
            writeLock.lock();
            try {
                if(!update){
                    //准备数据的过程
                    //...
                    update=true;
                }
            }finally {
                writeLock.unlock();
            }
            try {
                //使用数据的流程
                //...
            }finally {
                readLock.unlock();
            }
        }
    }

上面的示例中,当数据发生变化后,update变量设置为true,这时所有访问这个方法的线程都会感知到变化,但只有一个线程可以获取到写锁,其他线程会被阻塞在读锁和写锁的lock()方法上。当前程获取写锁完成数据准备之后,再 获取读锁,随后释放写锁,完成锁降级。

锁降级中读锁的获取是否必要呢?答案是必要的。主要原因是保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作 线程T)获取了写锁并修改了数据,则当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使 用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。原因也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程不可见。

总结

这一部分,我们一起学习了可重入锁和读写锁,下一部分开始,我们学习Condition接口。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值