深入理解AQS

深入理解AQS

AQS
概念
  • 是一种阻塞式锁和相关的同步器工具的框架
特点
  • 用state属性来表示资源的状态(分为独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取和释放锁
    • getState获取state状态
    • setState设置state状态
    • compareAndSetState 利用cas机制设置state状态
    • 独占模式是只有一个线程能够访问资源,共享模式是可以允许多个线程访问资源
  • 提供了基于队列的等待队列,类似于Monitor的EntryList
  • 条件变量来实现等待,唤醒机制,支持多个条件变量,类似于Monitor的WaitSet
AOS自定义实现锁
package com.zb.juc.test;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @Description:
 * @Author:啵啵啵啵啵啵唧~~~
 * @Date:2022/4/26
 */
@Slf4j(topic = "c.TestAqs")
public class TestAqs {
}

/**
 * 自定义锁(不可重入锁)
 */
class MyLock implements Lock {
    /**
     * 独占锁
     */
    class MySync extends AbstractQueuedSynchronizer{
        /**
         * 加锁
         * @param arg
         * @return
         */
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0,1)){
                //加上了锁,需要设置owner为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        /**
         * 解锁
         * @param arg
         * @return
         */
        @Override
        protected boolean tryRelease(int arg) {
            //将Qwn设置为空表示没有线程占用
            setExclusiveOwnerThread(null);
            //将状态改为0
            setState(0);
            return true;
        }

        /**
         * 是否持有独占锁
         * @return
         */
        @Override
        protected boolean isHeldExclusively() {
            return getState()==1;
        }

        public Condition newCondition() {
            return new ConditionObject();
        }
    }

    private MySync sync = new MySync();

    /**
     * 加锁,不成功进入等待队列等待
     */
    @Override
    public void lock() {
       sync.acquire(1);
    }

    /**
     * 加锁,可打断
     */
    @Override
    public void lockInterruptibly() throws InterruptedException {
       sync.acquireInterruptibly(1);
    }

    /**
     * 尝试加锁只加锁一次一次失败之后就返回false
     */
    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    /**
     * 带超时版本的tryLock
     * @param time
     * @param unit
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1,unit.toNanos(time));
    }

    /**
     * 解锁
     */
    @Override
    public void unlock() {
       sync.release(1);
    }

    /**
     * 条件变量
     * @return
     */
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}

ReentrantLock原理

在这里插入图片描述

非公平锁实现原理
加锁解锁原理
  • 先看构造器,默认为非公平锁的实现
/**
 * Creates an instance of {@code ReentrantLock}.
 * This is equivalent to using {@code ReentrantLock(false)}.
 */
public ReentrantLock() {
    //NonfairSync继承自AQS
    sync = new NonfairSync();
}
  • 非公平锁加锁源码
final void lock() {
    if (compareAndSetState(0, 1))
        //没有竞争的时候状态为1,进行加锁->将owner设置为当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
  • 没有竞争的时候,就加锁成功了
    在这里插入图片描述
竞争失败原理
  • 出现竞争的时候
final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //出现竞争的时候即当前的状态不为1->调用这个acquire()方法
                acquire(1);
        }
  • acquire()方法
public final void acquire(int arg) {
       //acquire()方法会调用一个tryAcquire(arg)方法其实就是尝试获取锁,
        if (!tryAcquire(arg) &&
            //如果再调用这个tryAcquire(arg)方法的时候其他线程恰好释放了锁,那么tryAcquire(arg) 方法的返回值就是false就不会走这个if块,否者就会走这个if语句块,执行acquireQueued()方法这个方法的作用式添加一个节点进入阻塞
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

  • 如果尝试获取锁失败会进入acquireQueued()方法在这个方法里面其实还会再尝试获取几次
/**
当前线程进入acquireQueued的逻辑
1、acquireQueued会在一个死循环中不断尝试获取锁,失败之后进入park阻塞
2、如果自己是紧邻着head那么自己排在第二位,此时尝试再次获取锁,如果这时恰好人家咱占有者释放锁成功,那么自己就有机会获得锁,如果人家没有释放锁释放锁,就还是获取失败
3、获取失败进入shouldParkAfterFailedAcquire逻辑将前驱node,即head的waitStatus改为-1,改为-1的意思是前驱节点需要唤醒这个后继节点,然后返回false再次尝试
4、shouldParkAfterFailedAcquire执行完毕之后回到这个acquireQueued,再次尝试tryAcquire,如果失败
5、这时候再次进入shouldParkAfterFailedAcquire方法时,因为前驱节点waitStatus已经时-1了,这次返回true
6、shouldParkAfterFailedAcquire返回true就会进入到这个parkAndCheckInterrupt方法,进行park
**/

//--------------------------------------------------------------------------------
//这就是等待对列 
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取前驱节点
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                //不再进行尝试
                cancelAcquire(node);
        }
    }

