实现一把锁应该如何设计

CAS+自旋,一个volatil修饰的state,一个线程�把state从0改为1表示获得锁成功,其他想获取锁的线程去自旋(自旋一定次数后阻塞)

AQS,CAS+同步队列,一个volatil修饰的state,一个线程�把state从0改为1表示获得锁成功,其他线程获取锁失败,进入同步队列。释放锁只要把state从1改为0

  • 独占锁,tryAcquire
  • 共享锁,tryAcquireShare

实现一把读写锁应该如何设计(读读共享,读写互斥,写写互斥)

  • 读锁:共享锁
  • 写锁:独占锁
  • 实现互斥,判断当前是否存在读锁或者写锁。AQS只提供了一个state
    • state是int类型,有32位,可以用高16位表示读锁,第低16位表示写锁
    • 高16位不为0,有读锁
    • 低16位不为0,有写锁

      读写锁

      JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了一对相关的锁,一个用于只读操作,称为读锁readerLock;一个用于写入操作,称为写锁writerLock。
      读写锁允许在没有写操作的时候,多个线程同时读一个资源没有任何问题,允许多个线程同时读取共享资源(读读可以并发)。
      如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写操作了(读写,写读,写写互斥)
      在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。
      写锁是独占的,读锁是共享的。

线程进入读锁的前提条件

  • 没有其他线程的写锁
  • 没有写请求,或者有写请求,但调用线程和持有锁的线程是同一个(锁降级,持有写锁的情况下可以去获取读锁)


线程进入写锁的前提条件

  • 没有其他线程的读锁
  • 没有其他线程的写锁

读写锁三个重要的特性

  • 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
  • 可重入:读锁和写锁都支持线程重入。以读写线程为例:读线程获取读锁后,能够再次获取读锁。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁
  • 锁降级:遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读锁

应用场景

ReentrantReadWriteLock适合读多写少的场景

  1. static Map<String, Object> map = new HashMap<String, Object>();
  2. static {
  3. map.put("1", "111111");
  4. }
  5. static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  6. static Lock readLock = readWriteLock.readLock();
  7. static Lock writeLock = readWriteLock.writeLock();
  8. public static Object get(String key) {
  9. System.out.println(Thread.currentThread().getName() + " ,准备获取读锁");
  10. readLock.lock();
  11. try {
  12. System.out.println(Thread.currentThread().getName() + " ,获取读锁成功");
  13. Thread.sleep(5000);
  14. return map.get(key);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. } finally {
  18. readLock.unlock();
  19. System.out.println(Thread.currentThread().getName() + " ,释放读锁");
  20. }
  21. return null;
  22. }
  23. public static Object put(String key, Object object) {
  24. System.out.println(Thread.currentThread().getName() + " ,准备获取写锁");
  25. writeLock.lock();
  26. try {
  27. System.out.println(Thread.currentThread().getName() + " ,获取写锁成功");
  28. Thread.sleep(3000);
  29. return map.put(key, object);
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. } finally {
  33. writeLock.unlock();
  34. System.out.println(Thread.currentThread().getName() + " ,释放写锁");
  35. }
  36. return null;
  37. }

读读场景

  1. new Thread(new Runnable() {
  2. @Override
  3. public void run() {
  4. get("1");
  5. }
  6. }, "thread0").start();
  7. new Thread(new Runnable() {
  8. @Override
  9. public void run() {
  10. get("1");
  11. }
  12. }, "thread1").start();

可以看到两个读锁之间没有冲突,可以共享
截屏2022-03-27 17.44.40.png

读写场景/写读场景

  1. new Thread(new Runnable() {
  2. @Override
  3. public void run() {
  4. get("1");
  5. }
  6. }, "thread0").start();
  7. new Thread(new Runnable() {
  8. @Override
  9. public void run() {
  10. put("2", "22222");
  11. }
  12. }, "thread1").start();

读写互斥了,获得到读锁的,然后有其他线程想要写入,需要等到读锁释放。
写读基本和读写一样,有写锁的同时,不允许其他线程读取
截屏2022-03-27 17.48.08.png

写写场景

  1. new Thread(new Runnable() {
  2. @Override
  3. public void run() {
  4. put("2", "22222");
  5. }
  6. }, "thread0").start();
  7. new Thread(new Runnable() {
  8. @Override
  9. public void run() {
  10. put("3", "333333");
  11. }
  12. }, "thread1").start();

写写也是互斥的,当获得了写锁,其他的写入也是要等待写锁的释放
截屏2022-03-27 17.55.57.png

HashMap本来是线程不安全的,但是可以通过读写锁的读和写来实现线程安全(上面例子)。在读操作get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。写操作put(String key,Object value)方法,在更新 HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而 只有写锁被释放之后,其他读写操作才能继续。

锁降级

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。锁降级可以帮助我们拿到当前线程修改后的结果而不被其他线程所破坏,防止更新丢失。

  1. static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  2. static Lock readLock = readWriteLock.readLock();
  3. static Lock writeLock = readWriteLock.writeLock();
  4. private static volatile boolean update = false;
  5. public static void processData() {
  6. readLock.lock();
  7. System.out.println(Thread.currentThread().getName() + " ,获取读锁");
  8. if (!update) {
  9. // 必须先释放读锁
  10. readLock.unlock();
  11. System.out.println(Thread.currentThread().getName() + " ,释放读锁");
  12. // 锁降级从写锁获取到开始
  13. writeLock.lock();
  14. System.out.println(Thread.currentThread().getName() + " ,获取写锁");
  15. try {
  16. if (!update) {
  17. // TODO 准备数据的流程(略)
  18. update = true;
  19. }
  20. readLock.lock();
  21. System.out.println(Thread.currentThread().getName() + " ,获取读锁");
  22. } finally {
  23. writeLock.unlock();
  24. System.out.println(Thread.currentThread().getName() + " ,释放写锁");
  25. }
  26. // 锁降级完成,写锁降级为读锁
  27. }
  28. try {
  29. //TODO 使用数据的流程(略)
  30. } finally {
  31. System.out.println(Thread.currentThread().getName() + " ,释放读锁");
  32. readLock.unlock();
  33. }
  34. }

截屏2022-03-27 18.17.43.png
锁降级主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新

RentrantReadWriteLock不支持锁升级,目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的