• 并发与高并发(十三)J.U.C之AQS


    前言

    什么是AQS,是AbstractQueuedSynchronizer类的简称。J.U.C大大提高了并发的性能,而AQS又是J.U.S的核心。

    主体概要

    • J.U.C之AQS介绍
    • J.U.C之AQS-CountDownLatch
    • J.U.C之AQS-Semaphore
    • J.U.C之AQS-CyclicBarrier
    • J.U.C之AQS-ReentrantLock与锁

    主体内容

    一、J.U.C之AQS介绍

    1.AbstractQueuedSynchronizer简称AQS

    AbstractQueuedSynchronizer是J.U.C(java.util.concurrent)中的重中之重。

    (1)我们看一下底层的数据结构

    解释一下:

    底层使用的是双向链表,是队列的一种实现,因此我们可以把它当做一个队列。其中Sync queue同步队列是双向链表,包括(head、tail)节点,head节点主要用于后期的调度。而Condition queue不是必须的,它是一个单向链表,只有当使用到Condition queue的时候才会存在这个单向链表,并且可能会有多个Condition queue。

    (2)接下来我们看一下AQS的设计

    • 使用Node实现FIFO队列,可以用于构建锁或者其他同步装置的基础框架。
    • 利用了一个int类型表示状态
    • 使用方法是继承(使用的时候需要继承AQS,并复写其中的方法)
    • 子类通过继承并通过实现它的方法管理其状态(aquire和release)的方法操纵状态
    • 可以同时实现排它锁和共享锁模式(独占、共享)

    这里介绍一下AQS大致实现的思路:

    首先,AQS内部维护了一个CLH(Craig,Landin,and Hagersten)队列来管理锁,线程会首先尝试获取锁,如果失败,就向当前线程以等待状态为信息包成一个lock节点加入到同步队列Sync Queue队列,接下来会不断循环尝试获取锁,它的条件是当前节点为head的直接后继才会尝试,如果失败就会阻塞自己,直到自己被唤醒,而当持有锁的线程释放锁的时候,会唤醒队列中的后继线程。

    (3)JDK为我们提供了许多AQS子类的同步组件。

    • CountDownLatch(通过计数来保证线程是否一直需要阻塞)
    • Semaphore(控制同一时间并发的线程数)
    • CyclicBarrier
    • ReetrantLock
    • Condition
    • FutureTask

    本章以下将详细介绍这几个类。

     二、J.U.C之AQS-CountDownLatch

    1.CountDownLatch是一个同步辅助类,通过它可以完成类似线程阻塞的功能,简单来说,就是让一个线程等待其他线程执行完成。CountDownLatch使用一个给定的计数器进行初始化,该计数器的操作是原子性的操作,就是同时只有一个线程可以执行该计数器。用该类的await()方法就可以让调用它的线程一直处于阻塞状态,当其他线程调用countDown()方法,每次计数减一,当计数值等于0的时候,因调用await()方法的阻塞线程就会继续往下执行。

    2.CountDownLatch使用场景(敬豪大帅哥看这里)

    (1)并行计算

    当某个处理运算量很大时,可以将该运算拆分成多个子任务,等待所有的子任务完成之后,父任务拿到所有子任务的运算结果进行汇总

    (2)举个例子,循环创建200个线程,分别执行每次循环的次数输出值,吗,每次线程执行完调用countDown(),计数值减一,最后主线程调用await()方法,意思是,主线程需要等待线程池创建的200个线程全部执行完毕,才可以继续执行。

    import lombok.extern.slf4j.Slf4j;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    @Slf4j
    public class CountDownLatchExample1 {
        private final static int threadCount= 200;//线程数
        private final static CountDownLatch countDownLatch= new CountDownLatch(threadCount);
    
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            for(int i=0;i<threadCount;i++){
                final int threadNum=i;
                executorService.execute(()->{
                    try {
                        test(threadNum);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }finally {
                        countDownLatch.countDown();
                    }
                });
            }
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("finish");
         executorService.shutdown();
    } 

    public static void test(int threadNum) throws Exception{
      Thread.sleep(
    100); log.info("test-{}",threadNum); Thread.sleep(100);
     }
    }

    所以,结果是

    省略其他结果值...
    ...
    00:25:24.082 [pool-1-thread-153] INFO com.practice.aqs.CountDownLatchExample1 - test-152
    00:25:24.082 [pool-1-thread-162] INFO com.practice.aqs.CountDownLatchExample1 - test-161
    00:25:24.069 [pool-1-thread-25] INFO com.practice.aqs.CountDownLatchExample1 - test-24
    00:25:24.088 [pool-1-thread-185] INFO com.practice.aqs.CountDownLatchExample1 - test-184
    00:25:24.069 [pool-1-thread-21] INFO com.practice.aqs.CountDownLatchExample1 - test-20
    00:25:24.068 [pool-1-thread-31] INFO com.practice.aqs.CountDownLatchExample1 - test-30
    00:25:24.073 [pool-1-thread-78] INFO com.practice.aqs.CountDownLatchExample1 - test-77
    00:25:24.073 [pool-1-thread-52] INFO com.practice.aqs.CountDownLatchExample1 - test-51
    00:25:24.077 [pool-1-thread-112] INFO com.practice.aqs.CountDownLatchExample1 - test-111
    00:25:24.225 [main] INFO com.practice.aqs.CountDownLatchExample1 - finish

     (3)上面是一个比较简单的例子,那我们来看一个复杂一点的例子,我现在想要给每个线程执行各自任务的时间有限,超过这个时间的结果不要了。假设我给所有线程10毫秒的时间执行,让每个线程睡上100毫秒,那么所有线程都赶不上这个时间,主线程睡10毫秒后继续执行。例子如下:

    import lombok.extern.slf4j.Slf4j;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    
    @Slf4j
    public class CountDownLatchExample2 {
        private final static int threadCount= 200;//线程数
        private final static CountDownLatch countDownLatch= new CountDownLatch(threadCount);
    
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            for(int i=0;i<threadCount;i++){
                final int threadNum=i;
                executorService.execute(()->{
                    try {
                        test(threadNum);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }finally {
                        countDownLatch.countDown();
                    }
                });
            }
            try {
                countDownLatch.await(10, TimeUnit.MILLISECONDS);//10毫秒结束等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("finish");
    
        }
    
        public static void test(int threadNum) throws Exception{
            Thread.sleep(100);
            log.info("test-{}",threadNum);
        }
    }

    结果:

    00:38:59.331 [main] INFO com.practice.aqs.CountDownLatchExample2 - finish
    00:38:59.397 [pool-1-thread-28] INFO com.practice.aqs.CountDownLatchExample2 - test-27
    00:38:59.395 [pool-1-thread-7] INFO com.practice.aqs.CountDownLatchExample2 - test-6
    00:38:59.397 [pool-1-thread-23] INFO com.practice.aqs.CountDownLatchExample2 - test-22
    00:38:59.397 [pool-1-thread-25] INFO com.practice.aqs.CountDownLatchExample2 - test-24
    00:38:59.396 [pool-1-thread-13] INFO com.practice.aqs.CountDownLatchExample2 - test-12
    00:38:59.397 [pool-1-thread-27] INFO com.practice.aqs.CountDownLatchExample2 - test-26
    00:38:59.395 [pool-1-thread-4] INFO com.practice.aqs.CountDownLatchExample2 - test-3
    ...
    以下省略...

    发现finish先输出了,那为什么线程会紧随其后继续输出呢?原因是线程池的shutdown方法调用后,并不是所有线程都被销毁了,而是允许他们执行完。

    以上就是CountDownLatch的两个小例子。

     三、J.U.C之AQS-Semaphore

    1.Semaphore(信号量),可以控制某个资源可被同时访问的线程个数。Semaphore也提供了两个方法,分别是aquire()和release()方法。aquire()是获取一个许可,如果没有,则等待。而release()方法则是在操作之后是否一个许可。Semaphore通过同步机制来控制同时访问的个数。

    2.使用场景:比如数据库连接池允许的最大连接数为10,如果同时超过10个线程访问数据库资源,则会导致异常,此时就需要信号量来控制。

    3.接下来,举一个小栗子来演示一下Semaphore的用法。

    mport lombok.extern.slf4j.Slf4j;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    @Slf4j
    public class SemaphoreExample1 {
        private final static int threadCount = 200;
        private final static Semaphore semaphore = new Semaphore(20);//同时允许20个线程执行
    
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            for(int i=0;i<threadCount;i++){
                final int threadNum =i;
                executorService.execute(()->{
                    try {
                        semaphore.acquire();//获取一个许可
                        test(threadNum);
                        semaphore.release();//释放一个许可
                    } catch (InterruptedException e) {
                        log.info("exception",e);
                    }
                });
            }
            executorService.shutdown();
            log.info("finish");
        }
        public static void test(int threadNum){
            log.info("test-{}",threadNum);
        }
    }

    结果:

    省略...
    22
    :07:14.467 [pool-1-thread-47] INFO com.practice.aqs.SemaphoreExample1 - test-46 22:07:14.467 [pool-1-thread-48] INFO com.practice.aqs.SemaphoreExample1 - test-47 22:07:14.467 [pool-1-thread-105] INFO com.practice.aqs.SemaphoreExample1 - test-172 22:07:14.467 [pool-1-thread-49] INFO com.practice.aqs.SemaphoreExample1 - test-48 22:07:14.468 [pool-1-thread-111] INFO com.practice.aqs.SemaphoreExample1 - test-182 22:07:14.467 [pool-1-thread-35] INFO com.practice.aqs.SemaphoreExample1 - test-170 22:07:14.469 [main] INFO com.practice.aqs.SemaphoreExample1 - finish 22:07:14.469 [pool-1-thread-65] INFO com.practice.aqs.SemaphoreExample1 - test-64 22:07:14.469 [pool-1-thread-62] INFO com.practice.aqs.SemaphoreExample1 - test-61 22:07:14.467 [pool-1-thread-37] INFO com.practice.aqs.SemaphoreExample1 - test-176 22:07:14.467 [pool-1-thread-51] INFO com.practice.aqs.SemaphoreExample1 - test-50 22:07:14.467 [pool-1-thread-52] INFO com.practice.aqs.SemaphoreExample1 - test-51
    省略...

    4.有时候我们需要多个许可,那么又该如何写呢?其实aquire()也提供了参数。

    直接在这两方法中加入参数3,这样就是获取3个许可,然后我让执行test()的每个线程睡个1秒。

    semaphore.acquire(3);
    test(threadNum);
    semaphore.release(3);

    结果如下:

     

     它会像这样等待3个许可全部被释放,才会执行下一组,所以结果看起来像是一块一块的执行。

     6.像这样,还有一种更复杂点的场景,如果现在我想超过信号量5的结果丢弃,如何完成呢?且看以下的例子。

    import lombok.extern.slf4j.Slf4j;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    @Slf4j
    public class SemaphoreExample1 {
        private final static int threadCount = 200;
        private final static Semaphore semaphore = new Semaphore(3);//同时允许3个线程执行
    
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            for(int i=0;i<threadCount;i++){
                final int threadNum =i;
                executorService.execute(()->{
                    try {
                        if(semaphore.tryAcquire()) {
                            test(threadNum);
                            semaphore.release();
                        }
                    } catch (Exception e) {
                        log.info("exception",e);
                    }
                });
            }
            executorService.shutdown();
            log.info("finish");
        }
        public static void test(int threadNum) throws Exception{
            log.info("test-{}",threadNum);
            Thread.sleep(1000);
        }
    }

    结果:

    22:30:26.356 [main] INFO com.practice.aqs.SemaphoreExample1 - finish
    22:30:26.356 [pool-1-thread-6] INFO com.practice.aqs.SemaphoreExample1 - test-5
    22:30:26.356 [pool-1-thread-1] INFO com.practice.aqs.SemaphoreExample1 - test-0
    22:30:26.356 [pool-1-thread-3] INFO com.practice.aqs.SemaphoreExample1 - test-2
    
    Process finished with exit code 0

    可见只有三个线程获得了许可。

    我们详细看一下tryAquire()的几个方法。

    (1)tryAquire() --boolean

    (2)tryAquire(int) --boolean 一次性获取多少个许可,如果获取不到即丢弃

    (3)tryAquire(long,TimeUnit) --boolean long:超时时间 TimeUnit:时间单位 意思是如果我获取许可可以最大在long时间内,如果超过long,则放弃获取许可。

    (4)tryAquire(int,long,TimeUnit) --boolean 相当于以上两个函数的结合。

    我还是举一个小例子吧!仅需要将以上的代码修改一处即可。

    if(semaphore.tryAcquire()) {
           test(threadNum);
           semaphore.release();
    }

    修改为

     if(semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS)) {
           test(threadNum);
           semaphore.release();
    }

    结果只有在5秒内获取到许可的线程们执行了,而且是3个一组,成块状执行:

     以上就是Semaphore的讲解,后续有待补充。

     四、J.U.C之AQS-CyclicBarrier

    1.CyclicBarrirer也是一个同步辅助类,它允许一组线程持续等待,直到到达某个工作屏障点。通过它可以完成多个线程相互等待,只有当每个线程都准备就绪后,才能各自继续往下执行后面的操作。它和CountDownLatch有相类似的地方,都是通过计数器来实现的,当某个线程调用了await()方法后,该线程就进入等待状态,注意,这里的计数器是执行+1操作,当计数器值达到我们设置的初始值时候,因为调用await()方法的线程会被唤醒,继续执行他们自己后续的操作。由于CyclicBarrier在释放等待线程后可以重用,我们又称之为循环屏障。

    2.CyclicBarrier和CountDownLatch的使用场景十分相似,它可以用于多线程合并最终计算结果。

    3.简单讲一下CyclicBarrier和CountDownLatch的区别

    (1)CountDownLatch的计数器只能使用一次,而CyclicBarrier可以使用reset()方法重置,可以循环使用。

    (2)CountDownLatch主要是实现一个或多个线程需要等待其他线程完成某项操作之后,才能继续往下执行,它描述的是一个或N个线程等待其他线程的关系。而CyclicBarrier主要是实现多个线程之间相互等待,直到所有线程满足条件后,才能执行后续的操作,它描述的是多个线程之间相互等待的关系。

    4.先列出一个小例子,展示先它的用法。

    import lombok.extern.slf4j.Slf4j;
    import java.util.concurrent.CyclicBarrier;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    @Slf4j
    public class CyclicBarrierExample1 {
        private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);//当屏障内的线程突破5个时才允许其继续执行
        public static void main(String[] args) throws Exception{
            ExecutorService executorService = Executors.newCachedThreadPool();
            for(int i=0;i<10;i++){
                final int threadNum = i;
                Thread.sleep(1000);
                executorService.execute(()->{
                    try {
                        race();
                    } catch (Exception e) {
                        log.error("exection",e);
                    }
                });
            }
         executorService.shutdown(); }
    public static void race() throws Exception{ Thread.sleep(1000); log.info("i'm ready "); cyclicBarrier.await(); log.info("i'm finished"); } }

    结果每5个线程ready了,才会执行紧接着的finish:

     5.CyclicBarrier.await();方法其实是可以放入时间参数的,也就是等待多久。但是直接加上时间往往会出现异常,我们需要将其异常捕捉,才能保证下面的任务会继续执行。

    例如,我将以上的cyclicBarrier.await();修改为:cyclicBarrier.await(2000,TimeUnit.MILLISECONDS);也就是让它等2000毫秒,如果超过就不等了,直接让屏障内未超时的线程继续向下执行。

    最后的结果图如下:

     6.最后,关于CyclicBarrier还有个例子,可以在线程达到屏障数的时候,可以指定一个线程,让它优先执行指定的线程。

    只需要改下CyclicBarrier的定义。

     private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5,()->{
            log.info("callback is running!!!");
        });//当屏障内的线程突破5个时才允许其继续执行

    结果会如下所示:

     五、J.U.C之AQS-ReentrantLock与锁

    1.这里重新复习一下Java里的锁,一种是之前提到的synchronized锁,一种是J.U.C里面提供的锁,即ReentrantLock。

    简要讲一下ReentrantLock(可重入锁)与synchronized的区别

    (1)可重入性

    (2)锁的实现

    synchronized是依赖于JVM实现的,而ReentrantLock是基于JDK实现的,具体区别相当于操作系统控制锁和用户自己控制锁的区别。

    (3)性能的区别

    在之前,synchronized锁性能相对于ReentrantLock是很差的,但后期synchronized引入了偏向锁,轻量锁后,他们两个的性能就差不多了。官方目前建议使用synchronized锁,因为它的写法比较容易。

    (4)功能区别

    synchronized可以自动加锁,释放锁。而ReentrantLock是需要手动加锁,释放锁,为了避免忘记释放锁而造成死锁,这里建议写在finally中释放。

    锁的细粒度和灵活度方面,ReentrantLock是优于synchronized的。

    ReentrantLock拥有自己独立的功能:

    • 可指定是公平锁还是非公平锁,而synchronized只能是非公平锁(所谓公平锁就是先等待的线程先获得锁)
    • 提供了一个Condition类,可以分组唤醒需要唤醒的线程
    • 提供能够中断等待锁的线程的机制,lock.lockInterruptibly()

    那么既然ReentrantLock比synchronized更全面,是不是应该舍弃synchronized呢?非也,java.util.concurrent是面向高级用户的,当有明确的需求或证据需要用到ReentrantLock特性的时候,才会建议使用ReentrantLock。主要建议synchronized是因为如果因为使用了ReentrantLock一旦忘记释放锁,后期项目测试无问题上线了出现了问题,很难定位到是没有释放锁的原因,所以建议初学者使用synchronized。

    2.下面利用最初的例子,来演示一下ReentrantLock的基本用法。

    import lombok.extern.slf4j.Slf4j;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    @Slf4j
    public class ReentrantLockExample1 {
        private final static int clientCount=5000;
        private final static int threadCount = 50;
        private static int count =0;
    
        private final static Lock lock = new ReentrantLock();
    
    
        public static void main(String[] args) {
            CountDownLatch countDownLatch = new CountDownLatch(clientCount);
            Semaphore semaphore = new Semaphore(threadCount);
            ExecutorService executorService = Executors.newCachedThreadPool();
            for(int i=0;i<clientCount;i++){
               executorService.execute(()->{
                   try {
                       semaphore.acquire();
                       test();
                       semaphore.release();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   countDownLatch.countDown();
               });
            }
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            executorService.shutdown();
            log.info("count:{}",count);
        }
        public static void test(){
            lock.lock();
            try {
                count++;
            } finally {
                lock.unlock();
            }
        }
    }

    结果:

    22:55:05.999 [main] INFO com.practice.aqs.ReentrantLockExample1 - count:5000
    Process finished with exit code 0

    3.像我们刚刚讲的,ReentrantLock拥有许多特性,实际上它提供了很多方法来实现这些特性。如

    lockInterruptibly():如果当前线程没有被中断的话,那么就获取锁,如果已经被中断,就抛出异常。

    isLocked():查询此锁定是否有任意线程保持。

    isHeldByCurrentThread():查询当前线程是否保持锁定状态。

    isFair():判断是不是公平锁。

    getHoldCount():查询当前线程保持锁定的个数。

    ...

    4.接下来,我们要说一下另外一个锁叫做ReentrantReadWriteLock,在没有任何读锁的时候,才可以取得写入锁,这个要求这个类的核心。

    @since 1.5
     * @author Doug Lea
     */
    public class ReentrantReadWriteLock
            implements ReadWriteLock, java.io.Serializable {
        private static final long serialVersionUID = -6992448646407690164L;
        /** Inner class providing readlock */
        private final ReentrantReadWriteLock.ReadLock readerLock;
        /** Inner class providing writelock */
        private final ReentrantReadWriteLock.WriteLock writerLock;
        /** Performs all synchronization mechanics */
        final Sync sync;

    这个ReentrantReadWriteLock实现了悲观读取,即如果我们执行中进行读取时,经常可能有另一个执行要写入的需求。为了保持同步ReentrantReadWriteLock的读取锁定就可以派上用场了。然而读取机会很多,写入很少的情况下,使用ReentrantReadWriteLock可能会造成写入线程遭遇饥饿,即写入线程一直处于等待状态。文字可能有点难懂,还是写一段代码演示一波。

    import lombok.extern.slf4j.Slf4j;
    import java.util.Map;
    import java.util.Set;
    import java.util.TreeMap;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    @Slf4j
    public class ReetrantReadWriteLockExample1 {
        private final Map<String,Data> map = new TreeMap<>();//定义一个map用于读写
        private final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();//定义ReentrantReadWriteLock
        private final Lock readLock = reentrantReadWriteLock.readLock();//定义读锁
        private final Lock wirteLock = reentrantReadWriteLock.writeLock();//定义写锁
        /**
         * 读取map
         * @param key
         * @return
         */
        public Data get(String key){
            readLock.lock();
            try {
                return map.get(key);
            } finally {
                readLock.unlock();
            }
    
        }
        /**
         * 获取所有的key
         * @return
         */
        public Set<String> getAllKeys(){
            readLock.lock();
            try {
                return map.keySet();
            } finally {
                readLock.unlock();
            }
        }
    
        /**
         * 写入map
         * @param key
         * @param value
         * @return
         */
        public Data put(String key,Data value){
            wirteLock.lock();
            try {
                return map.put(key,value);
            } finally {
                wirteLock.unlock();
            }
        }
        class Data{//这里声明一个内部类
    
        }
    
    }

    为什么说写线程容易遭遇饥饿呢?原因就是有线程不停的读,导致写永远难以执行,以上就是ReentrantReadWriteLock的简单介绍,后续有待补充。这个类其实应用场景很少,只要知道了解就可以了,不过有兴趣的话可以详细搜索一下这个类的用法。

    5.这里再介绍一个锁,叫做StampedLock。它控制锁有三种模式:读、写、乐观读,重点在于这个乐观读上。一个StampedLock是由版本和模式两个部分组成。锁获取方法返回的是一个数字作为票据,它由相关的锁状态来控制并发线程的访问。在读锁上分为悲观锁和乐观锁。所谓乐观读,其实就是如果读的操作很多,写的操作很少的情况下,我们可以乐观的认为读的操作和写的操作同时发生的几率很小。

    以下是StampedLock内部注释中提供的一个例子。已表明中文注释,可以试着理解一下。

     class Point {
            private double x, y;
            private final StampedLock sl = new StampedLock();
    
            void move(double deltaX, double deltaY) { // an exclusively locked method
                long stamp = sl.writeLock();
                try {
                    x += deltaX;
                    y += deltaY;
                } finally {
                    sl.unlockWrite(stamp);
                }
            }
            //下面看看乐观锁案例
            double distanceFromOrigin() { // A read-only method
                long stamp = sl.tryOptimisticRead();//获得一个乐观读锁
                double currentX = x, currentY = y;//将两个字段读入本地局部变量
                if (!sl.validate(stamp)) {//检查发出乐观读锁后同时是否有其他锁发生?
                    stamp = sl.readLock();//如果没有,我们再次获得一个读悲观锁
                    try {
                        currentX = x;//将两个字段读入本地局部变量
                        currentY = y;//将两个字段读入本地局部变量
                    } finally {
                        sl.unlockRead(stamp);
                    }
                }
                return Math.sqrt(currentX * currentX + currentY * currentY);
            }
            //以下是悲观读锁案例
            void moveIfAtOrigin(double newX, double newY) { // upgrade
                // Could instead start with optimistic, not read mode
                long stamp = sl.readLock();
                try {
                    while (x == 0.0 && y == 0.0) {//循环,检查当前状态是否符合
                        long ws = sl.tryConvertToWriteLock(stamp);//将读锁转为写锁
                        if (ws != 0L) {//这是确认写锁是否成功
                            stamp = ws;//如果成功,替换票据
                            x = newX;//进行状态改变
                            y = newY;//进行状态改变
                            break;
                        } else {//如果不能成功,转为写锁
                            sl.unlockRead(stamp);//我们显式的释放读锁
                            stamp = sl.writeLock();//显式直接进行写锁 然后通过循环再试
                        }
                    }
                } finally {
                    sl.unlock(stamp);//释放读锁或写锁
                }
            }
        }

    这里来个栗子演示一波StampedLock的用法。

    import lombok.extern.slf4j.Slf4j;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.locks.StampedLock;
    
    @Slf4j
    public class StampedLockExample1 {
        private final static int clientCount = 5000;
        private final static int threadCount = 50;
        private static int count =0;
        private final static CountDownLatch countDownLatch = new CountDownLatch(clientCount);
        private final static Semaphore semaphore = new Semaphore(threadCount);
        private final static StampedLock stampedLock = new StampedLock();//定义stampedLock
    
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            for(int i=0;i<clientCount;i++){
                try {
                    semaphore.acquire();
                    test();
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                countDownLatch.countDown();
            }
            try {
                countDownLatch.await();
                log.info("count:{}",count);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                executorService.shutdown();
            }
        }
        public static void test(){
            long stamped = stampedLock.writeLock();//定义写锁,这时候他会返回一个stamped值
            try {
                count++;
            } finally {
                stampedLock.unlock(stamped);//释放的时候带上这个票据stamped
            }
        }
    }

    结果自然是没有问题的,其实看中的是它在这种读线程多写线程少的场景下性能好的特点:

    22:00:33.261 [main] INFO com.practice.aqs.StampedLockExample1 - count:5000
    
    Process finished with exit code 0

    6.接下来看一下Condition这个类,直接看个例子。

    mport lombok.extern.slf4j.Slf4j;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.ReentrantLock;
    @Slf4j
    public class ConditionExample {
        public static void main(String[] args) {
            ReentrantLock reentrantLock = new ReentrantLock();//首先我们定义了一个reentrantLock
            Condition condition = reentrantLock.newCondition();//其次从reentrantLock里取出了condition
    
            new Thread(()->{//线程1
                try {
                    reentrantLock.lock();//线程1调用了reentrantLock的lock()方法,这个线程就加入到了AQS的等待队列里面去了
                    log.info("wait signal");//1
                    condition.await();//当线程1调用condition.await()方法时就从AQS等待队列里移除了,对应的操作其实就是锁的释放,接着它马上又加入了condition的等待队列里面去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("get signal");//4
                reentrantLock.unlock();
            }).start();
    
            new Thread(()->{//线程2因为判断线程1释放后被唤醒
                    reentrantLock.lock();//同样的,线程2加入到了AQS的等待队列中
                    log.info("get lock");//2
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                condition.signalAll();//发送信号,这时候condition的等待队列里面有我们线程1的节点,于是它(线程1)就被取出来加入到AQS的等待队列中,注意,此时线程1还没被唤醒
                log.info("send signal");//3
                reentrantLock.unlock();//当执行到这一步时,线程2释放锁,线程1被唤醒,于是上面的线程1继续开始执行,输出get signal
            }).start();
        }
    }

    结果:

    22:21:43.774 [Thread-0] INFO com.practice.aqs.ConditionExample - wait signal
    22:21:43.777 [Thread-1] INFO com.practice.aqs.ConditionExample - get lock
    22:21:46.777 [Thread-1] INFO com.practice.aqs.ConditionExample - send signal
    22:21:46.777 [Thread-0] INFO com.practice.aqs.ConditionExample - get signal
    
    Process finished with exit code 0

    例子中已经标明了程序的输出顺序。

    平时的时候大家可能Condition用的地方会很少,有兴趣的话可以继续研究一下。

    总结

    我们来总结一下这一章涉及的锁的类,第一个是synchronized,发生异常的时候,JVM会自动释放锁。而ReentrantLock,ReentrantReadWriteLock,StampedLock它们三都是对象层面的锁定,要保证锁一定会被释放,就必须把unlock放到finally里面执行。StampedLock对吞吐量有巨大的改进,特别是读线程很多写较少的场景下。

    这里涉及的锁还是比较多的,那么何时该应用什么锁呢?

    1.当只有少量竞争者的时候(少量竞争线程的时候),synchronized是一个很好的通用锁实现。

    2.竞争者比较多的时候,但是线程数增长趋势我们是可以预估的时候,ReentrantLock是一个很好的通用锁实现。

    在这里要注意一点,除了synchronized是会被JVM自动释放的,而其他锁一旦没有被手动释放,是会发生死锁的。

  • 相关阅读:
    逻辑思维杂想
    C++二叉树实现
    斐波那数列递归实现与动态规划实现
    C++双向链表的实现
    C++单链表实现
    C++顺序表实现
    windows下端口占用处理工具
    [项目记录]一个.net下使用HAP实现的吉大校园通知网爬虫工具:OAWebScraping
    [c++]大数运算---利用C++ string实现任意长度正小数、整数之间的加减法
    [C++]几种排序
  • 原文地址:https://www.cnblogs.com/xusp/p/12337891.html
Copyright © 2020-2023  润新知