//------------------------------------------------------------------------------------
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * 该节点已经设置了请求释放信号的状态,因此可以安全停止
             */
            return true;
        if (ws > 0) {
            /*
             * 前任被取消了。跳过前导并指示重试。
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus必须为0或0。表明我们需要信号,但先别停车。打电话的人需要重试以确保在停车前无法获取。
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

//---------------------------------------------------------------------------------------
   private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
RenntrantLock可重入的原理
  • nonfairTryAcquire()获取锁源码分析
 //acquires 为1
final boolean nonfairTryAcquire(int acquires) {
     //先获取当前的线程
            final Thread current = Thread.currentThread();
     //获取当前线程的状态
            int c = getState();
     //判断当前线程的状态是为0,为0表示没有线程占用
            if (c == 0) {
                //没有线程占用的话直接使用CAS进行交换,此时状态就改变为1
                if (compareAndSetState(0, acquires)) {
                    //设置当前锁的占有者为当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
    //如果状态不为0,表示当前锁还被占有着呢,此时线程比一定会被阻塞住,他会先判断锁的占有者是否是自己
            else if (current == getExclusiveOwnerThread()) {
                //如果此时锁的占有者就是自己,那么将state这个状态进行+1
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
可打断原理
可打断模式
  • 在此模式下,即使他被打断,仍然会被驻留在AQS对罗列中,等待获得锁之后才能继续运行
  • 线程在没有办法获得锁的时候会进入这个acquireQueued()方法进行循环尝试
  • acquireQueued方法分析
private final boolean parkAndCheckInterrupt() {
    //如果打断标记已经是true,则park会失效
        LockSupport.park(this);
    //interrupted 会清除打断标记
        return Thread.interrupted();
    }


final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    //尝试仍不成功进入这个park方法
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
公平锁实现原理
非公平锁实现
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //判断是否有获得锁
            if (c == 0) {
                //没有线程获得这个把锁的时候,当前线程直接尝试CAS获得,不会去检查AQS队列,所以是非公平的
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
公平锁实现
  • 公平锁和非公平锁区别主要在于 tryAcquire方法的实现
  • 公平锁在线程进来之后会先判断AQS队列当中是否有这个前序节点,没有才会去竞争
读写锁
ReentrantReadWriteLock
  • 当读操作远远高于写操作时,这时候使用读写锁让读读可以并发,提高性能。
  • 类似于数据库当中的select … from … lock in share mode
  • 提供一个 数据容器类 内部分别使用读锁保护数据的read()方法,写锁保护数据的write()方法
/**
 * 对数据进行一些读写实验
 */
@Slf4j(topic = "c.DataContainer")
class DataContainer{
    /**
     * 读写操作的数据
     */
    private Object data;
    private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    /**
     * 获取读锁对象
     */
    private ReentrantReadWriteLock.ReadLock r = rw.readLock();
    /**
     * 获取写锁对象
     */
    private ReentrantReadWriteLock.WriteLock w = rw.writeLock();

    /**
     * 读取数据操作
     * @return
     */
    public Object read(){
        log.debug("获取读锁");
        r.lock();
        try {
            log.debug("读取");
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            log.debug("释放读锁");
            r.unlock();
            return data;
        }
    }

    /**
     * 写数据操作
     * @param data
     */
    public void write(Object data){
        log.debug("获取写锁");
        w.lock();
        try {
            log.debug("写入");
            this.data = data;
        }finally {
            log.debug("释放写锁");
            w.unlock();
        }
    }

}
  • 验证读读不互斥
//开两个线程同时调用对这个数据进行读操作
@Slf4j(topic = "c.TestReadWriteLock")
public class TestReadWriteLock {
    public static void main(String[] args) {
        DataContainer dc = new DataContainer();
        new Thread(()->{
            dc.read();
        },"t1").start();
        new Thread(()->{
            dc.read();
        },"t2").start();
    }
}

读读并不会互斥

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kovpuiyw-1652074204608)(../../../AppData/Roaming/Typora/typora-user-images/image-20220428205957339.png)]

  • 验证读写操作的互斥
