并发与高并发(十三)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自动释放的,而其他锁一旦没有被手动释放,是会发生死锁的。

posted @ 2020-02-20 22:38  mcbbss  阅读(382)  评论(0编辑  收藏  举报