7、深入理解AQS独占锁之ReentrantLock源码分析

目录

1. 管程 — Java同步的设计思想

1.1.MESA模型

2. AQS原理分析

2.1 什么是AQS

2.2 AQS核心结构

2.3 AQS定义两种队列

AQS 定义了5个队列中节点状态:

3. ReentrantLock源码分析

3.1 公平和非公平锁,可重入锁是如何实现的 

3.2 设计的精髓:并发场景下入队和出队操作是如何设计的

线程竞争锁失败入队阻塞逻辑实现

 释放锁的线程唤醒阻塞线程出队竞争锁的逻辑实现


Java juc下的并发工具类大部分都是基于AQS设计的

AQS是AbstractQueuedSynchronizer的缩写,是Java中的一个同步工具类。AQS提供了一种基于FIFO队列的同步机制,可以用于构建锁、信号量、倒计时器等并发工具。AQS的实现方式是通过一个volatile int state来表示同步状态,当state等于0时表示未被占用,当state大于0时表示被占用。AQS提供了一些基本的操作方法,如acquire、release、tryAcquire等,用于协调多个线程对同一资源的访问。AQS的使用可以大大简化并发编程中锁的实现过程,提高代码可读性和可维护性。

思考: 如何设计一把独占锁?

1. 管程 — Java同步的设计思想

        在现代操作系统中,管程已经成为一种被广泛采用的同步和通信机制。管程作为高级抽象层,可以基于底层机制(如信号量)实现更加简洁、直观的同步和通信接口,方便程序员进行并发编程。管程不是操作系统必须要实现的技术,但它是现代操作系统中非常重要的一种并发控制方法。

        管程是由一个对象和一组方法组成,管程对象是负责协调各个线程访问共享资源的主体,而其中的方法是提供给线程调用的,只有获得管程对象锁的线程才可以执行管程中的方法。这样能够保证同一时刻只有一个线程可以进入管程执行,并且其他线程需要等待该线程执行完毕后才能进行执行。

        管程的实现方式中,对于获取管程对象锁失败的线程,可以选择等待或者阻塞,避免线程占用过多的系统资源。同时,管程可以提供条件变量(Condition)的机制,让等待的线程可以在满足某些条件后自动被唤醒。


管程:是一种多线程同步的方式,它提供了一种自动控制多个线程互斥访问共享资源的机制。管理共享变量以及对共享变量的操作过程,让他们支持并发。

互斥:同一时刻只允许一个线程访问共享资源;

同步:线程之间如何通信、协作。

1.1.MESA模型

在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在正在广泛使用的是MESA模型。

AQS基于这种模型,用代码实现

 管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。

Java中针对管程有两种实现

  • 一种是基于Object的Monitor机制,用于synchronized内置锁的实现
  • 一种是抽象队列同步器AQS,用于JUC包下Lock锁机制的实现

共享变量V:理解为独占锁的加锁标记、上锁=1,未上锁=0。同步机制中资源标记,资源来了加1,使用减1

入口等待队列:就绪队列

条件队列:阻塞队列。条件可理解为消费者和生产者的例子

如何设计一把独占锁:

  1. 定义一个state,0,1表示是否加锁
  2. 多线程来时,如何保证只有一个线程抢锁成功?用CAS。
  3. 等待队列,存放竞争锁失败的线程数据。一般用数组和链表。若是链表需要存放当前线程信息,被唤醒时出列
  4. 此处需要一个等待换醒机制,两种方案。一种是sychronized 和 object.wait()....;但有个缺陷,被调用方法时需要先获取锁,而且无法保证具体唤醒哪个线程,即无法保证链表的首个结点出列。另一种时ReentrantLock + Condition.await()/.singal()/.singalAll()..
  5. 此处用LockSupport.park()/unpark(),一个线程发许可一个线程获取,可以换醒需要的某一个线程
  6. 
    public class ParkAndUnparkDemo {
        public static void main(String[] args) {
            ParkAndUnparkThread myThread = new ParkAndUnparkThread(Thread.currentThread());
            myThread.start();
    
            System.out.println("before park");
            // 获取许可
            LockSupport.park();
            System.out.println("after park");
        }
    }
    
    class ParkAndUnparkThread extends Thread {
        private Object object;
    
        public ParkAndUnparkThread(Object object) {
            this.object = object;
        }
    
        public void run() {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("before unpark");
            // 释放许可
            LockSupport.unpark((Thread) object);
            System.out.println("after unpark");
        }
    }
  7. 第4点所说的ReentrantLock 等待唤醒机制底层用的是LockSupport.park()/unpark。sychronized的wait()和notify.all()是对应在jvm层面的LockSupport.park()/unpark
  8. 为入口队列和等待队列定义一个模板类(技术大牛Doug Lea就用模板方法,他喜欢设计东西前先规范能力即先写接口、抽象类)
  9. 借助AbstractQueuedSynchronizer类,自己实现内部逻辑(代码TulingLock,SyncDemo),

