【Java多线程】JUC之显示锁(Lock)与初识AQS(队列同步器)

文章目录

一.前言

并发编程最佳学习路线

【Java基础】多线程从入门到掌握

【Java多线程】线程通信

【Java多线程】JUC之CAS机制与原子类型(Atomic)

【Java多线程】JUC之Java并发包Concurrent发展简述(各版本JDK中的并发技术)

了解高并发必须知道的概念

【Java多线程】高并发修炼基础之高并发必须了解的概念

了解Java并发包Concurrent发展简述

【Java多线程】JUC之Java并发包Concurrent发展简述(各版本JDK中的并发技术)

了解锁的分类

【Java多线程】成神之路中必须要了解的锁分类

为什么要用锁?

  • 锁可以解决并发执行任务执行过程中对 共享数据顺序访问、修改的场景 。比如对同时对一个账户进行 扣款 或者 转账 。

线程安全三大特性

【Java多线程】重温并发BUG的源头之可见性、原子性、有序性

【Java多线程】成神之路中必须要了解的锁分类的第一节第8小点

二.内置锁-synchronized

三.显示锁-Lock

锁机制用于保证操作的原子性、可见性、顺序性。 JDK1.5的concurrent并发包 中新增了 Lock接口以及相关实现类来实现锁功能 ,最明显的特性就是需要显式的申请锁和释放锁。比synchronized更加灵活。

显示锁的释放锁的操作一定要放到finally块中,否则可能会因为异常导致锁永远无法释放!这是显式锁最明显的缺点。

1.Lock特性

1.1.显示加锁、解锁

  • synchronized 关键字是自动进行加锁、解锁的,而Lock的具体实现需要 lock() 和 unlock()方法配合 try/finally 语句块来完成,来手动加锁、解锁。

1.1.可重入

synchronized 和 ReentrantLock 都是可 重入锁 ,可重入性表明 了锁的分配机制是 基于线程的分配 ,而 不是基于方法调用 的分配。

  • 可重入锁:又名 递归锁 ,即一个线程得到一个对象锁后再次请求该对象锁,是永远可以拿到锁的。在 Java 中线程获得对象锁的操作是 以线程为单位的 ,而不是以方法调用为单位的。但 获取锁和释放锁必须要成对出现 。
  • ReentrantLock ,它是基于 AQS(AbstractQueueSyncronized) 实现的, AQS 是基于 volitale 和 CAS 实现的 ,其中 AQS 中维护一个 valitale 类型的变量 state 来做一个可重入锁的重入次数,加锁和释放锁也是围绕这个变量来进行的 。

1.2.可响应中断

  • 当线程因为获取锁而进入 阻塞状态 , 外部是可以中断该线程的 , 调用方通过捕获nterruptedException可以捕获中断
    • ReentrantLock 中的 lockInterruptibly() 方法 可以使线程在被 阻塞 时响应中断
      • 假设: 线程1 通过 lockInterruptibly() 方法获取到一个可重入锁,并执行一个长时间的任务,另一个 线程2 通过 interrupt() 方法 就可以立刻 打断 线程1的执行 ,来获取 线程1 持有的那个可重入锁。而通过 ReentrantLock 的 lock() 方法或者 Synchronized 持有锁的线程是不会响应其他线程的 interrupt() 方法的,直到该方法主动释放锁之后才会响应 interrupt() 方法。

1.3.可设置等待超时时间

  • synchronized 关键字无法设置锁的超时时间,如果一个获得锁的线程内部发生死锁,那么其他线程就会一直进入阻塞状态, 而 Lock的具体实现,可以 设置线程获取锁的等待超时时间 ,通过 方法返回值 判断是否成功获取锁,来 避免死锁

1.4.锁的公平性

