说完了我们的 synchronized, 这次我们来说说我们的显示锁 ReetrantLock.
上期回顾:
上次博客我们主要说了锁的分类, synchronized 的使用, 和 synchronized 隐式锁的膨胀升级过程, 从无锁是如何一步步升级到我们的重量级锁的, 还有我们的逃逸分析.
锁的粗化和锁的消除
这个本来应该是在 synchronized 里面去说的, 忘记了, 不是很重要, 但是需要知道有这么一个东西啦.
我们先来演示一下锁的粗化:
- StringBuffer sb = new StringBuffer();
- public void lockCoarseningMethod(){
- //jvm 的优化, 锁的粗化
- sb.append("1");
- sb.append("2");
- sb.append("3");
- sb.append("4");
- }
我们都知道我们的 StringBuffer 是线程安全的, 也就是说我们的 StringBuffer 是用 synchronized 修饰过的. 那么我们可以得出我们的 4 次 append 都应该是套在一个 synchronized 里面的.
- StringBuffer sb = new StringBuffer();
- public void lockCoarseningMethod() {
- synchronized (Test.class) {
- sb.append("1");
- }
- synchronized (Test.class) {
- sb.append("2");
- }
- synchronized (Test.class) {
- sb.append("3");
- }
- synchronized (Test.class) {
- sb.append("4");
- }
- }
按照理论来说应该是这样的, 其实 JVM 对 synchronized 做了优化处理, 底层会优化成一次的 synchronized 修饰, 感兴趣的可以用 javap -c 自己看一下, 这里就不带大家去看了, 我以前的博客有 javap 看汇编指令码的过程.
- StringBuffer sb = new StringBuffer();
- public void lockCoarseningMethod() {
- synchronized (Test.class) {
- sb.append("1");
- sb.append("2");
- sb.append("3");
- sb.append("4");
- }
- }
再来看一下锁的消除, 其实这个锁的消除, 真的对于 synchronized 理解了, 锁的消除一眼就知道是什么了.
- public static void main(String[] args) {
- synchronized (new Object()){
- System.out.println("开始处理逻辑");
- }
- }
对于 synchronized 而言, 我们每次去锁的都是对象, 而你每次都创建的一个新对象, 那还锁毛线了, 每个线程都可以拿到对象, 都可以拿到对象锁啊, 所以没不会产生锁的效果了.
概述 AQS:
AQS 是 AbstractQueuedSynchronizer 的简称, 字面意思, 抽象队列同步器. Java 并发编程核心在于 java.concurrent.util 包而 juc 当中的大多数同步器 实现都是围绕着共同的基础行为, 比如等待队列, 条件队列, 独占获取, 共享获 取等, 而这个行为的抽象就是基于 AbstractQueuedSynchronizer 简称 AQS,AQS 定 义了一套多线程访问共享资源的同步器框架, 是一个依赖状态 (state) 的同步器. 就是我们上次博客说的什么公平锁, 独占锁等等.
AQS 具备特性
阻塞等待队列
共享 / 独占
公平 / 非公平
可重入
允许中断
AQS 的简单原理解读:
ReetrantLock 的内部功能还是很强大的, 有很多的功能, 我们来一点点缕缕. 如 Lock,Latch,Barrier 等, 都是基于 AQS 框架实现, 一般通过定义内部类 Sync 继承 AQS 将同步器所有调用都映射到 Sync 对应的方法 AQS 内部维护属性 volatile int state (32 位),state 表示资源的可用状态
State 三种访问方式
- getState()
- setState()
- compareAndSetState()
AQS 定义两种资源共享方式
Exclusive - 独占, 只有一个线程能执行, 如 ReentrantLock
Share - 共享, 多个线程可以同时执行, 如 Semaphore/CountDownLatch
AQS 定义两种队列
同步等待队列
条件等待队列
AQS 已经在顶层实现好了. 自定义同步器实现时主要实现以下几种方法:
isHeldExclusively(): 该线程是否正在独占资源. 只有用到 condition 才需要去实现它.
tryAcquire(int): 独占方式. 尝试获取资源, 成功则返回 true, 失败则返回 false.
tryRelease(int): 独占方式. 尝试释放资源, 成功则返回 true, 失败则返回 false.
tryAcquireShared(int): 共享方式. 尝试获取资源. 负数表示失败; 0 表示成功, 但没有剩余可用资源; 正数表示成功, 且有剩余资源.
tryReleaseShared(int): 共享方式. 尝试释放资源, 如果释放后允许唤醒后续等待结点返回 true, 否则返回 false.
刚才提到那么多属性, 可能会有一些懵, 我们来看一下 ReentrantLock 内部是怎么来实现哪些锁的吧.
打开我们的 ReetrantLock 源代码可以看到一个关键的属性
private final Sync sync;
后面有一个抽象方法并且继承了 AbstractQueuedSynchronizer 类, 内部有一个用 volatile 修饰过的整型变量 state, 他就是用来记录上锁次数的, 这样就实现了我们刚才的说的重入锁和非可重入锁. 我们来画一个图.
AbstractQueuedSynchronizer 这个类里面定义了详细的 ReetrantLock 的属性, 后面我会一点点去说, 带着解读一下源码(上面都是摘自源码的).state 和线程 exclusiveOwnerThread 比较好理解, 最后那个队列可能不太好弄, 我这里写的也是比较泛化的, 后面我会弄一个专题一个个去说. 相面说的 CLH 队列其实不是很准确, 我们可以理解为就是一个泛型为 Node 的双向链表结构就可以了.
等待队列中 Node 节点内还有三个很重要的属性就是 prev 前驱指针指向我们的前一个 Node 节点, 和一个 next 后继指针来指向我们的下一个 Node 节点, 这样就形成了一个双向链表的结构, 于此同时还有一个 Thread 来记录我们的当前线程.
在条件队列中, prev 和 next 指针都是 null 的, 不管是什么队列, 他都有一个 waitStatus 的属性来记录我们的节点状态的, 就是我们刚才说的 CANCELLED 结束, SIGNAL 可唤醒那四个常量值.
AQS 中 ReetrantLock 的使用:
公平锁和非公平锁: 这个还是比较好记忆的, 举一个栗子, 我们去车站排队上车, 总有 ** 插队, 用蛇形走位可以上车的是吧, 这就是一个非公平的锁, 如果说, 我们在排队的时候加上护栏, 每次只能排一个人, 他人无法插队的, 这时就是一个公平锁. 总之就是不加塞的就是公平的, 我们都讨厌不公平.
重入锁与非可重入锁: 这个也很好理解, 重入锁就是当我们的线程 A 拿到锁以后, 可以继续去拿多把锁, 然后再陆陆续续的做完任务再去解锁, 非可重入呢, 就是只能获得一把锁, 如果想获取多把锁, 不好意思, 去后面排下队伍. 下面我化了一个重入锁的栗子, 快过年了, 大家提着行李回老家, 我们进去了会一并带着行李进去(不带行李的基本是行李丢了), 这就是一个重入锁的栗子, 我们人进去了获得通道通过(锁), 然后我们也拖着行李获得了通道通过(锁), 然后我们才空出通道供后面的人使用. 如果是非可重入锁就是人进去就进去吧, 行李再次排队, 说不准什么时候能进来.
上一段代码来验证一下我们上面说的那些知识点.
- import java.util.concurrent.locks.ReentrantLock;
- public class Test {
- private ReentrantLock lock = new ReentrantLock(true);//true 公平锁, false 非公平锁
- public void lockMethod(String threadName) {
- lock.lock();
- System.out.println(threadName + "得到了一把锁 1");
- lock.lock();
- System.out.println(threadName + "得到了一把锁 2");
- lock.lock();
- System.out.println(threadName + "得到了一把锁 3");
- lock.unlock();
- System.out.println(threadName + "释放了一把锁 1");
- lock.unlock();
- System.out.println(threadName + "释放了一把锁 2");
- lock.unlock();
- System.out.println(threadName + "释放了一把锁 3");
- }
- public static void main(String[] args) {
- Test test = new Test();
- new Thread(() -> {
- String threadName = Thread.currentThread().getName();
- test.lockMethod(threadName);
- }, "线程 A").start();
- }
- }
通过代码阅读我们知道我们弄一个重入锁, 加三次锁, 解三次锁, 我们来看一下内部 sync 的变化, 调试一下.
我们看到了我们的 state 变量是用来存储我们的入锁次数的. 刚才去看过源码的小伙伴知道了我们的 state 是通过 volatile 修饰过的, 虽然可以保证我们的有序性和可见性, 但是一个 int++ 的操作, 他是无法保证原子性的, 我们继续来深挖一下代码看看内部是怎么实现高并发场景下保证数据准确的. 点击 lock 方法进去, 我们看到 lock 方法是基于 sync 来操作的, 就是我们上面的画的那个 ReetrantLock 的图.
- /**
- * Sync object for fair locks
- */
- static final class FairSync extends Sync {
- private static final long serialVersionUID = -3000897897090466540L;
- final void lock() {// 开始加锁
- acquire(1);
- }
- /**
- * Fair version of tryAcquire. Don't grant access unless
- * recursive call or no waiters or is first.
- */
- protected final boolean tryAcquire(int acquires) {
- final Thread current = Thread.currentThread();// 得到当前线程
- int c = getState();// 得到上锁次数
- if (c == 0) {// 判断是否上过锁
- if (!hasQueuedPredecessors() &&//hasQueuedPredecessors 判断是否有正在等待的节点,
- compareAndSetState(0, acquires)) {// 通过 unsafe 去更新上锁次数
- setExclusiveOwnerThread(current);// 设置线程
- return true;
- }
- }
- else if (current == getExclusiveOwnerThread()) {
- int nextc = c + acquires;
- if (nextc < 0)
- throw new Error("Maximum lock count exceeded");
- setState(nextc);
- return true;
- }
- return false;
- }
- }
这次我们开启多个线程来同时访问来看一下我们的 Node 的变化. 同时开启 ABCD 四个线程来执行这个
这次我们看到了 head 属性和 tail 属性不再是空的. head 是也是一个 node 节点, 前驱指针是空的, 后驱指针指向后继节点, Thread 为空, tail 的 node 节点正好是和 head 相对应的节点. 这样的设计就是为了更好的去验证队列中还是否存在剩余的线程节点需要处理. 然后该线程运行结束以后会唤醒在队列中的节点, 然其它线程继续运行.
我们知道我们创建的公平锁, 如果说 BCD 好好的在排队, E 线程来了, 只能好好的去排队, 因为公平, 所以排队, 如果我们创建的是非公平锁, E 线程就有机会拿到锁, 拿到就运行, 拿不到就去排队.
来源: https://www.cnblogs.com/cxiaocai/p/12191666.html