@Slf4j(topic = "c.TestReadWriteLock")
public class TestReadWriteLock {
    public static void main(String[] args) throws InterruptedException {
        DataContainer dc = new DataContainer();
        new Thread(()->{
            dc.read();
        },"t1").start();
        Thread.sleep(100L);
        new Thread(()->{
            dc.write(1);
        },"t2").start();
    }
}

读写互斥

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XZXqMBGR-1652074204610)(../../../AppData/Roaming/Typora/typora-user-images/image-20220428210312343.png)]

注意事项
  • 读写锁不支持条件变量
  • 重入时升级不支持,即持有读锁的情况下去获取写锁,会导致读写锁永久的等待
  • 重入时降级时支持的,意思就是持有写锁的情况下去获取读锁是支持的
class CachedData {
    Object data;
    // 是否有效,如果失效,需要重新计算 data
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {
// 获取写锁前必须释放读锁
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
// 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新
                if (!cacheValid) {
                    data = ...
                    cacheValid = true;
                }
// 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存
                rwl.readLock().lock();
            } finally {
                rwl.writeLock().unlock();
            }
        }
        // 自己用完数据, 释放读锁
        try {
            use(data);
        } finally {
            rwl.readLock().unlock();
        }
    }
}
  • 读写锁的应用是可以拿来做缓存
读写锁原理
  • 读写锁用的是用一个Sync同步器,因此等待队列,state等也是同一个

**场景演示:t1线程加写锁 w.lock t2线程加读锁 r.lock **

  1. t1成功上锁,流程和ReentrantLock加锁相比没有特殊之处,不同是写锁状态使用的是state的低16位,读锁使用的是state的高16位

  • 源码分析:WriteLock的lock方法
public void lock() {
   sync.acquire(1);
}

