写点什么

ReentrantReadWriteLock 读写锁简单原理案例证明

用户头像
叫练
关注
发布于: 2021 年 01 月 31 日
ReentrantReadWriteLock读写锁简单原理案例证明

ReentrantReadWriteLock 存在原因?


我们知道 List 的实现类 ArrayList,LinkedList 都是非线程安全的,Vector 类通过用 synchronized 修饰方法保证了 List 的多线程非安全问题,但是有个缺点:读写同步,效率低下。于是就出现了 CopyOnWriteArrayList,它通过写时复制数组实现了读写分离,提高了多线程对 List 读的效率,适合多读少些的情况。同理:我们知道 ReentrantLock,它是一把独占的锁,是用来控制线程同步的,如果我们用 ReentrantLock 来实现 ArrayList 安全,能否达到 CopyOnWriteArrayList 同样的效果呢?


import java.util.ArrayList;import java.util.List;import java.util.concurrent.locks.ReentrantReadWriteLock;
/** * @author :jiaolian * @date :Created in 2021-01-26 15:49 * @description:ReentrantReadWriteLock多读少写的场景 * @modified By: * 公众号:叫练 */public class MultReadTest {

private static class MyList { private final ReentrantReadWriteLock REENTRANT_READ_WRITE_LOCK = new ReentrantReadWriteLock(); private final ReentrantReadWriteLock.WriteLock WRITE_LOCK = REENTRANT_READ_WRITE_LOCK.writeLock(); private final ReentrantReadWriteLock.ReadLock READ_LOCK = REENTRANT_READ_WRITE_LOCK.readLock(); private List<String> list = new ArrayList();
//读list public void readList() { try { READ_LOCK.lock(); Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+":"+list.size()); } catch (InterruptedException e) { e.printStackTrace(); } finally { READ_LOCK.unlock(); } }
//写list public void writeList() { try { WRITE_LOCK.lock(); Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+":新增1个元素"); list.add("叫练【公众号】");
} catch (InterruptedException e) { e.printStackTrace(); } finally { WRITE_LOCK.unlock(); } }
}
public static void main(String[] args) { MyList myList = new MyList(); //读写锁适合多读少写情况 //新建10个读线程,1个写线程 new Thread(()->{myList.writeList();},"写线程").start(); for (int i=0; i<10; i++) { new Thread(()->{myList.readList();},"读线程"+(i+1)).start(); } }
}
复制代码

上面案例我们用 ReentrantReadWriteLock 实现了 CopyOnWriteArrayList,主线程新建了 1 个写线程写 list,10 个读线程读 list,程序一共花费 2 执行完毕,如果用 Vector 需要花费 11 秒。在多线程的情况下,通过读写锁操作 List,提高了 List 的读效率,在 List 读的部分,线程是共享的,在对 List 写的过程中,在对写的线程是同步的,因此我们可以得出一个结论:读写锁是读读共享,读写同步


独占获取锁简单流程



image.png


如上图,我们简单的梳理下独占获取锁流程。

  1. 独占锁获取(上述例子中的 WRITE_LOCK 写锁),首先判断是否有线程获取了锁,是否有线程获取了锁的判断通过读写锁中通过 32 位 int 类型 state 可以获取,其中低 16 位表示读锁,高 16 表示写锁。

  2. 有读锁:直接排队阻塞。

  3. 有写锁:还需要判断写锁线程是否是自己,如果是自己就是锁重入了,如果不是自己说明已经有其他的线程获取锁正在执行,那么当前线程需要排队阻塞。

  4. 无锁:直接获取锁,其他抢占的独占锁线程需要排队阻塞,当前线程执行完毕后释放锁通知下一个排队线程获取锁。


共享获取锁简单流程



image.png


如上图,我们简单的梳理下共享锁获取锁流程。

  1. 独占锁获取(上述例子中的 READ_LOCK 读锁),首先判断是否有线程获取了锁。

  2. 有读锁:当前线程发现此时读锁状态被占用,说明有线程获取了读锁。该线程通过 cas 自旋【死循环】获取到读锁为止。

  3. 有写锁:还需要判断持有写锁的线程是否是自己,如果是自己而且此时是获取的是读锁会获取锁成功,我们称为锁降级,如果不是自己说明此时有其他线程获取了写锁,那么当前线程需要排队阻塞。

  4. 无锁:直接获取锁。


写锁降级


我们说读写互斥,但同一个线程中,先写后读也是允许的,我们称之为锁降级。在面试中共享锁面试频率也比较高,方便理解我们举个简单的案例说明下。

import java.util.concurrent.locks.ReentrantReadWriteLock;
/** * @author :jiaolian * @date :Created in 2021-01-28 15:44 * @description:ReentrantReadWriteLock读写锁降级测试 * @modified By: * 公众号:叫练 */public class WriteLockLowerTest {
private static final ReentrantReadWriteLock REENTRANT_READ_WRITE_LOCK = new ReentrantReadWriteLock(); private static final ReentrantReadWriteLock.WriteLock WRITE_LOCK = REENTRANT_READ_WRITE_LOCK.writeLock(); private static final ReentrantReadWriteLock.ReadLock READ_LOCK = REENTRANT_READ_WRITE_LOCK.readLock();
public static void main(String[] args) { try { WRITE_LOCK.lock(); System.out.println("获取写锁"); READ_LOCK.lock(); System.out.println("获取读锁"); } finally { READ_LOCK.unlock(); System.out.println("释放写锁"); WRITE_LOCK.unlock(); System.out.println("释放读锁"); } }}
复制代码

如上述代码:程序可以运行完毕,说明锁可以降级。另外说一句,上面的程序先获取读锁再获取写锁,程序是会阻塞的,为什么呢?欢迎小伙伴在留言区写下评论!


总结


今天我们用通俗易懂的文字描述了 ReentrantReadWriteLock 读写锁。喜欢的请点赞加评论哦!点关注,不迷路,我是叫练【公众号】,边叫边练。期待我们下次再见!


tempimage1611629165941.gif


发布于: 2021 年 01 月 31 日阅读数: 14
用户头像

叫练

关注

我是叫练,边叫边练 2020.06.11 加入

Java高级工程师,熟悉多线程,JVM

评论

发布
暂无评论
ReentrantReadWriteLock读写锁简单原理案例证明