提供公平锁和非公平锁2种选择。

  • 公平锁( 默认 ) :线程将按照他们发出 发出申请锁的顺序 来获取锁,先进先出, 不允许插队

  • 非公平锁: 允许插队 ,当一个线程请求获取锁时,如果这个锁是 可用 的,那这个线程将 跳过所在队列里等待线程并获得锁。

    • 如: synchronized 关键字是一 种非公平锁 ,先抢到锁的线程先执行。而 ReentrantLock的构造方法中允许设置 true/false 来实现公平、非公平锁 ,如果设置为 true ,则线程获取锁要遵循 "先来后到" 的规则,每 次都会构造一个 线程 Node ,然后到双向链表的 "尾巴"后面排队,等待前面的 Node 释放锁资源。
    • 考虑这么一种情况:A线程持有锁,B线程请求这个锁,因此B线程被挂起;A线程释放这个锁时,B线程将被唤醒,因此再次尝试获取锁; 与此同时,C线程也请求获取这个锁,那么C线程很可能在B线程被完全唤醒之前 获得、使用以及释放 这个锁。这是种双赢的局面,B获取锁的时刻(B被唤醒后才能获取锁)并没有推迟,C更早地获取了锁,并且吞吐量也获得了提高。 在大多数情况下,非公平锁的性能要高于公平锁的性能。
  • 另外,这个公平性是针对 线程 而言的,不能依赖此来实现业务上的公平性,应该由开发者自己控制,比如通过 FIFO队列 来保证公平。

1.5.读写锁

  • 允许读锁和写锁分离,读锁与写锁互斥,但是多个读锁可以共存, 即一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行 。适用于 读远大于写 的场景
    • 即:读读共享、写写互斥、读写互斥

关于读写锁的一些知识:

1.重入方面其内部的写锁可以获取读锁,但是反过来读锁想要获得写锁则永远都不要想。

2.写锁可以降级为读锁,顺序是: 先获得写锁再获得读锁,然后释放写锁 ,这时候线程将保持读锁的持有。反过来读锁想要升级为写锁则不行

3. 读锁被线程持有时排斥任何的写锁,而线程持有写锁则是完全的互斥.这一特性最为重要, ,对于 读多写少 的场景使用此类才可以提高并发量。

4.不管是读锁还是写锁都支持 响应中断 

5.写锁支持Condition且用于与ReentrantLock一样, 而读锁则不能使用Condition ,否则抛出UnsupportedOperationException异常。

1.6.丰富的API

提供了多个方法来获取锁相关的信息,可以帮助开发者监控和排查问题

  • isFair() :判断锁是否是公平锁
  • isLocked() :判断锁是否被任何线程获取了
  • isHeldByCurrentThread() :判断锁是否被当前线程获取了
  • hasQueuedThreads() :判断是否有线程在等待该锁
  • getHoldCount() :查询当前线程占有lock锁的次数
  • getQueueLength() :获取正在等待此锁的线程数

1.7.常用方法

  • void lock() :在线程获取锁时如果锁已被其他线程获取,则进行 等待
Lock lock = new ReentrantLock();//获取锁
lock.lock();
try{
 
    //获取到了被本锁保护的资源,处理任务
    //捕获异常
}finally{
 
    lock.unlock();   //释放锁
}
  • boolean tryLock() : 尝试获取锁 ,如果当前锁没有被其他线程占用,则获取成功,返回 true,否则返回 false
    • 我们可以根据是否能获取到锁来决定后续程序的行为。该方法会立即返回,在拿不到锁时也不会一直等待,通常我们用 if 语句 判断 tryLock() 的返回结果 , 根据是否获取到锁来执行不同的业务逻辑 ,使用方法如下。
Lock lock = new ReentrantLock();//获取锁
//如果能获取到锁
if(lock.tryLock()) {
 
     try{
 
         //处理任务
     }finally{
 
         lock.unlock();   //释放锁
     } 
}
//如果不能获取锁,则做其他事情
else {
 

}

利用 tryLock() 方法我们还可以解决死锁问题

