ReadWriteLock接口

205 阅读9分钟

前言

上一篇内容分享了lock接口API使用及其ReentrantLock实现类内部原理,我们知道了lock能够实现一把锁用于保证线程安全

线程安全问题其实本质上来说就是因为多线程同时操作同一个共享资源导致的

根据jmm规范可以用锁的机制来保证的共享资源的可见性、操作的原子性

但是锁这个玩意会让多线程程序强行从并行执行改为串行执行,这会降低程序的运行效率

对于共享资源的操作来说一般分为两种情况读/写,线程并行的组合场景无非是以下四种

线程1线程2

根据这四种场景可以得出以下结论

结论描述
读线程可以并行读我不管
写线程只能串行但写不行
读和写之间是互斥关系读/写不允许同时出现

总结起来得到结论读最新数据(并行),写具备原子性(串行),读写互斥

ReadWriteLock

在jdk中提供了ReadWriteLock接口有一个标准实现类ReentrantReadWriteLock,从名字就可以看出来它是一把读写锁

概念

它维护了一对关联锁,一个只用于读操作,一个只用于写操作

读锁可以由多个读线程同时持有,写锁则是排他

同一时间两把锁不能被不同线程持有

使用场景

适合读取操作对于写入操作的场景、改进互斥锁的性能

比如: 集合的并发线程安全性改造、缓存组件

锁降级

指的是写锁降级成为读锁.持有写锁的同时,再获取读锁,随后释放写锁的过程

写锁是线程独占,读锁是共享.所以写到读是降级.读到写是不能实现的

使用

/**
 * <p>
 *
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/17 0017 16:13
 */
public class ReadWriteLockDemo {

    private static int i = 0;
    private static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private static Lock readLock = readWriteLock.readLock();

    public static void write() {
        writeLock.lock();
        try {
            Thread.sleep(1000);
            i++;
            System.out.println("写后的值:" + i + "写的时间" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }
    }

    public static int read() {
        readLock.lock();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
        }
        return i;
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();

        System.err.println("====并行读======");
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> System.out.println("读到的值:" + read() + "读取的时间" + System.currentTimeMillis()));
        }

        Thread.sleep(1 * 1000 + 100);

        System.err.println("====并行写======");
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> write());
        }

        Thread.sleep(10 * 1000 + 100);

        System.err.println("====并行读写=====");
        for (int i = 0; i < 10; i++) {
            if (i % 2 == 0) {
                executorService.execute(() -> System.out.println("读到的值:" + read() + "读取的时间" + System.currentTimeMillis()));
            } else {
                executorService.execute(() -> write());
            }
        }
        executorService.shutdown();
    }
}

可以看到具体了之前所说的读最新数据(并行),写具备原子性(串行),读写互斥

推理

知道了怎么去使用以后我们就得去思考它是如何实现的了

回顾一下上一篇文章中说到ReentrantLock的内部实现原理图

img

ReentrantLock中主要由cas操作一个count值来控制线程是否能够获取锁

既然读写锁已经将读/写操作各自分成了两把锁,那么是否也可以将count分为读count和写count呢?

读写锁内部实现原理流程推理如下:

读线程先判断writercount是否为0:

​ 1.如果为0通过cas操作将readcount+1 表示获取到读锁

​ 2.不为0则进入等待队列

写线程先判断readcount是否为0:

​ 1.如果不为0则进入等待队列

​ 2.readcount==0 && writercount ==0 情况下通过cas操作将writercount+1并将owner设置为当前线程 表示获取到写锁

​ 3.readcount==0 && owner ==当前线程 情况下将writercount+1 表示再次获取到写锁

通过推理这个流程发现确实很接近读写锁,但是如果有两个线程分别同时操作读/写时

  • 读线程发现writercount == 0
  • 写线程发现readcount == 0

这种情况会导致这个读写锁不安全.因为我们之前说了读和写之间是互斥关系

cas操作针对的是某一个内存地址,如果被拆成了两个count值后会有两个cas操作它们之间是不同步的

手写实现

/**
 * <p>
 *
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/18 0018 15:21
 */
public class MyReadWriteLock implements ReadWriteLock {