//-----------进入acquire方法----------------------------------
public final void acquire(int arg) {
    //调用tryAcquire方法尝试加锁
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
//----------读写锁的tryAcquire方法--------------------------------
 protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
     //先拿到状态
            int c = getState();
     //获取读锁部分状态
            int w = exclusiveCount(c);
     //判断一下状态是否等于0,现在不等于0存在两种情况
       //1、加了读锁占用的是state的高16位
       //2、加了写锁占用的是state的低16位
            if (c != 0) {
                //判断w是否为0,意思就是判断读锁是否为0,如果读锁不为0且我们现在进行的是这个写锁操作,那么读写锁互斥会返回false
                //判断当前线程是否是线程的占有者,因为可能存在锁重入的现象,如果不是锁重入的现象直接返回false
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                //判断锁重入次数是否超过65535次,基本不可能
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                //进行加锁,将当前锁状态值+1
                setState(c + acquires);
                //加锁成功返回最大值
                return true;
            }
     //如果状态c等于0,那就好办了,说明当前锁没有被线程占用
     //writerShouldBlock 判断写锁是否被阻塞,这个要看是公平锁还是非公平锁,如果是非公平锁,直接返回false,如果是公平锁回去检查队列的老二是否存在,若存在则进入队列
            if (writerShouldBlock() ||
                //在c的基础上进行+1
                !compareAndSetState(c, c + acquires))
                return false;
     //设置线程拥有着为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }

StampedLock

该类自JDK8加入,是为了进一步优化读性能,它的特点是在使用读锁,写锁的时候必须配合使用

加解读锁锁

//stamp就是一个戳
long stamp = lock.readLock();
lock.unlockRead(stamp);

加解写锁

long stamp = lock.writeLock();
lock.unlockWrite(stamp);

乐观读,StampedLock支持tryOptimisticRead()方法乐观读,读取完毕后需要做一次戳校验如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。

//进行一个乐观读
long stamp = lock.tryOptimisticRead();
//验戳
if(!lock.validate(stamp)){
    //进行锁升级
}

StampedLock 使用的应用案例

@Slf4j(topic = "c.DataContainerStamped")
class DataContainerStamped{
    /**
     * 共享数据
     */
    private int data;

    /**
     * 初始化StampedLock锁
     */
    private final StampedLock lock = new StampedLock();

    /**
     * 对数据进行写操作
     * @param newData
     */
    public void write(int newData){
        //调用StampedLock锁的写锁方法返回一个戳
         long stamp = lock.writeLock();
         log.debug("write lock{}",stamp);
        try {
            Thread.sleep(2000L);
            this.data = newData;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            log.debug("write unlock{}",stamp);
            //通过这个戳来释放指定的锁
            lock.unlockWrite(stamp);
        }
    }

    /**
     * 对数据进行读操作
     * @param readTime 设定读取时间,用来测试用
     * @return
     */
    public int read(int readTime) throws InterruptedException {
        long stamp = lock.tryOptimisticRead();
        log.debug("进行乐观读...{}",stamp);
        Thread.sleep(readTime);
        //进行戳的检验
        if (lock.validate(stamp)){
            //检验成功,返回当前数据
            log.debug("read finish...",stamp);
            return data;
        }
        //戳检验失败,进行锁的升级 加读锁
        log.debug("updating to read lock...{}",stamp);
        try {
            //加读锁
            stamp = lock.readLock();
            log.debug("read lock{}",stamp);
            Thread.sleep(readTime);
            log.debug("read finish...{}",stamp);
            return data;
        }finally {
            //释放读锁
            lock.unlockRead(stamp);
        }
    }
}
  • 测试读读不会互斥
@Slf4j(topic = "TestStampedLock")
public class TestStampedLock {
    public static void main(String[] args) throws InterruptedException {
       DataContainerStamped dataContainer = new DataContainerStamped();
       new Thread(() ->{
           try {
               dataContainer.read(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       },"t1").start();
       Thread.sleep(500L);
       new Thread(() ->{
           try {
               dataContainer.read(0);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       },"t2").start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ykK27IAE-1652074204611)(../../../AppData/Roaming/Typora/typora-user-images/image-20220429121750298.png)]

  • 测试读写
@Slf4j(topic = "TestStampedLock")
public class TestStampedLock {
    public static void main(String[] args) throws InterruptedException {
       DataContainerStamped dataContainer = new DataContainerStamped();
       new Thread(() ->{
           try {
               dataContainer.read(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       },"t1").start();
       Thread.sleep(500L);
       new Thread(() ->{
           dataContainer.write(1000);
       },"t2").start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xuF6foaE-1652074204613)(../../../AppData/Roaming/Typora/typora-user-images/image-20220429122443223.png)]

  • StampedLock是否能够替代ReenterLock呢?
  • 不行,因为,乐观读存在缺点,不支持条件变量,不支持可重入
Semaphore
  • 信号量,用来限制等同时访问资源的线程上限
Semaphore s = new Semaphore(3); //限制上限为3
for(int i = 0; i < 10; i++){
    new Thread(() -> {
         try{
        s.acquire();//获此信号量
        System.out.prinltn("我是线程" + Thread.currentThread().getName());
        try{
            Thread.sleep(2000L);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
    }catch(InterruptedException e){
        s.release();//释放信号量
    }  
  }).start();
}
  • Semaphore 案例
  • 不进行线程访问数量的限制
@Slf4j(topic = "c.TestSemaphore")
public class TestSemaphore {
    public void main(String[] args) {
        for (int i = 0; i < 10; i++){
            new Thread(() ->{
                try {
                    log.debug("running...");
                    Thread.sleep(1000L);
                    log.debug("end...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gjHqArlD-1652074204614)(../../../AppData/Roaming/Typora/typora-user-images/image-20220429113637444.png)]

  • 限制最多两个线程执行任务
@Slf4j(topic = "c.TestSemaphore")
public class TestSemaphore {
    public static void main(String[] args) {
        //1、创建semaphore对象,设置线程的上限为3
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i < 6; i++){
            new Thread(() ->{
                try {
                    //获得许可
                    semaphore.acquire();
                    log.debug("running...");
                    Thread.sleep(1000L);
                    log.debug("end...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //释放许可
                    semaphore.release();
                }
            }).start();
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Thnt1Ocl-1652074204616)(../../../AppData/Roaming/Typora/typora-user-images/image-20220429113412345.png)]

  • Semaphore 应用
  • 使用Semaphore进行限流,在访问高风期的时候,让请求线程阻塞,高峰期过去再申请释放,当然它只适合限制单线程数量,二不是限制资源次数
  • 用Semaphore实现简单的连接池
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值