Java锁原理剖析

1.AQS——锁的底层支持

AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的。
在这里插入图片描述

  如图所示,AQS是一个FIFO的双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node。其中Node的thread变量用来存放进入AQS队列里面的线程。
  AQS维持了一个单一的状态信息state,可以通过getState、setState、compareAndSetState函数修改其值。对于ReentrantLock的实现来说,state可以用来表示当前线程获取锁的可重入次数;对于读写锁ReentrantReadWriteLock来说,state的高16位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的可重入次数;对于semaphore来说,state用来表示当前可用信号的个数;对于CountDownlatch来说,state用来表示计数器当前的值。
  AQS有个内部类ConditionObject,用来结合锁实现线程同步。ConditionObject可以直接访问AQS对象内部的变量,比如state状态值和AQS队列。ConditionObject是条件变量,每个条件变量对应一个条件队列(单向链表队列),用来存放调用条件变量的await()方法后被阻塞的线程,这个条件队列的头、尾元素分别为firstWaiter和lastWaiter。
  对于AQS来说,线程同步的关键是对状态值state进行操作。根据state是否属于一个线程,操作state的方式分为独占方式和共享方式。在独占方式下获取和释放资源使用的方法为void acquire(int arg) void acquireInterruptibly(int arg) boolean release(int arg).
在共享方式下获取和释放资源的方法为void acquireShared(int arg) void acquireSharedInterruptibly(int arg) boolean releaseShared(int arg)
AQS——条件变量的支持
在这里插入图片描述如图所示,一个锁对应一个AQS阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。
当多个线程同时调用lock.lock()获取锁时,只有一个线程获取到了锁,其他线程会被转换为Node节点插入到lock锁对应的AQS阻塞队列里面,并做自旋CAS尝试获取锁。
如果获取到锁的线程又调用了条件变量的await()方法,则该线程会释放获取到的锁,并被转换为Node节点插入到条件变量对应的条件队列里面。
这时候因为调用lock.lock()方法被阻塞到AQS队列里面的一个线程会获取到被释放的锁,如果该线程也调用了条件变量的await()方法则该线程也会被放入条件变量的条件队列里面。
当另外一个线程调用条件变量的signal()或者signalAll()方法时,会把条件队列里面的一个或者全部Node节点移动到AQS的阻塞队列里面,等待时机获取锁。

2.独占锁 ReentrantLock的原理

ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞而被放入该锁的AQS阻塞队列里面。
在这里插入图片描述
从类图可以看出,ReentrantLock的内部类Sync,还是由AQS来实现的,并且根据参数来决定其内部是一个公平还是非公平锁,默认是非公平锁。在这里AQS的状态值为0表示当前锁空闲,为大于等于1则说明该锁已经被占用。由于该锁是独占锁,所以某时只有一个线程可以获取该锁。

3.读写锁ReentrantReadWriteLock的原理

读写锁的内部维护了一个ReadLock和一个WriteLock,它们依赖Sync实现具体功能,而Sync继承自AQS,并且也提供了公平和非公平的实现。我们知道AQS只维护了一个state状态,而ReentrantReadWriteLock则需要维护读状态和写状态,它巧妙地使用state的高16位表示读状态,也就是获取到读锁的次数;使用低16位表示获取到写锁的线程的可重入次数,并通过CAS对其进行操作实现了读写分离,这在读多写少的场景下比较适用。

线程进入写锁的前提条件:

没有其他线程持有写锁

没有其他线程持有读锁

由于写锁是可重入锁 如果当前线程已经获取了该锁,再次获取只是简单地把可重入次数加1后直接返回

如果当前线程持有了读锁,不能获取写锁,因为读写锁不支持锁升级,具体原因看下方

线程进入读锁的前提条件

没有其他线程持有写锁

如果当前线程已经持有了写锁,可以获取读锁。但处理完后要把两个锁都释放掉。

其他线程持有读锁或没有锁,当前线程都可以获取读锁,读锁不是排它锁

读锁也是可重入锁,当前线程持有读锁后,还可以再次获取

相关问题

1.Java的读写锁中为什么读锁不能升级为写锁?

这是因为当线程申请读锁时,多线程是可以同时持有读锁的。假设持有读锁的线程都想升级为写锁,由于写锁是排它锁,那么线程需要等待其他持有读锁的线程释放读锁后,才能获取到写锁。这就容易发生死锁的情况,谁都不愿意率先放掉自己手中的锁。
但是读写锁的升级并不是不可能的,也有可以实现的方案,如果我们保证每次只有一个线程可以升级,那么就可以保证线程安全。只不过最常见的 ReentrantReadWriteLock 对此并不支持,我们可以自定义去实现一下。

2.什么是锁降级,锁降级的使用场景

锁降级就是指持有了写锁的线程,由于它一定独占了读写锁,因此可以让它继续获取读锁,当它同时持有写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。
锁降级的用途是为了保证数据修改的可见性,在释放写锁的时候,如果另一个线程拿到了写锁,然后再覆盖了当前线程修改的值,这样当前线程修改的值就不可见了,加这个读锁进行锁降级是为了可以读到当前线程修改过的数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值