    private AtomicReference<Thread> owner = new AtomicReference<>();
    private AtomicInteger readCount = new AtomicInteger();
    private AtomicInteger writerCount = new AtomicInteger();
    private LinkedBlockingDeque<WaitNode> waitNodes = new LinkedBlockingDeque<>();

    private Lock readLock;
    private Lock writeLock;

    public MyReadWriteLock() {
        readLock = new ReadLock();
        writeLock = new WriteLock();
    }

    /**
     * 等待线程类模板
     */
    class WaitNode {
        /**
         * 当前线程
         */
        private Thread thread;
        /**
         * 读/写区分
         */
        private boolean read;

        public WaitNode(Thread thread, boolean read) {
            this.thread = thread;
            this.read = read;
        }
    }

    /**
     * 读锁
     */
    class ReadLock implements Lock {

        @Override
        public void lock() {
            if (!tryLock()) {
                Thread thread = Thread.currentThread();
                waitNodes.offer(new WaitNode(thread, true));
                while (true) {
                    WaitNode nextNode = waitNodes.peek();
                    if (nextNode.thread == thread && nextNode.read && tryLock()) {
                        waitNodes.poll();
                        return;
                    } else {
                        LockSupport.park(thread);
                    }
                }
            }
        }

        @Override
        public void lockInterruptibly() throws InterruptedException {
            if (!tryLock()) {
                Thread thread = Thread.currentThread();
                waitNodes.offer(new WaitNode(thread, true));
                while (true) {
                    WaitNode nextNode = waitNodes.peek();
                    if (thread.isInterrupted()) {
                        throw new InterruptedException();
                    } else if (nextNode.thread == thread && nextNode.read && tryLock()) {
                        waitNodes.poll();
                        return;
                    } else {
                        LockSupport.park(thread);
                    }
                }
            }
        }

        @Override
        public boolean tryLock() {
            if (writerCount.get() != 0 && owner.get() != Thread.currentThread()) {
                return false;
            }
            readCount.incrementAndGet();
            return true;
        }

        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            long beginNanosTimeout = System.nanoTime();
            long awaitNanosTimeout = unit.toNanos(time);
            while (System.nanoTime() - beginNanosTimeout < awaitNanosTimeout) {
                if (tryLock()) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public void unlock() {
            if (readCount.decrementAndGet() >= 0 && writerCount.get() == 0) {
                if (waitNodes.size() > 0) {
                    WaitNode nextNode = waitNodes.peek();
                    LockSupport.unpark(nextNode.thread);
                }
            }
        }

        @Override
        public Condition newCondition() {
            throw new UnsupportedOperationException();
        }
    }

    /**
     * 写锁
     */
    class WriteLock implements Lock {

        @Override
        public void lock() {
            if (!tryLock()) {
                Thread thread = Thread.currentThread();
                waitNodes.offer(new WaitNode(thread, false));
                while (true) {
                    WaitNode nextNode = waitNodes.peek();
                    if (nextNode.thread == thread && !nextNode.read && tryLock()) {
                        waitNodes.poll();
                        return;
                    } else {
                        LockSupport.park(thread);
                    }
                }
            }
        }

        @Override
        public void lockInterruptibly() throws InterruptedException {
            if (!tryLock()) {
                Thread thread = Thread.currentThread();
                waitNodes.offer(new WaitNode(thread, false));
                while (true) {
                    WaitNode nextNode = waitNodes.peek();

                    if (thread.isInterrupted()) {
                        throw new InterruptedException();
                    } else if (nextNode.thread == thread && !nextNode.read && tryLock()) {
                        waitNodes.poll();
                        return;
                    } else {
                        LockSupport.park(thread);
                    }
                }
            }
        }

