JUC Lock 读写锁

2024-01-09 10:56:42

ReentrantReadWriteLock1.5+ 读写锁

对共享数据的查看、查询就是读,对共享数据的修改就是写,读时不会涉及共享数据的修改,不修改意味着多个线程读取的数据就不会变化,那么在此情况下多个线程在用锁来进行排它读取操作就会影响效率。反过来说,如果此共享数据需要修改(写),那么开始写到结束写这段时间就不能读。

以上总结起来就是,多个线程可以同时读,但一旦有某一个线程开始写,那么其余线程就不能读也不能写。再总结简单点:读读线程可以同时进行,读写不能同时进行,写写也不能同时进行。

ReentrantReadWriteLock 继承关系图

在这里插入图片描述

  • ReentrantReadWriteLock 并不是直接实现 Lock 接口,而是实现 ReadWriteLock 接口
public interface ReadWriteLock {
    /**
     * 返回读锁,就是 ReadLock 对象
     *
     */
    Lock readLock();

    /**
     * 返回写锁,就是 WriteLock 对象
     */
    Lock writeLock();
}
  • ReentrantReadWriteLock 内部定义了 5 个内部类,Sync(以及对应的两个实现类:FairSync、NonfairSync)、WriteLock、ReadLock
    • Sync 和 ReentrantLock 一样,继承自 AQS。
    • WriteLock、ReadLock:都实现了 Lock 接口,他们的实例都包含一个 Sync 对象,该 Sync 对象和ReentrantReadWriteLock 实例化时的 Sync 对象是一致的。

    public ReentrantReadWriteLock() {
        this(false);
    }

    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);// 传入当前的 ReentrantReadWriteLock 对象
        writerLock = new WriteLock(this);// 传入当前的 ReentrantReadWriteLock 对象
    }
protected ReadLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;
}
protected WriteLock(ReentrantReadWriteLock lock) {
     sync = lock.sync;
 }

示例 1

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

@Slf4j
public class ReentrantReadWriteLockTest1 {


    private  static  int x = 0;

    public static void main(String[] args) {
        ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

        for(int i = 0; i < 10; i++){
            new Thread(()->{
                rwLock.readLock().lock();
                try {
                    log.debug("开始读取数据");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug("{}", x);
                    log.debug("数据读取完毕");
                }finally {
                    rwLock.readLock().unlock();
                }
            },"read-" + i).start();
        }

        for(int i = 0; i < 10; i++){
            new Thread(()->{
                rwLock.writeLock().lock();
                try {
                    log.debug("开始写数据");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    x++;
                    log.debug("数据写入完毕");
                }finally {
                    rwLock.writeLock().unlock();
                }
            },"write-" + i).start();
        }

    }

}

结果:

16:19:12.658 [read-4] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-6] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-2] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-0] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-7] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-3] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-1] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:13.663 [read-1] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.665 [read-1] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.663 [read-6] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.663 [read-2] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.665 [read-2] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.665 [read-6] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.663 [read-7] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.663 [read-0] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.666 [read-7] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.663 [read-3] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.666 [read-3] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.663 [read-4] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.666 [read-4] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.666 [read-0] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.666 [write-9] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:14.672 [write-9] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:14.672 [write-0] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:15.677 [write-0] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:15.677 [write-1] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:16.682 [write-1] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:16.682 [write-4] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:17.691 [write-4] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:17.691 [write-5] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:18.697 [write-5] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:18.697 [write-8] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:19.705 [write-8] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:19.705 [read-5] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:19.705 [read-9] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:19.705 [read-8] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:20.713 [read-5] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 6
16:19:20.713 [read-9] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 6
16:19:20.713 [read-8] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 6
16:19:20.713 [read-5] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:20.713 [read-9] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:20.714 [read-8] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:20.714 [write-2] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:21.725 [write-2] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:21.725 [write-3] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:22.729 [write-3] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:22.729 [write-6] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:23.734 [write-6] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:23.735 [write-7] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:24.744 [write-7] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕

从结果可以看出,不同的读线程可以同时进行(结果顺序不是以“1.开始读取数据;2. x的值;3. 数据读取完毕”这样的顺序完成),但读写、写写线程是互斥的(打印结果顺序一定是“1. 开始写数据;2. 数据写入完毕”)。

示例 2

示例 1 是读锁和写锁在不同的线程中,如果读锁和写锁都在同一个线程中,有什么要求呢?

在同一个线程中,同时使用读锁和写锁的情况有如下几种:
获取读锁 -> 释放读锁 -> 获取写锁 -> 释放写锁
获取写锁 -> 释放写锁 -> 获取读锁 -> 释放读锁
以上两种方式不存在嵌套,不会出现问题