public void tryLock(Lock lock1, Lock lock2) throws InterruptedException {
 
    	//自旋
        while (true) {
 
        	//如果能获取锁1
            if (lock1.tryLock()) {
 
                try {
 
                   //如果能获取锁2
                    if (lock2.tryLock()) {
 
                        try {
 
                            System.out.println("获取到了两把锁,完成业务逻辑");
                            return;
                        } finally {
 
                            //释放锁2
                            lock2.unlock();
                        }
                    }
                } finally {
 
                	//释放锁1
                    lock1.unlock();
                }
            }
            //如果不能能获取锁,线程休眠若干秒 
			else {
 
                Thread.sleep(new Random().nextInt(1000));
            }
        }
    }
  • boolean tryLock(long time, TimeUnit unit): 可响应中断并且有超时时间的尝试获取锁 ,在拿不到锁时会等待一定的时间,如果在时间结束后,还获取不到锁,就会返回 false;如果一开始就获取锁或者等待期间内获取到锁,则返回 true。

    这个方法解决了 lock() 方法容易发生死锁的问题,使用 tryLock(long time, TimeUnit unit) 时 ,在等待了一段指定的超时时间后, 线程会主动放弃获取这把锁 ,避免永久等待。 等待获取锁的期间,也可以 随时中断线程 ,这就避免了死锁的发生。

  • void lockInterruptibly(): 可响应中断的去获取锁 ,如果这个锁当前是可以获得的,那么这个方法会立刻返回,但是如果这个锁当前是不能获得的(被其他线程占用),那么当前线程便会开始 等待 ,除非它 等到了这把锁或者是在等待的过程中被中断了 ,否则这个线程便会一直在这里执行这行代码。一句话总结就是, 除非当前线程在获取锁期间被 中断 ,否则便会 一直尝试获取 直到获取到为止。

    顾名思义, lockInterruptibly() 是可以 响应中断 的。相比于不能响应中断的 synchronized 锁 , lockInterruptibly() 可以让程序更灵活,可以在获取锁的同时, 保持对中断的响应 。我们可以把这个方法理解为 超时时间是无穷长的 tryLock(long time, TimeUnit unit) ,因为 tryLock(long time, TimeUnit unit) 和 lockInterruptibly() 都能响应中断 ,只不过 lockInterruptibly() 永远不会超时。

public void lockInterruptibly() {
 
        try {
 
            lock.lockInterruptibly();
            try {
 
                System.out.println("操作资源");
            } finally {
 
                lock.unlock();
            }
        } catch (InterruptedException e) {
 
            e.printStackTrace();
        } finally {
 
           //释放锁
           lock.unlock();
        }
    }
  • void unlock() :释放当前线程占用的锁, 必须使用finally块保证发生异常时锁一定被释放
  • Condition newCondition() :创建绑定到此 Lock 实例的 Condition实例 ,用于替代wait()/notify()/notifyAll()方法, 实现线程的等待通知机制

    Condition对象的await()/signal()/signalAll() 的功能和wait()/notify()/notifyAll()一样

2.锁的使用

2.1.ReentrantLock

独占锁 的具体实现,拥有上面列举Lock 除读写锁之外的所有特性 ,使用比较简单

原理:通过 AQS 的方式来实现的, 通过 Unsafe 包提供的 CAS 操作来进行 锁状态(state)的竞争。然后通过 LockSupport.park(this) 进行 park 住线程,在 unpack() 唤醒在 AQS 队列头的对象让他去竞争锁。

.

在并发包中, 使用 ReetrantLock 的地方有:

  • CyclicBarrier
  • DelayQueue
  • LinkedBlockingDeque
  • ThreadPoolExecutor
  • ReentrantReadWriteLock
  • StampedLock
public class ReentrantLockTest {
 
    // 声明独占锁实例,构造函数传入true为公平锁,false为非公平锁(默认)
    private final ReentrantLock lock = new ReentrantLock();

    public void test() {
 
        //申请获取锁
        lock.lock();
        try {
 
            //方法主体业务逻辑
        } finally {
 
            // 必须要释放锁,unlock与lock成对出现
            lock.unlock();
        }
    }
}

ReetrantLock 加锁和解锁的过程入下图所示:

2.2.ReentrantReadWriteLock