        @Override
        public boolean tryLock() {
            if (readCount.get() == 0) {
                int wct = writerCount.get();
                if (wct == 0 && writerCount.compareAndSet(wct, 1)) {
                    owner.set(Thread.currentThread());
                    return true;
                } else if (Thread.currentThread() == owner.get()) {
                    writerCount.set(wct + 1);
                    return true;
                }
            }
            return false;
        }

        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            long beginNanosTimeout = System.nanoTime();
            long awaitNanosTimeout = unit.toNanos(time);
            while (System.nanoTime() - beginNanosTimeout < awaitNanosTimeout) {
                if (tryLock()) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public void unlock() {
            int oldWriterCount = writerCount.get();
            int updateWriterCount = oldWriterCount - 1;
            writerCount.compareAndSet(oldWriterCount, updateWriterCount);
            if (updateWriterCount == 0 && readCount.get() == 0) {
                if (waitNodes.size() > 0) {
                    WaitNode nextNode = waitNodes.peek();
                    LockSupport.unpark(nextNode.thread);
                }
            }
        }

        @Override
        public Condition newCondition() {
            throw new UnsupportedOperationException();
        }
    }

    @Override
    public Lock readLock() {
        return readLock;
    }

    @Override
    public Lock writeLock() {
        return writeLock;
    }
}
/**
 * <p>
 *
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/18 0018 20:11
 */
public class ReadWriteLockDemo {
    private static int i = 0;
    private static ReadWriteLock readWriteLock = new MyReadWriteLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private static Lock readLock = readWriteLock.readLock();

    public static void write() {
        writeLock.lock();
        try {
            Thread.sleep(1000);
            i++;
            System.out.println("写后的值:" + i + "写的时间" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }
    }

    public static int read() {
        readLock.lock();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
        }
        return i;
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();

        System.err.println("====并行读======");
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> System.out.println("读到的值:" + read() + "读取的时间" + System.currentTimeMillis()));
        }

        Thread.sleep(1 * 1000 + 100);

        System.err.println("====并行写======");
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> write());
        }

        Thread.sleep(10 * 1000 + 100);

        System.err.println("====并行读写=====");
        for (int i = 0; i < 10; i++) {
            if (i % 2 == 0) {
                executorService.execute(() -> System.out.println("读到的值:" + read() + "读取的时间" + System.currentTimeMillis()));
            } else {
                executorService.execute(() -> write());
            }
        }
        executorService.shutdown();
    }
}

从结果中可以看出自己写的这把读写锁可以实现读最新数据(并行),写具备原子性(串行),读写互斥的功能

但是从这份代码中我们可以发现,出现了大量重复的代码,而这些重复代码大多数聚集于加锁/解锁的方法上

我们是否能够通过设计模式将这些重复的代码进行封装抽象出来?

以往我们经常听到juc和aqs常常伴随到一起.那么这个aqs到底是什么?和我们的lock锁又有什么关系?

AbstractQueuedSynchronizer

这玩意就是江湖人称的aqs(抽象队列同步器),它到底有着什么样的背景足以占据整个juc包的半壁江山

从名字中我们可以看出来它是一个抽象类,事实上来说它其实是定义了一个队列同步的模板

多线程同步的规则或者说步骤进行封装,又将特定的行为规划为子类必须实现的抽象方法

就好比jdbc来说无论谁来写都是如下步骤

加载驱动 创建连接 创建Statement 执行sql 获取结果集 提交/回滚事务

因为jdbc已经规范了操作数据库的行为,这些步骤中只有执行不同的sql其余的操作都几乎都一样

有了这个基础之后便能封装操作数据库的公共行为,所以才有了jdbctemplate的诞生

模板模式

定义一个算法的骨架,将骨架中的特定步骤延迟到子类中(抽象方法由子类实现)

模板方法模式使得子类可以不改变算法的结构即可重新定义该算法的某些特定步骤,也使得子类不必大量编写重复机械性的代码

既然如此那么可以将手写的MyReadWriteLock通过模板模式进行改造,初步认识一下抽象队列同步器

公共锁抽象类

/**
 * <p>
 * 公共锁抽象类
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/22 0022 11:00
 */
public abstract class AbstractCommonLock {

    protected AtomicReference<Thread> owner = new AtomicReference<>();
    private LinkedBlockingDeque<WaitNode> waitNodes = new LinkedBlockingDeque<>();


    /**
     * 等待线程类模板类
     */
    class WaitNode {
        /**
         * 当前线程
         */
        private Thread thread;
        /**
         * 读/写区分
         */
        private boolean read;