获取读锁 -> 获取写锁 -> 释放读锁 -> 释放写锁 》》》先获取读锁再获取写锁
获取写锁 -> 获取读锁 -> 释放写锁 -> 释放读锁 》》》先获取写锁再获取读锁
其他 。。。。。。

先获取读锁再获取写锁

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantReadWriteLock;

@Slf4j
public class ReentrantReadWriteLockTest2 {

    public static void main(String[] args) {
        ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

        rwLock.readLock().lock();
        log.debug("获取读锁成功");

        rwLock.writeLock().lock();
        log.debug("获取写锁成功");

        rwLock.readLock().unlock();
        log.debug("释放读锁成功");
        
        rwLock.writeLock().unlock();
        log.debug("释放写锁成功");

    }

}

结果:在获取写锁的地方阻塞了。因为写锁在开始写的时候,是不能读取的,与读锁互斥,这样的写法将导致写锁阻塞。

先获取写锁再获取读锁

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.ReentrantReadWriteLock;

@Slf4j
public class ReentrantReadWriteLockTest2 {

    public static void main(String[] args) {
        ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

        rwLock.writeLock().lock();
        log.debug("获取写锁成功");

        rwLock.readLock().lock();
        log.debug("获取读锁成功");

        rwLock.readLock().unlock();
        log.debug("释放读锁成功");

		// 这里不论是线解锁写还是先解锁读,都可以正常执行结束
        rwLock.writeLock().unlock();
        log.debug("释放写锁成功");

    }

}

先获取写锁,再嵌套获取读锁,可以正常执行完毕。按照我们前面的说法,只要有写操作,就会互斥,但是这里怎么能执行完成呢?

锁降级

导致上面示例可以正常执行完成的机制,称作锁降级。就是写锁降级为了读锁。

  • 因为我们在线程内先获取的是写锁,获取成功后,其他线程的读和写锁都互斥,进行阻塞等待该线程是释放锁。
  • 当前线程再次获取读锁,因为其他线程不会获取到锁,所以,这里再次获取读锁是可以的(此时的读锁和写锁都是同一个线程,至于什么时候修改数据,什么时候对写操作对读操作可见,就是在该线程中自己定义了,这变成了一个单线程的问题,跟多线程无关)
  • 最后释放锁的时候,如果写锁先释放,那么其他线程的读锁就可以访问了,当前线程读写锁都释放后,其他线程的写锁就可以尝试获取锁了。

总结

  • ReentrantReadWriteLock 适合线程读多写少的情况(缓存)
  • ReentrantReadWriteLock 容易导致写线程的饥饿情况(因为写线程少,在极端情况下可能一直争抢不到 CPU 资源)

StampedLock1.8+ (邮戳锁)

基本使用

    public static void main(String[] args) {
        // 只有一个无参构造
        StampedLock sl = new StampedLock();
        // 获取读锁
        long stamp = sl.readLock();
        try{
            log.debug("获取读锁,{}",stamp);
        }finally {
            // 释放读锁
            sl.unlockRead(stamp);
        }
        
        // 获取写锁
        stamp = sl.writeLock();
        try {
            log.debug("获取写锁,{}",stamp);
        }finally {
            // 释放写锁
            sl.unlockWrite(stamp);
        }
    }

示例1

    public static void main(String[] args) {

        // 只有一个无参构造
        StampedLock sl = new StampedLock();

        long stamp = sl.readLock();
        log.debug("第一次获取读锁,{}",stamp);

        stamp = sl.readLock();
        log.debug("第二次次获取读锁,{}",stamp);

    }

结果:

21:39:57.127 [main] DEBUG com.yyoo.thread.lock.StampedLockTest1 - 第一次获取读锁,257
21:39:57.132 [main] DEBUG com.yyoo.thread.lock.StampedLockTest1 - 第二次次获取读锁,258

此处两次的 stamp 值是不一样的。

示例2

    public static void main(String[] args) {

        // 只有一个无参构造
        StampedLock sl = new StampedLock();

        long stamp = sl.readLock();
        log.debug("第一次获取读锁,{}",stamp);
        sl.unlockRead(stamp);

        stamp = sl.readLock();
        log.debug("第二次次获取读锁,{}",stamp);
        sl.unlockRead(stamp);

    }

示例2 ,两次打印 stamp 的值是一样的。说明我们每次 readLock() 上锁时,都会打个戳(邮戳),在解锁时使用相同的戳才能正常解锁。如果解锁时的 stamp 与上锁时不一样,则会抛出 IllegalMonitorStateException 异常。

