详解ReentrantLock/ReentrantReadWriteLock/BlockingQueue原理
解决线程安全问题使用ReentrantLock就可以,但是ReentrantLock是独占锁,某时只有一个线程可以获取该锁,而实际中会有写少读多的场景,显然ReentrantLock满足不了这个需求,所以ReentrantReadWriteLock应运而生。
ReentrantReadWriteLock采用读写分离的策略,允许多个线程可以同时获取读锁。
读写锁内部维护了两个锁,一个用于读操作,一个用于写操作。所有ReadWriteLock实现都必须保证writeLock操作的内存同步效果也要保持与相关readLock的联系。也就是说,成功获取读锁的线程会看到写入锁之前版本所做的所有更新。
ReentrantReadWriteLock支持以下功能:
- 支持公平和非公平的获取锁的方式;
- 支持可重入,读线程在获取了读锁后还可以获取读锁;写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁;
- 还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读锁,最后释放写锁。但是,从读取锁升级为写入锁是不允许的;
- 读取锁和写入锁都支持锁获取期间的中断;
- Condition支持,仅写入锁提供了Condition实现,读取锁不支持Condition,readLock().newCondition()会抛出异常。
读写锁内部维护了一个ReadLock和一个WriteLock,它们依赖Sync实现具体功能。而Sync继承自AQS,并且也提供了公平和非公平的实现。
我们知道AQS中只维护了一个state状态,而ReentrantReadWriteLock则需要维护读状态和写状态,一个state怎么表示读和写两种状态呢?ReentrantReadWriteLock巧妙地使用state的高16位表示读状态,也就是获取到的读锁的次数;使用低16位表示获取到写锁的线程的可重入次数。
写锁的获取与释放:
1、void lock()方法
写锁是个独占锁,某时只有一个线程可以获取该锁。如果当前没有线程获取到读锁和写锁,则当前线程可以获取到写锁然后返回。如果当前已经有线程获取到读锁和写锁,则当前请求写锁的线程会被阻塞挂起。另外,写锁是可重入锁,如果当前线程已经获取到了该锁,再次获取只是简单地把可重入次数加1后直接返回。
2、void lockInterruptibly()方法
类似于lock方法,它的不同之处在于,它会对中断进行响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,当前线程会抛出异常InterruptedException异常。
3、boolean tryLock()方法
尝试获取写锁,如果当前没有其他线程持有写锁或者读锁,则当前线程获取写锁会成功,然后返回true。如果当前已经有其他线程持有写锁或者读锁则该方法直接返回false,且当前线程并不会阻塞。如果当前线程已经持有了该写锁则简单增加AQS的状态值后直接返回true。
4、boolean tryLock(long timeout, TimeUnit unit)
与tryAcquire()的不同之处在于,多了超时时间参数,如果尝试获取写锁失败则会把当前线程挂起指定时间,待超时时间到后当前线程被激活,如果还是没有获取到写锁则返回false。另外,该方法会对中断进行响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,当前线程会抛出InterruptedException异常。
5、void unlock()
尝试释放锁,如果当前线程持有该锁,调用方法会让该线程对该线程持有的AQS状态值减1,如果减去1后当前状态值位0则当前线程会释放该锁,否则仅仅减1而已。如果当前线程没有持有该锁而调用了该方法则会抛出IllegalMonitorStateException异常。
读锁的获取与释放:
1、void lock()
获取读锁,如果当前没有其他线程持有写锁,则当前线程可以获取读锁,AQS的状态值state的高16位的值会增加1,然后方法返回。否则如果其他一个线程持有写锁,则当前线程会被阻塞。
2、void lockInterruptibly()
类似lock方法,不同之处在于,该方法会对中断进行响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,当前线程会抛出InterruptedException异常。
3、boolean tryLock()
尝试获取读锁,如果当前没有其他线程持有写锁,则当前线程获取读锁会成功,然后返回true。如果当前已经有其他线程持有写锁则该方法直接返回false,且当前线程并不会阻塞。如果当前线程已经持有了该读锁则简单增加AQS的状态值高16位后直接返回true。
4、boolean tryLock(long timeout, TimeUnit unit)
与tryLock()的不同之处在于,多了超时时间参数,如果尝试获取读锁失败则会把当前线程挂起指定时间,待超时时间到后当前线程被激活,如果还是没有获取到读锁则返回false。另外,该方法会对中断进行响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,当前线程会抛出InterruptedException异常。
应用:
使用ReentrantReadWriteLock实现线程安全的list:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | public class ReentrantLockList { // 线程不安全的list private ArrayList<String> array = new ArrayList<String>(); // 独占锁 private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock readLock = lock.readLock(); private final Lock writeLock = lock.writeLock(); // 添加元素 public void add(String e) { // 加锁 System.out.println( "add方法 加锁" ); writeLock.lock(); try { // 添加元素 array.add(e); } finally { // 最后释放锁 System.out.println( "add方法 释放锁" ); writeLock.unlock(); } } // 删除元素 public void remove(String e) { writeLock.lock(); try { array.remove(e); } finally { writeLock.unlock(); } } // 获取数据 public String get( int index) { // 加读锁 System.out.println( "get方法 加锁" ); readLock.lock(); try { return array.get(index); } finally { System.out.println( "get方法 释放锁" ); readLock.unlock(); } } public static void main(String[] args) throws InterruptedException { Thread[] ts = new Thread[ 10 ]; Thread[] td = new Thread[ 30 ]; ReentrantLockList reentrantLockList = new ReentrantLockList(); // 创建10个线程添加元素 for ( int i= 0 ;i< 10 ;i++) { ts[i] = new Thread(() ->{ reentrantLockList.add( "el" ); }); ts[i].start(); } for ( int i= 0 ;i< 10 ;i++) { ts[i].join(); } // 读取元素 for ( int j= 0 ;j< 30 ;j++) { td[j] = new Thread(() -> { System.out.println(reentrantLockList.get( 6 )); }); td[j].start(); } for ( int j= 0 ;j< 30 ;j++) { td[j].join(); } } } |
BlockingQueue即阻塞队列
它算是一种将ReentrantLock用得非常精彩的一种表现,依据它的基本原理,我们可以实现Web中的长连接聊天功能,当然其最常用的还是用于实现生产者与消费者模式,大致如下图所示:
在Java中,BlockingQueue是一个接口,它的实现类有ArrayBlockingQueue、DelayQueue、 LinkedBlockingDeque、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等,它们的区别主要体现在存储结构上或对元素操作上的不同,但是对于take与put操作的原理,却是类似的。下面的源码以ArrayBlockingQueue为例。
2. 分析
BlockingQueue内部有一个ReentrantLock,其生成了两个Condition,在ArrayBlockingQueue的属性声明中可以看见:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /** Main lock guarding all access */ final ReentrantLock lock; /** Condition for waiting takes */ private final Condition notEmpty; /** Condition for waiting puts */ private final Condition notFull; ... public ArrayBlockingQueue( int capacity, boolean fair) { if (capacity <= 0 ) throw new IllegalArgumentException(); this .items = new Object[capacity]; lock = new ReentrantLock(fair); notEmpty = lock.newCondition(); notFull = lock.newCondition(); } |
而如果能把notEmpty、notFull、put线程、take线程拟人的话,那么我想put与take操作可能会是下面这种流程:
put(e)
take()
其中ArrayBlockingQueue.put(E e)源码如下(其中中文注释为自定义注释,下同):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | /** * Inserts the specified element at the tail of this queue, waiting * for space to become available if the queue is full. * * @throws InterruptedException {@inheritDoc} * @throws NullPointerException {@inheritDoc} */ public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this .lock; lock.lockInterruptibly(); try { while (count == items.length) notFull.await(); // 如果队列已满,则等待 insert(e); } finally { lock.unlock(); } } /** * Inserts element at current put position, advances, and signals. * Call only when holding lock. */ private void insert(E x) { items[putIndex] = x; putIndex = inc(putIndex); ++count; notEmpty.signal(); // 有新的元素被插入,通知等待中的取走元素线程 } ArrayBlockingQueue.take()源码如下: public E take() throws InterruptedException { final ReentrantLock lock = this .lock; lock.lockInterruptibly(); try { while (count == 0 ) notEmpty.await(); // 如果队列为空,则等待 return extract(); } finally { lock.unlock(); } } /** * Extracts element at current take position, advances, and signals. * Call only when holding lock. */ private E extract() { final Object[] items = this .items; E x = this .<E>cast(items[takeIndex]); items[takeIndex] = null ; takeIndex = inc(takeIndex); --count; notFull.signal(); // 有新的元素被取走,通知等待中的插入元素线程 return x; } |
可以看见,put(E)与take()是同步的,在put操作中,当队列满了,会阻塞put操作,直到队列中有空闲的位置。而在take操作中,当队列为空时,会阻塞take操作,直到队列中有新的元素。
而这里使用两个Condition,则可以避免调用signal()时,会唤醒相同的put或take操作。
总结:
ReentrantReadWriteLock底层使用AQS实现,巧妙地使用AQS的状态值的高16位表示获取到读锁的个数,低16位表示获取写锁的线程的可重入次数,并通过CAS对其进行操作实现读写分离,这在读多写少的场景下比较适用。
------------恢复内容结束------------
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)