ReentrantReadWriteLock是 读写锁 的具体实现,拥有上面列举Lock的所有特性。拥有2把锁,一把是 WriteLock (写锁,独占锁) ,一把是 ReadLock(读锁,共享锁) 。 读锁 可以允许 多个不同线程重入 ,但对于 写锁 ,同时只能有 一个线程重入 , 把读和写操作分离开来了,粒度更细,所以在性能上有所提高. 并且 写锁可降级为读锁,反之不行 。

  • 原理 :

ReentrantReadWriteLock也是基于 AQS 实现的

  • 读写锁中的加锁、释放锁也是基于 Sync (继承于 AQS ) ,并且主要使用 AQS 中的 state 和node 中的 waitState 变量进行实现的。
  • 实现读写锁与实现普通互斥锁的主要区别在于 : 需要分别记录读锁状态及写锁状态,并且等待队列中需要区别处理两种加锁操作。
  • ReentrantReadWriteLock 中将 AQS 中的 int 类型的 state 分为 高 16 位 与 低16 位 分别记录读锁和写锁的状态,如下图所示: 

  • ReadLock(读锁)是 共享锁(乐观锁)
  • WriteLock(写锁)是 悲观锁(排他锁、互斥锁)
  • 特点:
    • 读读共享( 最大特性 )、读写互斥、写写互斥。
    • 当一个线程拥有写锁时,不释放写锁的情况下,再占有读锁,此时写锁会被 降级 为读锁.
    • 在公平模式下 , 无论读锁还是写锁的申请都需要按照 先进先出 的原则, 非公平模式下 , 写锁无条件插队.
  • 场景:常用于 读多写少 的场景,即请求读操作的线程 多而频繁 而请求写操作的线程极 少且间隔长 , !!!但在读写都相当频繁的场景并不能体现出性能优势
    • 缺点: 基于 读读共享(不互斥) 的特性极有可能造成 写线程饥饿 。比如, R1线程 此时持有读锁且在进行读取操作, W1线程 请求写锁所以需要排队等候,在 R1 释放锁之前,如果 R2,R3,...,Rn 不断的到来请求读锁,因为读读共享,所以他们不用等待马上可以获得锁,如此下去 W1永远无法获得写锁,一直处于饥饿状态 。
public class CachedData {
 
 	//声明读写锁实例
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private Object data;
    private volatile boolean cacheValid;

    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();
        }
    }
}

2.3.StampedLock

StampedLock是JDK1.8新增的一种 读写锁 ,是对 ReentrantReadWriteLock 的增强版,提供 2种读模式 : 乐观读和悲观读 。

  • 乐观读 允许读的过程中也可以获取写锁后写入! 这样一来,我们读的数据就可能不一致, 因此需要一点额外的代码来判断读的过程中是否有写入。
  • 乐观锁的意思就是 乐观地认为读的过程中大概率不会有写入 ,因此被称为乐观锁。反过来,悲观锁则是 读的过程中拒绝有写入操作 ,也就是 写入必须等待 。显然乐观锁的并发效率更高,但 一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行 。
public class Point {
 
    //声明锁实例
    private final StampedLock stampedLock = new StampedLock();
    //横坐标
    private double x;
    //纵坐标
    private double y;

    /**
     * 移动坐标
     * @param deltaX
     * @param deltaY
     */
    public void move(double deltaX, double deltaY) {
 
    	//获取写锁
        long stamp = stampedLock.writeLock();
        try {
 
            x += deltaX;
            y += deltaY;
        } finally {
 
        	//释放写锁
            stampedLock.unlockWrite(stamp);
        }
    }