2. AQS原理分析

2.1 什么是AQS

java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这些行为的抽象就是基于AbstractQueuedSynchronizer(简称AQS)实现的,AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。

JDK中提供的大多数的同步器如Lock, Latch, Barrier等,都是基于AQS框架来实现的

  • 一般是通过一个内部类Sync继承 AQS
  • 将同步器所有调用都映射到Sync对应的方法

AQS具备的特性:

  • 阻塞等待队列
  • 共享/独占
  • 公平/非公平
  • 可重入
  • 允许中断 

2.2 AQS核心结构

AQS内部维护属性volatile int state

  • state表示资源的可用状态

State三种访问方式:

  • getState()
  • setState()
  • compareAndSetState()

定义了两种资源访问方式:

  • Exclusive-独占,只有一个线程能执行,如ReentrantLock
  • Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch

AQS实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

2.3 AQS定义两种队列

  • 同步等待队列: 主要用于维护获取锁失败时入队的线程。
  • 条件等待队列: 调用await()的时候会释放锁,然后线程会加入到条件队列,调用signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁。

AQS 定义了5个队列中节点状态:

  1. 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
  2. CANCELLED,值为1,表示当前的线程被取消;
  3. SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;
  4. CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
  5. PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;

3. ReentrantLock源码分析

Demo 代码如下

package sync;


import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 模拟抢票场景
 */
public class ReentrantLockDemo {

    private final ReentrantLock lock = new ReentrantLock();//默认非公平
    private static int tickets = 8; // 总票数

    public void buyTicket() {
        lock.lock(); // 获取锁
        try {
            if (tickets > 0) { // 还有票    读
                try {
                    Thread.sleep(10); // 休眠10ms
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "购买了第" + tickets-- + "张票"); //写

                buyTicket();
            } else {
                System.out.println("票已经卖完了," + Thread.currentThread().getName() + "抢票失败");
            }

        } finally {
            lock.unlock(); // 释放锁
        }
    }


    public static void main(String[] args) {
        ReentrantLockDemo ticketSystem = new ReentrantLockDemo();
        for (int i = 1; i <= 10; i++) {
            Thread thread = new Thread(() -> {
                ticketSystem.buyTicket(); // 抢票

            }, "线程" + i);
            // 启动线程
            thread.start();
        }


        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("剩余票数:" + tickets);
    }
}

3.1 公平和非公平锁,可重入锁是如何实现的 

 开始断点:

 同时进到源码里面,断住lock(),和unlock()方法

选择线程查看

流程如下: 

 了解下:ReentrantLock里有个内部类FairSync:

3.2 设计的精髓:并发场景下入队和出队操作是如何设计的

线程竞争锁失败入队阻塞逻辑实现

 往下走到进acquireQueued(final Node node, int arg);方法

源码中 Node.SIGNAL = -1

 执行完后发现线程被挂起(这里应该是操作系统挂起的,因为唤醒后条件不足),等待锁。若有其他线程给当前线程设置中断异常,则阻塞失败。(这一步感觉会有点奇怪,但它源码非要这样设置一下。或者也有可能我理解错了,欢迎大家指正)

之后线程状态类似

 释放锁的线程唤醒阻塞线程出队竞争锁的逻辑实现

代码差不多,把status=0,可重入锁标志exclusiveOwnerThread=null。设置当前线程就是head结点,就可以执行抢锁操作,前驱执行出队操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值