• java并发:线程并发控制机制之ReadWriteLock


    ReadWriteLock

    Java中的ReadWriteLock是什么?

    一般而言,读写锁是用来提升并发程序性能的锁分离技术的成果,Java中的ReadWriteLock是Java5中新增的一个接口,提供了readLock和writeLock两种锁机制。

    一个ReadWriteLock维护一对关联的锁,一个用于只读操作,一个用于写;在没有写线程的情况下,一个读锁可能会同时被多个读线程持有,写锁是独占的。

    我们来看一下ReadWriteLock的源码:

    public interface ReadWriteLock{
        Lock readLock();
        Lock writeLock();
    }

    解读:

    从源码上面我们可以看出来ReadWriteLock并不是Lock的子接口,只不过ReadWriteLock借助Lock来实现读写两个锁并存、互斥的操作机制。

    在ReadWriteLock中每次读取共享数据时需要读取锁,当修改共享数据时需要写入锁,这看起来好像是两个锁,但是并非如此。

    ReentrantReadWriteLock

    ReentrantReadWriteLock是ReadWriteLock在java.util里面唯一的实现类,主要使用场景是当有很多线程都从某个数据结构中读取数据,而很少有线程对其进行修改。

    示例

    例一

    ReadLock和WriteLock单独使用的情况

    package demo.thread;
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    public class ReadWriteLockDemo {
        public static void main(String[] args) {
            final Count ct = new Count();
            for (int i = 0; i < 2; i++) {
                new Thread() {
                    @Override
                    public void run() {
                        ct.get();
                    }
                }.start();
            }
            for (int i = 0; i < 2; i++) {
                new Thread() {
                    @Override
                    public void run() {
                        ct.put();
                    }
                }.start();
            }
        }
    }
    
    class Count {
        private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    
        public void get() {
            rwl.readLock().lock();// 上读锁,其他线程只能读不能写,具有高并发性
            try {
                System.out.println(Thread.currentThread().getName() + " read start.");
                Thread.sleep(1000L);// 模拟干活
                System.out.println(Thread.currentThread().getName() + "read end.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                rwl.readLock().unlock(); // 释放写锁,最好放在finnaly里面
            }
        }
    
        public void put() {
            rwl.writeLock().lock();// 上写锁,具有阻塞性
            try {
                System.out.println(Thread.currentThread().getName() + " write start.");
                Thread.sleep(1000L);// 模拟干活
                System.out.println(Thread.currentThread().getName() + "write end.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                rwl.writeLock().unlock(); // 释放写锁,最好放在finnaly里面
            }
        }
    
    }

    运行结果如下:

    Thread-1 read start.
    Thread-0 read start.
    Thread-1read end.
    Thread-0read end.
    Thread-3 write start.
    Thread-3write end.
    Thread-2 write start.
    Thread-2write end.

    从结果上面可以看的出来,读的时候是并发的,写的时候是有顺序的带阻塞机制的

    例二

    ReadLock和WriteLock的复杂使用情况,模拟一个有读写数据的场景

    private final Map<String, Object> map = new HashMap<String, Object>();// 假设这里面存了数据缓存
    private final ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
    
    public Object readWrite(String id) {
          Object value = null;
          rwlock.readLock().lock();// 首先开启读锁,从缓存中去取
          try {
                value = map.get(id);
                if (value == null) { // 如果缓存中没有数据,释放读锁,上写锁
                    rwlock.readLock().unlock();
                    rwlock.writeLock().lock();
                    try {
                        if (value == null) {
                            value = "aaa"; // 此时可以去数据库中查找,这里简单的模拟一下
                        }
                    } finally {
                        rwlock.writeLock().unlock(); // 释放写锁
                    }
                    rwlock.readLock().lock(); // 然后再上读锁
                }
            } finally {
                rwlock.readLock().unlock(); // 最后释放读锁
            }
          return value;
    }

    解读:

    请一定要注意读写锁的获取与释放顺序。

    源码解读

    类图如下:

    其构造函数如下:

        /**
         * Creates a new {@code ReentrantReadWriteLock} with
         * default (nonfair) ordering properties.
         */
        public ReentrantReadWriteLock() {
            this(false);
        }
    
        /**
         * Creates a new {@code ReentrantReadWriteLock} with
         * the given fairness policy.
         *
         * @param fair {@code true} if this lock should use a fair ordering policy
         */
        public ReentrantReadWriteLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
            readerLock = new ReadLock(this);
            writerLock = new WriteLock(this);
        }

    解读:

    读写锁的内部维护了一个 ReadLock 和一个 WriteLock,它们依赖 Sync 实现具体功能;Sync继承自 AQS,提供了公平和非公平的实现。

    重点说明:

    AQS 中只维护了一个 state 状态,而 ReentrantReadWriteLock 需要维护读状态和写状态,一个 state 怎么表示写和读两种状态呢?

    ReentrantReadWriteLock 巧妙地使用 state 的高 16 位表示读状态,也即获取到读锁的次数;使用低 16 位表示获取到写锁的线程的可重入次数。

    写锁

    写锁使用 WriteLock 来实现

    获取锁

    对应代码如下:

            /**
             * Acquires the write lock.
             *
             * <p>Acquires the write lock if neither the read nor write lock
             * are held by another thread
             * and returns immediately, setting the write lock hold count to
             * one.
             *
             * <p>If the current thread already holds the write lock then the
             * hold count is incremented by one and the method returns
             * immediately.
             *
             * <p>If the lock is held by another thread then the current
             * thread becomes disabled for thread scheduling purposes and
             * lies dormant until the write lock has been acquired, at which
             * time the write lock hold count is set to one.
             */
            public void lock() {
                sync.acquire(1);
            }

    解读:

    类似于ReentrantLock的实现,实际上是调用了AbstractQueuedSynchronizer的acquire方法,代码如下:

        /**
         * Acquires in exclusive mode, ignoring interrupts.  Implemented
         * by invoking at least once {@link #tryAcquire},
         * returning on success.  Otherwise the thread is queued, possibly
         * repeatedly blocking and unblocking, invoking {@link
         * #tryAcquire} until success.  This method can be used
         * to implement method {@link Lock#lock}.
         *
         * @param arg the acquire argument.  This value is conveyed to
         *        {@link #tryAcquire} but is otherwise uninterpreted and
         *        can represent anything you like.
         */
        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }

    重点关注子类中tryAcquire的实现

    从上图可知,tryAcquire方法定义在Sync中,具体代码如下:

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

    解读:

    写锁是个独占锁,某个时刻只有一个线程可以获取该锁。

    如果当前没有线程获取到读锁和写锁,则当前线程可以获取到写锁然后返回;如果当前己经有线程获取到读锁或者写锁,则当前请求写锁的线程会被阻塞挂起。

    对应上述代码分支:

    (1)AQS状态值不为0

    • 如果w==0则说明状态值的低16位为0,由于其高 16位不为 0,即表明己经有线程获取到读锁,所以直接返回 false。
    • 如果w!=0则说明已经有线程获取到写锁,若当前线程不是该锁的持有者,则返回 false。

    (2)AQS状态值为0

    说明目前没有线程获取到读锁和写锁,对于 writerShouldBlock 方法,由Sync子类实现,公平锁和非公平锁的具体实现不一样。

        /**
         * Nonfair version of Sync
         */
        static final class NonfairSync extends Sync {
            private static final long serialVersionUID = -8159625535654395037L;
            final boolean writerShouldBlock() {
                return false; // writers can always barge
            }
            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();
            }
        }

    解读:

    对于非公平锁来说总是返回false,故如果抢占式执行CAS尝试获取写锁成功则设置当前锁的持有者为当前线程并返回 true,否则返回 false。

        /**
         * Fair version of Sync
         */
        static final class FairSync extends Sync {
            private static final long serialVersionUID = -2274990926593161451L;
            final boolean writerShouldBlock() {
                return hasQueuedPredecessors();
            }
            final boolean readerShouldBlock() {
                return hasQueuedPredecessors();
            }
        }

    解读:

    对于公平锁,其使用 hasQueuedPredecessors来判断当前线程节点是否有前驱节点,如果有则当前线程放弃获取写锁的权限,直接返回 false。

    Note:

    写锁是可重入锁,如果当前线程己经获取了该锁,再次获取时则只是简单地把可重入次数加 1 后直接返回。

    释放锁

            /**
             * Attempts to release this lock.
             *
             * <p>If the current thread is the holder of this lock then
             * the hold count is decremented. If the hold count is now
             * zero then the lock is released.  If the current thread is
             * not the holder of this lock then {@link
             * IllegalMonitorStateException} is thrown.
             *
             * @throws IllegalMonitorStateException if the current thread does not
             * hold this lock
             */
            public void unlock() {
                sync.release(1);
            }

    解读:

    类似于ReentrantLock的实现,实际上是调用了AbstractQueuedSynchronizer的release方法,而子类Sync只需要实现tryRelease方法即可,对应方法如下:

            /*
             * Note that tryRelease and tryAcquire can be called by
             * Conditions. So it is possible that their arguments contain
             * both read and write holds that are all released during a
             * condition wait and re-established in tryAcquire.
             */
            @ReservedStackAccess
            protected final boolean tryRelease(int releases) {
                if (!isHeldExclusively())
                    throw new IllegalMonitorStateException();
                int nextc = getState() - releases;
                boolean free = exclusiveCount(nextc) == 0;
                if (free)
                    setExclusiveOwnerThread(null);
                setState(nextc);
                return free;
            }

    解读:

    如果当前线程持有锁,调用unlock方法会让持有的 AQS 状态值减 1,若减 1 后当前状态值为 0,则当前线程会释放锁,否则仅仅执行减 1。

    如果当前线程没有持有锁,则调用该方法会抛出 IllegalMonitorStateException异常。

    读锁

    读锁是使用ReadLock来实现的。

    获取锁

    对应代码如下:

            /**
             * Acquires the read lock.
             *
             * <p>Acquires the read lock if the write lock is not held by
             * another thread and returns immediately.
             *
             * <p>If the write lock is held by another thread then
             * the current thread becomes disabled for thread scheduling
             * purposes and lies dormant until the read lock has been acquired.
             */
            public void lock() {
                sync.acquireShared(1);
            }

    解读:

    类似于ReentrantLock的实现,实际上是调用了AbstractQueuedSynchronizer的acquireShared方法,代码如下:

        /**
         * Acquires in shared mode, ignoring interrupts.  Implemented by
         * first invoking at least once {@link #tryAcquireShared},
         * returning on success.  Otherwise the thread is queued, possibly
         * repeatedly blocking and unblocking, invoking {@link
         * #tryAcquireShared} until success.
         *
         * @param arg the acquire argument.  This value is conveyed to
         *        {@link #tryAcquireShared} but is otherwise uninterpreted
         *        and can represent anything you like.
         */
        public final void acquireShared(int arg) {
            if (tryAcquireShared(arg) < 0)
                doAcquireShared(arg);
        }

    重点关注子类中tryAcquireShared的实现,即查看Sync中tryAcquireShared方法:

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

    解读:

    如果当前没有其他线程持有写锁,则当前线程可以获取读锁,AQS 的状态值 state的高16位的值会增加 1,然后方法返回;

    如果某个线程持有写锁,则当前线程会被阻塞。

    对应上述代码分支:

    (1)判断写锁是否被占用,如果是则直接返回 -1,而后调用 AQS 的 doAcquireShared方法把当前线程放入 AQS 阻塞队列。

    (2)变量 r 是获取到读锁的个数

    (3)readerShouldBlock方法,由Sync子类实现,公平锁和非公平锁的具体实现不一样;此处不展开描述,可以查看本文前面NonfairSync 和 FairSync的实现代码。

    Note:

    如果当前要获取读锁的线程己经持有了写锁,则也可以获取读锁;当其处理完事情,需要把读锁和写锁都释放掉,不能只释放写锁。

    释放锁

    对应代码如下:

            /**
             * Attempts to release this lock.
             *
             * <p>If the number of readers is now zero then the lock
             * is made available for write lock attempts. If the current
             * thread does not hold this lock then {@link
             * IllegalMonitorStateException} is thrown.
             *
             * @throws IllegalMonitorStateException if the current thread
             * does not hold this lock
             */
            public void unlock() {
                sync.releaseShared(1);
            }

    解读:

    类似于ReentrantLock的实现,实际上是调用了AbstractQueuedSynchronizer的releaseShared方法

        /**
         * Releases in shared mode.  Implemented by unblocking one or more
         * threads if {@link #tryReleaseShared} returns true.
         *
         * @param arg the release argument.  This value is conveyed to
         *        {@link #tryReleaseShared} but is otherwise uninterpreted
         *        and can represent anything you like.
         * @return the value returned from {@link #tryReleaseShared}
         */
        public final boolean releaseShared(int arg) {
            if (tryReleaseShared(arg)) {
                doReleaseShared();
                return true;
            }
            return false;
        }

    子类Sync只需要实现tryReleaseShared方法即可,对应方法如下:

            @ReservedStackAccess
            protected final boolean tryReleaseShared(int unused) {
                Thread current = Thread.currentThread();
                if (firstReader == current) {
                    // assert firstReaderHoldCount > 0;
                    if (firstReaderHoldCount == 1)
                        firstReader = null;
                    else
                        firstReaderHoldCount--;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null ||
                        rh.tid != LockSupport.getThreadId(current))
                        rh = readHolds.get();
                    int count = rh.count;
                    if (count <= 1) {
                        readHolds.remove();
                        if (count <= 0)
                            throw unmatchedUnlockException();
                    }
                    --rh.count;
                }
                for (;;) {
                    int c = getState();
                    int nextc = c - SHARED_UNIT;
                    if (compareAndSetState(c, nextc))
                        // Releasing the read lock has no effect on readers,
                        // but it may allow waiting writers to proceed if
                        // both read and write locks are now free.
                        return nextc == 0;
                }
            }

    解读:

    在无限循环里面,首先获取当前 AQS 状态值并将其保存到变量 c,然后变量 c 减去一个读计数单位后使用 CAS 操作更新 AQS 状态值。

    如果更新成功则查看当前 AQS 状态值

    • 当前AQS 状态值是否为 0,为 0 则说明当前己经没有读线程占用读锁,则 tryReleaseShared 返回 true(随后会调用 doReleaseShared 方法释放一个由于获取写锁而被阻塞的线程)。
    • 当前 AQS 状态值不为 0,则说明当前还有其他线程持有了读锁,则 trγReleaseShared 返回 false。

    Note:

    关于无限循环的解释——如果 CAS 更新 AQS 状态值失败,则自旋重试直到成功。

    比较分析

    ReentrantReadWriteLock与ReentrantLock的比较:

    (1)相同点:都是一种显式锁,手动加锁和解锁,都很适合高并发场景

    (2)不同点:ReentrantLock 是独占锁,ReentrantReadWriteLock是对ReentrantLock的复杂扩展,能适合更复杂的业务场景,ReentrantReadWriteLock可以实现一个方法中读写分离的锁机制。

  • 相关阅读:
    【QT 学习笔记】 一、 VS2015+ QT环境安装
    Ubuntu16.04 + caffe + cuda 环境搭建
    【Caffe学习笔记】一 、环境安装 Caffe + cuda + windows10 + VS2015 安装笔记, win7也适用
    【CNN】 吴恩达课程中几种网络的比较
    【图像处理】二 高斯滤波及其加速方法
    【图像处理 】 一、OSTU分割法
    1028: C语言程序设计教程(第三版)课后习题8.2
    函数和带参的宏,从三个数中找出最大的数
    Last Defence (run time error)
    Penalty
  • 原文地址:https://www.cnblogs.com/studyLog-share/p/15129249.html
Copyright © 2020-2023  润新知