        public WaitNode(Thread thread, boolean read) {
            this.thread = thread;
            this.read = read;
        }
    }

    /**
     * 获取共享锁
     *
     * @return 是否获取锁
     */
    public abstract boolean tryLockShared();


    /**
     * 解锁共享锁
     *
     * @return 是否能解锁
     */
    public abstract boolean tryUnlockShared();


    /**
     * 获取独占锁
     *
     * @return 是否获取到锁
     */
    public abstract boolean tryLock();

    /**
     * 解锁独占锁
     *
     * @return 是否能解锁
     */
    public abstract boolean tryUnlock();


    public void lockShared() {
        if (!tryLockShared()) {
            Thread thread = Thread.currentThread();
            waitNodes.offer(new WaitNode(thread, true));
            while (true) {
                WaitNode nextNode = waitNodes.peek();
                if (nextNode.thread == thread && nextNode.read && tryLockShared()) {
                    waitNodes.poll();
                    return;
                } else {
                    LockSupport.park(thread);
                }
            }
        }
    }

    public void lockSharedInterruptibly() throws InterruptedException {
        if (!tryLockShared()) {
            Thread thread = Thread.currentThread();
            waitNodes.offer(new WaitNode(thread, true));
            while (true) {
                WaitNode nextNode = waitNodes.peek();
                if (thread.isInterrupted()) {
                    throw new InterruptedException();
                } else if (nextNode.thread == thread && nextNode.read && tryLockShared()) {
                    waitNodes.poll();
                    return;
                } else {
                    LockSupport.park(thread);
                }
            }
        }
    }


    public boolean tryLockShared(long time, TimeUnit unit) {
        long beginNanosTimeout = System.nanoTime();
        long awaitNanosTimeout = unit.toNanos(time);
        while (System.nanoTime() - beginNanosTimeout < awaitNanosTimeout) {
            if (tryLockShared()) {
                return true;
            }
        }
        return false;
    }

    public void unlockShared() {
        if (tryUnlockShared() && waitNodes.size() > 0) {
            WaitNode nextNode = waitNodes.peek();
            LockSupport.unpark(nextNode.thread);
        }
    }

    public void lock() {
        if (!tryLock()) {
            Thread thread = Thread.currentThread();
            waitNodes.offer(new WaitNode(thread, false));
            while (true) {
                WaitNode nextNode = waitNodes.peek();
                if (nextNode.thread == thread && !nextNode.read && tryLock()) {
                    waitNodes.poll();
                    return;
                } else {
                    LockSupport.park(thread);
                }
            }
        }
    }

    public void lockInterruptibly() throws InterruptedException {
        if (!tryLock()) {
            Thread thread = Thread.currentThread();
            waitNodes.offer(new WaitNode(thread, false));
            while (true) {
                WaitNode nextNode = waitNodes.peek();

                if (thread.isInterrupted()) {
                    throw new InterruptedException();
                } else if (nextNode.thread == thread && !nextNode.read && tryLock()) {
                    waitNodes.poll();
                    return;
                } else {
                    LockSupport.park(thread);
                }
            }
        }
    }

    public boolean tryLock(long time, TimeUnit unit) {
        long beginNanosTimeout = System.nanoTime();
        long awaitNanosTimeout = unit.toNanos(time);
        while (System.nanoTime() - beginNanosTimeout < awaitNanosTimeout) {
            if (tryLock()) {
                return true;
            }
        }
        return false;
    }

    public void unlock() {
        if (owner.get() != Thread.currentThread()) {
            throw new IllegalMonitorStateException();
        } else if (tryUnlock()) {
            if (waitNodes.size() > 0) {
                WaitNode nextNode = waitNodes.peek();
                LockSupport.unpark(nextNode.thread);
            }
        }
    }
}

改造自定义读写锁实现类

/**
 * <p>
 * 自定义读写锁实现类
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/18 0018 15:21
 */
public class MyReadWriteLock implements ReadWriteLock {