    /**
     * 计算距离原点平台根
     * @return
     */
    public double distanceFromOrigin() {
 
        // 获得一个乐观读锁
        long stamp = stampedLock.tryOptimisticRead();

        // 注意下面两行代码不是原子操作
        // 假设x,y = (100,200)
        double currentX = x;
        // 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
        double currentY = y;
        // 此处已读取到y,如果没有写入,读取是正确的(100,200)
        // 如果有写入,读取是错误的(100,400)

        // 检查乐观读锁后是否有其他写锁发生
        if (!stampedLock.validate(stamp)) {
 
            // 获取一个悲观读锁
            stamp = stampedLock.readLock();
            try {
 
                currentX = x;
                currentY = y;
            } finally {
 
                // 释放悲观读锁
                stampedLock.unlockRead(stamp);
            }
        }

        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

2.4.Condition

Condition是在 JDK1.5加入concurrent包 的,用来替代传统的 Object的wait()、notify() 实现线程间的通信,相比使用Object的wait()、notify(),使用Condition的 await()、signal() 方法实现线程间通信更加安全和高效。 通过Lock+Condition组合使用可以实现 等待通知机制 阻塞队列BlockQueue 实际上是使用了 Condition 来进实现线程通信的。

  • await() 必须在获取锁之后的调用 ,表示释放当前锁,阻塞当前线程;等待其他线程调用锁的 signal()或signalAll() 唤醒线程重新去获取锁。
  • Lock配合Condition ,可以实现 synchronized 与 对象的 wait(),notify()、notifyAll() 同样的效果 ,来进行线程间基于共享变量的通信。 但优势在于同一个锁可以由多个条件队列,当某个条件满足时,只需要唤醒对应的条件队列即可,避免无效的竞争。
  • 调用await()和signal()方法,都必须在lock保护之内,就是说 必须在lock.lock()和lock.unlock之间才可以使用

使用 ReentrantLock+Condition 实现一个简单的阻塞队列

public class BoundedBuffer {
 
    //声明可重入锁
    final Lock lock = new ReentrantLock();
    //如果队列满时用于挂起put线程/唤醒take线程的Condition
    final Condition notFull = lock.newCondition();
    //如果队列为空时用于挂起take线程/唤醒put线程的Condition
    final Condition notEmpty = lock.newCondition();
    //存储元素的容器
    final Object[] items = new Object[100];
    int putptr, takeptr, count; // 进队元素索引,出队元素索引 ,元素总量

    /**
     * 添加元素
     *
     * @param x
     * @throws InterruptedException
     */
    public void put(Object x) throws InterruptedException {
 
        //获取锁
        lock.lock();
        try {
 
            //元素总量等于容器大小(容器满了)
            while (count == items.length) {
 
                //挂起put元素线程
                notFull.await();
            }

            //---------容器未满--------
            //当前位置putptr插入元素
            items[putptr] = x;

            //如果当前取的元素索引 等于 容器大小,从对首开始存
            if (++putptr == items.length) {
 
                putptr = 0;
            }
            //元素总量自增
            ++count;
            //唤醒take元素线程
            notEmpty.signal();
        } finally {
 
            //释放锁
            lock.unlock();
        }
    }

    /**
     * 获取元素
     *
     * @return
     * @throws InterruptedException
     */
    public Object take() throws InterruptedException {
 
        //获取锁
        lock.lock();
        try {
 
            //元素总量等于0(为空满了)
            while (count == 0) {
 
                //挂起take元素线程
                notEmpty.await();
            }

            //---------容器不为空--------
            //获取队首元素
            Object x = items[takeptr];

            //如果当前取的元素索引 等于 容器大小,从对首开始取
            if (++takeptr == items.length) {
 
                takeptr = 0;
            }

            //元素总量自减
            --count;

            //唤醒put元素线程
            notFull.signal();
            return x;
        } finally {
 
            //释放锁
            lock.unlock();
        }
    }
}

测试

public static void main(String[] args) {
 
        BoundedBuffer buffer = new BoundedBuffer();

        new Thread(() -> {
 
            try {
 
                while (true) {
 
                    int item = new Random().nextInt(10000);
                    buffer.put(item);
                    log.info("生产者:{}生产了一个元素:{}", Thread.currentThread().getName(),item);
                    TimeUnit.MILLISECONDS.sleep(100);
                }
            } catch (InterruptedException e) {
 
                e.printStackTrace();
            }
        }, "producer").start();


        new Thread(() -> {
 
            try {
 
                while (true) {
 
                    Object item = buffer.take();
                    log.info("消费者:{}消费了一个元素:{}", Thread.currentThread().getName(),item);
                    TimeUnit.MILLISECONDS.sleep(200);
                }
            } catch (InterruptedException e) {
 
                e.printStackTrace();
            }
        }, "consumer1").start();


        new Thread(() -> {
 
            try {
 
                while (true) {
 
                    Object item = buffer.take();
                    log.info("消费者:{}消费了一个元素:{}", Thread.currentThread().getName(),item);
                    TimeUnit.MILLISECONDS.sleep(200);
                }
            } catch (InterruptedException e) {
 
                e.printStackTrace();
            }
        }, "consumer2").start();
    }

拓展·

  • Condition 实现
  • 每个 Condition 对象包含一个等待队列。等待队列中的节点复用了同步器中同步队列中的节点。 Condition 对象的 await()和 signal()操作就是对等待队列以及同步队列的操作。
    • await()操作 :将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,最后进入等待状态。
    • signal()操作 :会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前会将节点移到同步队列中。唤醒后的节点尝试竞争锁 (自旋) 。

2.5.BlockingQueue

BlockingQueue阻塞队列实际上是一个 生产者/消费者模型 ,当队列长度大于指定的最大值,生产线程就会被阻塞;反之当队列元素为空时,消费线程就会被阻塞;同时当消费成功时,就会唤醒阻塞的生产者线程;生产成功就会唤醒消费者线程;

  • 实现原理: 内部使用就是 ReentrantLock + Condition 来实现的可以参照上面的 BoundedBuffer 示例。

2.6.CountDownLatch

俗称: (同步计数器/闭锁) ,可以使 一个线程等待其他线程全部执行完毕后再执行。 类似join()的效果。

  • 声明对象时需要初始化需要等待线程数 ,调用 countDown() 方法等待线程数减1,当数值减为0时 ,就会唤醒所有因为调用 await() 方法而阻塞的线程。
    • 可以达到一组线程等待另外一组线程都执行完成的效果。

2.7.CyclicBarrier

俗称: 同步屏障 ,可以使 一组线程互相等待,“直到所有线程到达某个公共的执行点后在继续执行”

  • 声明该对象时需要初始化等待线程数 ,调用 await() 方法会使得线程阻塞,直到 指定数量的线程都调用await方法时,所有被阻塞的线程会被唤醒,继续执行 。

与CountDownLatch的区别是:CountDownLatch是 一组线程等待另外一组线程执行完在执行,而CyclicBarrier是 一组线程之间相互等待,直到所有线程执行到某个点在执行 。

2.8.Semaphore

称之为 信号量 ,与 互斥锁ReentrantLock用法类似 ,区别就是Semaphore 共享的资源是多个,允许多个线程同时竞争成功。

2.9.初识AQS原理

AQS 是 AbstractQueuedSynchronizer 的缩写,中文为 抽象队列同步器 ,是用来 构建各类锁和同步器的基础实现 。内部维护了 共享变量state (int类型) 和 双向队列 (包含头指针和尾指针)

基于AQS构建的同步器

  • ReentrantLock
  • ReentrantReadWriteLock
  • Semaphore
  • CountDownLatch
  • SynchronusQueue
  • FutureTask

AQS模型如下图:

1.AQS并发问题解决方案

原子性

  • 内部通过 Unsafe.compareAndSwapXXX 实现 CAS 更新 state 和 队列指针 (prev/next/head/tail)
  • 内部依赖CPU提供的原子指令

可见性与有序性

  • volatile 修饰 state 与 队列指针 (prev/next/head/tail)

线程阻塞与唤醒

  • Unsafe.park
  • Unsafe.parkNanos
  • Unsafe.unpark

Unsafe类是在 sun.misc包 下,不属于Java标准。提供了 内存管理、对象实例化、数组操作、CAS操作、线程挂起与恢复等功能 ,Unsafe类提升了Java运行效率,增强了Java语言底层的操作能力。很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等

AQS内部有2种模式: 独占模式(独占锁)和共享模式(共享锁)

  • AQS 的设计是基于 模板方法 的,使用者需要 继承 AQS 并重写指定的方法 。不同的自定义队列同步器竞争共享资源的方式不同,比如 可重入、公平性等都是子类来实现。
  • 自定义队列同步器在实现时 只需要实现共享资源state的获取与释放方式即可 ,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),由AQS 内部 实现。

2.独占模式

一个线程
唤醒后继节点

AQS提供的独占模式相关的方法

// 获取独占锁(线程阻塞直至获取成功)
public final void acquire(int arg)
// 获取独占锁,可被中断
public final void acquireInterruptibly(int arg)
// 获取独占锁,可被中断 和 指定超时时间
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
// 释放独占锁(释放锁后,将等待队列中第一个等待节点唤醒 )
public final boolean release(int arg)

AQS子类需要实现的独占模式相关的方法

// 尝试获取独占锁,成功则返回true,失败则返回false。
protected boolean tryAcquire(int arg)
// 尝试释放独占锁,成功则返回true,失败则返回false。
protected boolean tryRelease(int arg)

获取独占锁的流程