示例3

    public static void main(String[] args) {

        // 只有一个无参构造
        StampedLock sl = new StampedLock();

        long stamp = sl.readLock();
        log.debug("第一次获取读锁,{}",stamp);

        stamp = sl.writeLock();
        log.debug("第二次次获取写锁,{}",stamp);

    }

示例3 在获取读锁后,再次获取写锁就阻塞了(这里无论是先获取写锁还是读锁,都会阻塞),其表现和 ReentrantReadWriteLock 的读写锁一致,读读可重复获取(只是戳不一样),读写、写读、写写都互斥。如果和 ReentrantReadWriteLock 一样,那么 JDK1.8 就没必要再提供 StampedLock 了

StampedLock 提供了如下两种方式来实现乐观锁机制

  • tryOptimisticRead 尝试获取读戳,通常和 validate 方法一起使用,validate 方法来验证是否同时存在写,如果存在就可以通过 readLock 升级为读锁的方式来互斥
  • tryConvertToWriteLock 尝试转换为写戳,其返回 0 表示获取失败,在此之前需要先获取读锁或读戳。只要不是 0 就可以进行写操作,表示没有并发的写操作,如果返回0获取写戳失败,则进行锁升级,将当前的读锁升级为写锁来互斥
  • StampedLock 就是对 ReentrantReadWriteLock 的优化,就是通过以上两个方法及其相关方法来实现乐观锁方式的写操作,以提高效率,减少写线程饥饿问题。

这里的描述不是特别清楚,请结合下面的示例来理解

StampedLock 源码文档上的示例

根据源码文档上的示例进行修改的示例

import lombok.extern.slf4j.Slf4j;

import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;

@Slf4j
public class StampedLockTest3 {

    public static void main(String[] args) throws InterruptedException {
        Point p = new Point();
        Random r = new Random();

        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                try {
                    TimeUnit.MILLISECONDS.sleep(r.nextInt(5000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("{}",p.distanceFromOrigin());
            },"read-"+i).start();
        }


        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                try {
                    TimeUnit.MILLISECONDS.sleep(r.nextInt(5000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                p.moveIfAtOrigin(r.nextInt(100),r.nextInt(100));
            },"write-"+i).start();
        }

        TimeUnit.SECONDS.sleep(10);
    }


    static class Point{
        // x和y轴
        private double x, y;
        private final StampedLock sl = new StampedLock();

        // writeLock 方式(会阻塞)
        void move(double deltaX, double deltaY) { // an exclusively locked method
            long stamp = sl.writeLock();
            try {
                x += deltaX;
                y += deltaY;
            } finally {
                sl.unlockWrite(stamp);
            }
        }

        // 乐观锁读
        double distanceFromOrigin() { // A read-only method
            // 尝试获取读戳
            long stamp = sl.tryOptimisticRead();
            double currentX = x, currentY = y;
            // 用 validate 方法检查当前戳是否可直接使用(没有写)
            if (!sl.validate(stamp)) {
                // 直接获取读锁
                stamp = sl.readLock();
                try {
                    currentX = x;
                    currentY = y;
                } finally {
                    sl.unlockRead(stamp);
                }
            }

            // 如果当前没有写,那么就可以直接计算,否则就得重新独占式的重新读取(if 里面的逻辑)
            return currentX * currentX + currentY * currentY;
        }

        // 乐观锁进行写
        void moveIfAtOrigin(double newX, double newY) { // upgrade
            // Could instead start with optimistic, not read mode
            // 先获取读锁(示例上提示可以使用 tryOptimisticRead 来获取)
            long stamp = sl.readLock();
            try {
                // 尝试获取写戳
                // tryConvertToWriteLock 方法如果返回 0,则表示获取写戳失败
                long ws = sl.tryConvertToWriteLock(stamp);
                if (ws != 0L) {// ws != 0 表示写戳获取成功
                    // 设置 stamp 为写戳
                    stamp = ws;
                    // 修改数据
                    x = newX;
                    y = newY;
                    log.debug("使用写戳修改数据");
                }
                else {
                    // 获取写戳失败,则释放读锁
                    sl.unlockRead(stamp);
                    // 并切换为写锁(也可以说升级为写锁)
                    stamp = sl.writeLock();
                    // 修改数据
                    x = newX;
                    y = newY;
                    log.debug("升级为写锁修改数据");
                }
            } finally {
                // 释放锁
                sl.unlock(stamp);
            }
        }
    }

}

执行后会发现大多数都是使用的写戳(如果将写操作的线程的sleep 去掉,会有写戳和写锁两种方式出现)

文章来源:https://blog.csdn.net/forlinkext/article/details/135279051
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。