    private AbstractCommonLock sync = new AbstractCommonLock() {

        private AtomicInteger readCount = new AtomicInteger();
        private AtomicInteger writerCount = new AtomicInteger();

        @Override
        public boolean tryLockShared() {
            if (writerCount.get() != 0 && owner.get() != Thread.currentThread()) {
                return false;
            }
            readCount.incrementAndGet();
            return true;
        }

        @Override
        public boolean tryUnlockShared() {
            return readCount.decrementAndGet() > 0 && writerCount.get() == 0;
        }

        @Override
        public boolean tryLock() {
            if (readCount.get() == 0) {
                int wct = writerCount.get();
                if (wct == 0 && writerCount.compareAndSet(wct, 1)) {
                    owner.set(Thread.currentThread());
                    return true;
                } else if (Thread.currentThread() == owner.get()) {
                    writerCount.set(wct + 1);
                    return true;
                }
            }
            return false;
        }

        @Override
        public boolean tryUnlock() {
            return writerCount.decrementAndGet() == 0 && readCount.get() == 0;
        }
    };

    private Lock readLock;
    private Lock writeLock;

    public MyReadWriteLock() {
        readLock = new ReadLock();
        writeLock = new WriteLock();
    }

    /**
     * 读锁
     */
    class ReadLock implements Lock {

        @Override
        public void lock() {
            sync.lockShared();
        }

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

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

        @Override
        public boolean tryLock(long time, TimeUnit unit) {
            return sync.tryLockShared(time, unit);
        }

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

        @Override
        public Condition newCondition() {
            throw new UnsupportedOperationException();
        }
    }

    /**
     * 写锁
     */
    class WriteLock implements Lock {

        @Override
        public void lock() {
            sync.lock();
        }

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

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

        @Override
        public boolean tryLock(long time, TimeUnit unit) {
            return sync.tryLock(time, unit);
        }

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

        @Override
        public Condition newCondition() {
            throw new UnsupportedOperationException();
        }
    }

    @Override
    public Lock readLock() {
        return readLock;
    }

    @Override
    public Lock writeLock() {
        return writeLock;
    }
}

测试

/**
 * <p>
 * 自定义读写锁测试类
 * </p>
 *
 * @author pangbohuan
 * @date 2020/12/18 0018 20:11
 */
public class ReadWriteLockDemo {
    private static int i = 0;
    private static ReadWriteLock readWriteLock = new MyReadWriteLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private static Lock readLock = readWriteLock.readLock();

    public static void write() {
        writeLock.lock();
        try {
            Thread.sleep(1000);
            i++;
            System.out.println("写后的值:" + i + "写的时间" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }
    }

    public static int read() {
        readLock.lock();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
        }
        return i;
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        System.err.println("====并行读======");
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> System.out.println("读到的值:" + read() + "读取的时间" + System.currentTimeMillis()));
        }

        Thread.sleep(1 * 1000 + 100);

        System.err.println("====并行写======");
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> write());
        }

        Thread.sleep(10 * 1000 + 100);

        System.err.println("====并行读写=====");
        for (int i = 0; i < 10; i++) {
            if (i % 2 == 0) {
                executorService.execute(() -> System.out.println("读到的值:" + read() + "读取的时间" + System.currentTimeMillis()));
            } else {
                executorService.execute(() -> write());
            }
        }
        executorService.shutdown();
    }
}

从这个本质上来说和我们定义的公共锁抽象类几乎是一样的,但它的实现和我们稍有不同

比如说同步队列,aqs用的是链表而我们用的是队列

但是aqs是一个非常抽象的东西,我们可以先这么简单的理解为就是手写的这个样子

实现了同步状态的管理阻塞线程排队等待通知

  • 同步队列
  • 锁的释放和获取(包含独占/共享)

状态位等其他东西可以在下一篇文章分享多线程协同工具中再进行深入理解

总结

上面手写的读写锁实现类中的读写锁数量被我们分成了两个数量(readcount和writecount)

如果多个线程cas操作这两个count值肯定会出现并发安全问题

但事实上ReadWriteLock用一个int值存储了两个count值

int类型占用32个字符,它把这32个字符分为了前16位(readcount)后16位(writecount)

当我们具体在操作读写锁的时候其实本质上就是在操作被分出来的字符

为了便于理解上面分为了两个count值,既然是一个count那么cas肯定是线程安全的