栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

文章目录
  • 一.前言
    • 了解高并发必须知道的概念
    • 了解Java并发包Concurrent发展简述
    • 了解锁的概念
    • 线程安全三大特性
    • 自旋锁
  • 二.内置锁-synchronized
  • 三.显示锁-Lock
    • 1.Lock特性
      • 1.1.显示加锁、解锁
      • 1.1.可重入
      • 1.2.可响应中断
      • 1.3.可设置等待超时时间
      • 1.4.锁的公平性
      • 1.5.读写锁
      • 1.6.丰富的API
      • 1.7.常用方法
    • 2.锁的使用
      • 2.1.ReentrantLock
      • 2.2.ReentrantReadWriteLock
      • 2.3.StampedLock
      • 2.4.Condition
      • 2.5.BlockingQueue
      • 2.6.CountDownLatch
      • 2.7.CyclicBarrier
      • 2.8.Semaphore
      • 2.9.初识AQS原理
        • 1.AQS并发问题解决方案
        • 2.独占模式
        • 3.共享模式
        • 4.ReentrantLock示例
  • 四.总结

JUC架构图

一.前言

并发编程最佳学习路线

【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
  • 内置锁通过文章【Java多线程】内置锁(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(读锁)。 读锁可以允许多个不同线程重入,但对于写锁,同时只能有一个线程重入, 把读和写操作分离开来了,粒度更细,所以在性能上有所提高. 并且写锁可降级为读锁,反之不行。

  • 特点:
    • 读读共享(最大特性)、读写互斥、写写互斥。
    • 当一个线程拥有写锁时,不释放写锁的情况下,再占有读锁,此时写锁会被降级为读锁.
    • 在公平模式下,无论读锁还是写锁的申请都需要按照先进先出的原则,非公平模式下,写锁无条件插队.
  • 场景:常用于读多写少的场景,即请求读操作的线程多而频繁而请求写操作的线程极少且间隔长 ,!!!但在读写都相当频繁的场景并不能体现出性能优势
    • 缺点: 基于读读共享(不互斥)的特性极有可能造成写线程饥饿。比如,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;

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

    
    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; // 进队元素索引,出队元素索引 ,元素总量

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

    
    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模型如下图:

1.AQS并发问题解决方案

原子性

  • 内部通过Unsafe.compareAndSwapXXX 实现CAS更新 state 和 队列指针
  • 内部依赖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)
// 获取独占锁,可被中断
public final void acquireInterruptibly(int) 
// 获取独占锁,可被中断 和 指定超时时间
public final boolean tryAcquireNanos(int, long) 
// 释放独占锁(释放锁后,将等待队列中第一个等待节点唤醒 )
public final boolean release(int) 

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

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

获取独占锁的流程

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

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

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

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

    • 将当前节点状态置为CANCELED,并从队列删除
    • 如果前置节点为Head,则将后置节点唤醒

释放独占锁的流程

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

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

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

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

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

获取共享锁的流程

  • 调用子类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实现分布式锁。实现分布式场景下的并发控制。

网络好文

GOOD-纯锁–不可不说的Java“锁”事(CAS)
GOOD-Java中的锁分类与使用

GOOD-聊聊 Java 的几把 JVM 级锁
GOOD-队列同步器(AQS)
如何根据不同的业务场景,来选择合适的锁?


JUC之AQS中的CLH队列
JUC系列 - AQS CLH同步队列
并发之AQS原理(二) CLH队列与Node解析
JUC–AQS源码分析(一)CLH同步队列


【高并发专题】-高并发下前后端常用解决方案总结(全套)
【高并发专题】-java线程安全-线程安全编程规约

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/584297.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号