  • 调用子类 tryAcquire 尝试获取锁,获取成功,直接返回

  • 通过自旋CAS将当前线程封装成节点加入 队列末尾

  • 循环等待或尝试 tryAcquire 获取锁

    • 判断前置节点如果为 head ,则尝试获取锁
    • 根据队列中节点状态,决定是否需要阻塞当前线程
    • tryAcquire 获取锁成功后,将当前节点设置为 head 并 返回
  • 如果当前线程中断或超时,则执行 cancelAcquire

    CANCELED
    Head
    

释放独占锁的流程

3.共享模式

  • 多个线程 都能够获取到锁
  • 锁释放后需要 唤醒后继节点
  • 锁获取后如果 还有资源 ,需要 唤醒后继共享节点

AQS提供的共享模式相关的方法

// 获取共享锁(线程阻塞直至获取成功)
public final void acquireShared(int arg)
// 获取共享锁,可被中断
public final acquireSharedInterruptibly(int arg)
// 获取共享锁,可被中断 和 指定超时时间
public final tryAcquireSharedNanos(int arg, long nanosTimeout)
// 获取共享锁
public final boolean releaseShared(int arg)

AQS子类需要实现的共享模式相关的方法

// 尝试获取共享锁。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected int tryAcquireShared(int arg)
// 尝试释放共享锁,如果释放后允许唤醒后续等待节点返回true,否则返回false。
protected boolean tryReleaseShared(int arg)

获取共享锁的流程

