JUC - 为了线程安全 - 读写锁

1. 共享锁和排它锁

1.1 排它锁

排它锁又称独占锁,获得了以后既能读又能写,其他没有获得锁的线程不能读也不能写,典型的synchronized就是排它锁

1.2 共享锁

共享锁又称读锁,获得了共享锁以后可以查看但无法修改和删除数据,其他线程也能获得共享锁,也可以查看但不能修改和删除数据

在没有读写锁之前,我们虽然保证了线程安全,但是也浪费了一定的资源,因为多个读操作同时进行并没有线程安全问题

ReentrantReadWriteLock中 读锁就是共享锁,写锁是排它锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果不这样,读是无限阻塞的,这样提高了程序的执行效率

1.3 读写锁的规则

  • 多个线程只申请读锁,都能申请到
  • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放该锁
  • 如果有一个线程已经占用 写锁,则其他线程申请写锁或读锁都要等待它释放
  • 也就是说,要么多读要么一写

下面的示例让两个线程去读,两个线程去写,使用读写锁读的线程是同时进行的,而写的线程等读的线程执行完再依次执行

public class RWLock {
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    //读锁
    private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    //写锁
    private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

    private static void read()  {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"得到了读锁,正在读取");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName()+"释放了读锁");
            readLock.unlock();
        }
    }

    private static void write()  {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"得到了写锁,正在写入");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName()+"释放了写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                read();
            }
        },"t1").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                read();
            }
        },"t2").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                write();
            }
        },"t3").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                write();
            }
        },"t4").start();
    }

}
/*    t1得到了读锁,正在读取
      t2得到了读锁,正在读取
      t1释放了读锁
      t2释放了读锁
      t3得到了写锁,正在写入
      t3释放了写锁
      t4得到了写锁,正在写入
      t4释放了写锁*/

1.4 读写锁我们需要思考的几个问题

① 选择规则: 假设我们现在一个线程持有读锁,后面有写锁在等待,这个时候又来了一个读线程,那这个读线程能不能插队,不管前面的排队的写线程和当前的线程一起读?
插队虽然可能提高了整体的效率,但是对写锁来说是不公平的

② 升降级: 能不能让一个持有写锁的线程直接降级获得读锁呢?或者反过来让一个持有读锁的线程直接升级获得写锁呢?

下面我们来看看ReentrantReadWriteLock是怎么实现的

2. ReentrantReadWriteLock

  • 在插队方面不允许读锁插队
  • 在升降级的方面,允许降级不允许升级
2.1 插队策略

如果我们设置为公平的,那么自然不会允许插队

private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);

但是对于默认不公平的呢
假设现在有线程2和线程4同时读取,线程3想要写入,拿不到锁,于是进入等待队列,线程5 不在队列里,现在过来想要读取,这个时候有两种选择
① 让5插队
在这里插入图片描述
这样读取插队效率高,但是对于写操作的线程3会造成饥饿

② 不允许插队
在这里插入图片描述
这样可以避免饥饿

ReentrantReadWriteLock
ReentrantReadWriteLock大体选用了策略2

  • 写锁可以随时插队
  • 读锁仅在等待队列的头节点不是想获取写锁的线程的时候可以插队

可以看一下源码
对于公平锁,看里面有两个方法,writerShouldBlockreaderShouldBlock就是用来判断当前进来的读写线程是否需要排队,可以看到他们的实现都是调用hasQueuedPredecessors方法看看前面有没有人在等待
在这里插入图片描述
对于非公平的情况,可以看到writerShouldBlock方法返回的是false,也就是写者永远不需要排队,而对于读者调用apparentlyFirstQueuedIsExclusive方法,这个方法看名字就能看出来就是判断排队队列的第一个是不是读锁,如果是,那么返回true,要排队,如果不是,则可以插队
在这里插入图片描述

/**
 * 描述:     演示非公平和公平的ReentrantReadWriteLock的策略 
 *            主要难模拟的是读线程的插队情况
 */
public class NonfairBargeDemo {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
            true);

    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write() {
        System.out.println(Thread.currentThread().getName() + "开始尝试获取写锁");
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            try {
                Thread.sleep(40);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(()->write(),"Thread1").start();
        new Thread(()->read(),"Thread2").start();
        new Thread(()->read(),"Thread3").start();
        new Thread(()->write(),"Thread4").start();
        new Thread(()->read(),"Thread5").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Thread thread[] = new Thread[1000];
                for (int i = 0; i < 1000; i++) {
                    thread[i] = new Thread(() -> read(), "子线程创建的Thread" + i);
                }
                for (int i = 0; i < 1000; i++) {
                    thread[i].start();
                }
            }
        }).start();
    }
}

2.2 升降级

假设我现在有一个线程,线程的前半部分是对日志的写操作,而线程的后半部分是对一个资源的读操作,当然我一开始要获取写锁,但是当我写操作执行完了我还持有写锁不就是对资源的一种浪费么?(别的读线程不能读),对于这一点ReentrantReadWriteLock对写锁可以降级为读锁,但是读锁不能升级为写锁

注意锁降级指的是写锁降级成为读锁,如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这个过程是不能被称为锁降级的。锁降级是指把当前拥有的写锁,再获取到读锁,随后释放写锁的过程

class CachedData {
  Object data;
  volatile boolean cacheValid;
  final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

  void processCachedData() {
    // 获取读锁
    rwl.readLock().lock();
    if (!cacheValid) {
      // 在获取写锁之前必须释放读锁,不释放的话下面写锁会获取不成功,造成死锁
      rwl.readLock().unlock();
     // 获取写锁
      rwl.writeLock().lock();
      try {
        // 重新检查state,因为在获取写锁之前其他线程可能已经获取写锁并且更改了state
        if (!cacheValid) {
          data = ...
          cacheValid = true;
        }
        // 通过在释放写锁定之前获取读锁定来降级
        // 这里再次获取读锁,如果不获取,那么当写锁释放后可能其他写线程再次获得写锁,导致下方`use(data)`时出现不一致的现象
        // 这个操作就是降级
        rwl.readLock().lock();
      } finally {
        rwl.writeLock().unlock(); // 释放写锁,由于在释放之前读锁已经被获取,所以现在是读锁获取状态
      }
    }

    try {
    // 使用完后释放读锁
      use(data);
    } finally {
      rwl.readLock().unlock(); //释放读锁
    }
  }
 }}

为什么不能升级呢?
升级可能会造成死锁,因为获得写锁的前提是当前既没有读的也没有写的,假设现在有两个读线程A和B,AB都想升级为写锁,那必须要等待对方释放读锁,这个时候就会造成死锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值