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可以实现一个方法中读写分离的锁机制。

posted @ 2021-08-13 19:25  时空穿越者  阅读(241)  评论(0编辑  收藏  举报