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:

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();
        }
    }
}

  

总结:

ReentrantReadWriteLock底层使用AQS实现,巧妙地使用AQS的状态值的高16位表示获取到读锁的个数,低16位表示获取写锁的线程的可重入次数,并通过CAS对其进行操作实现读写分离,这在读多写少的场景下比较适用。

posted @ 2022-06-14 16:37  a快乐码农  阅读(22)  评论(0编辑  收藏  举报