  • 调用子类 tryAcquireShared 尝试获取锁,获取成功,直接返回

  • 通过自旋CAS将当前线程封装成节点加入 队列末尾

  • 循环等待或尝试 tryAcquireShared 获取锁

    • 判断前置节点如果为 head ,则尝试获取锁
    • 根据队列中节点状态,决定是否需要阻塞当前线程
    • tryAcquireShared 获取锁成功后,将当前节点设置为 head
      • 如果资源有 剩余 或者原先的 head 节点状态为 SIGNAL / PROPAGATE ,则调用 doReleaseShared
      • 如果当前head节点状态为 SIGNAL ,唤醒后继节点
      • 如果当前head节点状态为 ZERO ,将 head 节点状态置为 PROPAGATE
  • 如果当前线程中断或超时,则执行 cancelAcquire

    CANCELED
    Head
    

释放共享锁的流程

等待队列中节点的状态变化

4.ReentrantLock示例

tryAcquire逻辑

tryRelease逻辑

四.总结

  • 对于 单机环境 我们在 JVM内进行并发控制我们可以使用 synchronized (内置锁) 和 RentrantLock 。
  • 对于 自增 或者 原子数据累加 我们可以使用 Unsafe 提供的原子类,比如 AtomicInteger , AtomicLong
  • 对于数据库的话,对于用户金额扣除的场景我们可以使用 乐观锁(版本号) 的方式来进行控制
    update table_name set amount = 100, version = version + 1 where id = 1 and version = 1;
  • 对于 分布环境景 下可以使用 Redis 或者 Zk 实现 分布式锁 。实现分布式场景下的并发控制。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值