5-2 AQS应用(组件)

Posted qmillet

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了5-2 AQS应用(组件)相关的知识,希望对你有一定的参考价值。

本章内容:

  1.CountDownLatch

  2.CyclicBarrier

  3.Semaphore

  4.ReentrantLock


一、CountDownLatch

  CountDownLatch类使用AQS同步状态来表示计数。当该计数为0时,所有的acquire操作(对应到CountDownLatch中就是await方法)才能通过。通过CountDownLatch可以实现类似计数器的功能。必有一个任务A,他要等待其他四个任务执行完才能执行,此时就可以使用CountDownLatch。

//构造器
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

//调用await的线程会被挂起,直到count的值为0时才会被执行
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

//等待一段时间,不管count是否为0,都会被执行
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

//count值减一
public void countDown() {
    sync.releaseShared(1);
}

 举例技术图片

 

 

 

二、CyclicBarrier(回环栏栅)

  通过CyclicBarrier可以实现让一组线程等待至某个状态之后再全部执行。当所有等待线程都被释放以后,CyclicBarrier可以被重用,我们将这个状态记为barrier,当调用await()之后,线程便会处于barrier状态

 1 //构造器 参数parties是指让多少个线程等待到barrier状态。
 2 public CyclicBarrier(int parties) {}
 3 
 4 //构造器 barrierAction指当parties个线程等待到barrier状态时,在执行的后续任务,
 5 //先于线程动作执行
 6 public CyclicBarrier(int parties, Runnable barrierAction) {}
 7 
 8 //用于挂起线程,知道所有线程达到barrier状态,再执行后续任务
 9 public int await() throws InterruptedException, BrokenBarrierException {}
10 
11 //让线程等待一段时间,如果还未有线程达到barrier状态,就先让达到barrier状态的线程先//执行
12 public int await(long timeout, TimeUnit unit)
13 throws InterruptedException,BrokenBarrierException,TimeoutException {}

 举例

 技术图片技术图片

 

 技术图片技术图片

 

 

 

 三、Semaphore(信号量)

  通过Semaphore可以控制同时访问线程的个数,通过acquire()获取一个许可,如果没有则等待,而replace()是释放一个许可。举例:假如一个工厂有5台机器,8个工人,一台机器只能被一人使用,只有使用完其他人才能使用。可设置许可数量为5,线程数量为8.

 1 //构造器1 参数permits表示许可数量,即同时可以允许多少线程可以访问
 2 public Semaphore(int permits) {}
 3 
 4 //构造器2 fair表示是否公平的,即等待时间越久越先获取许可
 5 public Semaphore(int permits, boolean fair) {}
 6 
 7 //获取一个或者多个许可
 8 public void acquire() throws InterruptedException {}
 9 public void acquire(int permits) throws InterruptedException {}//一个线程获取多个许可
10 
11 //释放一个或者多个许可
12 public void release() {}
13 public void release(int permits) {}
14 //上面四个方法会导致阻塞,一直在获取状态
15 
16 //尝试获取许可,如果成功返回true,如果失败返回false
17 public boolean tryAcquire() {}
18 //若在指定时间内获取返回true,反之false
19 public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException{}
20 //尝试获取多个许可,如果成功返回true,如果失败返回false
21 public boolean tryAcquire(int permits) {}

 

举例:

技术图片技术图片

 

 

 

 

 

 

 

 

 

 

☆★☆★☆★CountDownLatch、CyclicBarrier、Semaphore总结☆★☆★☆★

①CountDownLatch和CyclicBarrier都能实现线程之间的等待,只不过侧重点不同:CountDownLatch用于某个线程等待其他若干个线程执行完之后才执行,不可重用。CyclicBarrier适用于一组进程互相等待至某个状态,然后同时执行,可重用。

②Semaphore和锁类似,一般用于控制对某组资源的访问权限。

 

四、ReentantLock(可重入锁)

1.ReentantLock和synchronized总结

  ①可重入性:从名字上理解,ReenTrantLock的字?意思就是再进?的锁,其实synchronized关键字所使?的锁也是可重?的,两者关于这个的区别不?。两者都是同?个线程每进??次,锁的计数器都?增1,所以要等到锁的计数器下降为0时才能释放锁。

  ②锁的实现:Synchronized是依赖于JVM实现的,?ReenTrantLock是JDK实现的,说?了就类似于操作系统来控制实现和?户??敲代码实现的区别。前者的实现是?较难?到的,后者有直接的源码可供阅读。

  ③性能的区别:在Synchronized优化以前,synchronized的性能是?ReenTrantLock差很多的,但是?从Synchronized引?了偏向锁,轻量级锁(?旋锁)后,两者的性能就差不多了,在两种?法都可?的情况下,官?甚?建议使?synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在?户态就把加锁问题解决,避免进?内核态的线程

  ④功能区别:

    便利性:很明显Synchronized的使??较?便简洁,并且由编译器去保证锁的加锁和释放,?ReenTrantLock需要??声明来加锁和释放锁,为了避免忘记??释放锁造成死锁,所以最好在finally中声明释放锁。

技术图片

 

    锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized。

2.ReentantLock特有的能力(为什么使用ReenTrantLock?应用场景)

  ①ReenTrantLock可以指定是公平锁还是?公平锁。?synchronized只能是?公平锁。所谓的公平锁就是先等待的线程先获得锁。

  ②ReenTrantLock提供了?个Condition(条件)类,?来实现分组唤醒需要唤醒的线程们,?不是像synchronized要么随机唤醒?个线程要么唤醒全部线程。

  ③ReenTrantLock提供了?种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

3.ReentantLock实现原理

  简单来说,ReenTrantLock的实现是?种?旋锁,通过循环调?CAS操作来实现加锁。它的性能?较好也是因为避免了使线程进?内核态的阻塞状态。想尽办法避免线程进?内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

4.ReentrantReadWriteLock(读写锁)

  ①概念:JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,?个是读操作相关的锁,称为共享锁;?个是写相关的锁,称为排他锁,描述如下:

  线程进入读锁(共享锁)的前提条件:没有其他线程的写锁、没有写请求或者只用一个持有锁的线程执行写操作。

  线程进入写锁(排他锁)的前提条件:没有其他线程的读锁、没有其他线程的写锁。

  ②使用场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读?个资源没有任何问题,所以应该允许多个线程同时读取共享资源(读锁,共享锁);但是如果?个线程想去写这些共享资源,就不应该允许其他线程对该资源进?读和写的操作了(写锁,排他锁)。

  ③读写锁重要特性

    Ⅰ.公平选择性:?持?公平(默认)和公平的锁获取?式,吞吐量还是?公平优于公平。

    Ⅱ.重进?:读锁和写锁都?持线程重进?。

    Ⅲ.锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁

  实例API:

 1 public class LockExample3 {
 2     private final Map<String, Data> map = new TreeMap<>();
 3     private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
 4     private final Lock readLock = lock.readLock();
 5     private final Lock writeLock = lock.writeLock();
 6     //和ReentrantLock一样需要手动加锁解锁。
 7     public Data get(String key) {
 8         readLock.lock();
 9         try {
10             return map.get(key);
11         } finally {
12             readLock.unlock();
13         }
14     }
15 
16     public Data put(String key, Data value) {
17         writeLock.lock();
18         try {
19             return map.put(key, value);
20         } finally {
21             readLock.unlock();
22         }
23     }
24     class Data {}
25 }

 

 

5.StampedLock

  StampedLock是Java 8中引?的?种新的锁机制。读写锁虽然分离了读和写的功能,使得读与读之间可以完全并发。但是,读和写之间依然是冲突的。读锁会完全阻塞写锁,它使?的依然是悲观锁的策略,如果有?量的读线程,它也有可能引起写线程的“饥饿”。?StampedLock提供了?种乐观的读策略。这种乐观策略的锁?常类似?锁的操作,使得乐观锁完全不会阻塞写线程。

  拓展:

  【悲观锁】当我们要对一个数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制。

  悲观锁主要是共享锁或排他锁

  • 共享锁又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
  • 排他锁又称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。

  【乐观锁】乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。

 1 package com.mmall.concurrency.example.lock;
 2 
 3 import java.util.concurrent.locks.StampedLock;
 4 
 5 public class LockExample4 {
 6 
 7     class Point {
 8         private double x, y;
 9         private final StampedLock sl = new StampedLock();
10 
11         void move(double deltaX, double deltaY) { // an exclusively locked method
12             long stamp = sl.writeLock();
13             try {
14                 x += deltaX;
15                 y += deltaY;
16             } finally {
17                 sl.unlockWrite(stamp);
18             }
19         }
20 
21         //下面看看乐观读锁案例
22         double distanceFromOrigin() { // A read-only method
23             long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
24             double currentX = x, currentY = y;  //将两个字段读入本地局部变量
25             if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?
26                 stamp = sl.readLock();  //如果没有,我们再次获得一个读悲观锁
27                 try {
28                     currentX = x; // 将两个字段读入本地局部变量
29                     currentY = y; // 将两个字段读入本地局部变量
30                 } finally {
31                     sl.unlockRead(stamp);
32                 }
33             }
34             return Math.sqrt(currentX * currentX + currentY * currentY);
35         }
36 
37         //下面是悲观读锁案例
38         void moveIfAtOrigin(double newX, double newY) { // upgrade
39             // Could instead start with optimistic, not read mode
40             long stamp = sl.readLock();
41             try {
42                 while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
43                     long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
44                     if (ws != 0L) { //这是确认转为写锁是否成功
45                         stamp = ws; //如果成功 替换票据
46                         x = newX; //进行状态改变
47                         y = newY;  //进行状态改变
48                         break;
49                     } else { //如果不能成功转换为写锁
50                         sl.unlockRead(stamp);  //我们显式释放读锁
51                         stamp = sl.writeLock();  //显式直接进行写锁 然后再通过循环再试
52                     }
53                 }
54             } finally {
55                 sl.unlock(stamp); //释放读锁或写锁
56             }
57         }
58     }
59 }

 

 

6.Condition

  Condition 将 Object 监视器?法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使?,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized ?法和语句的使?,Condition 替代了Object 监视器?法的使?。在Condition中,?await()替换wait(),?signal()替换notify(),?signalAll()替换notifyAll(),传统线程的通信?式,Condition都可以实现,这?注意,Condition是被绑定到Lock上的,要创建?个Lock的Condition必须?newCondition()?法。

  这样看来,Condition和传统的线程通信没什么区别,Condition的强?之处在于它可以为多个线程间建?不同的Condition

  Condition与Object中的wati,notify,notifyAll区别

  1.Condition中的await()?法相当于Object的wait()?法,Condition中的signal()?法相当于Object的notify()?法,Condition中的signalAll()相当于Object的notifyAll()?法。不同的是,Object中的这些?法是和同步锁捆绑使?的;?Condition是需要与互斥锁/共享锁捆绑使?的

  2.Condition它更强?的地?在于:能够更加精细的控制多线程的休眠与唤醒对于同?个锁,我们可以创建多个Condition,在不同的情况下使?不同的Condition。例如,假如多线程读/写同?个缓冲区:当向缓冲区中写?数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";并且当缓冲区满的时候,"写线程"需要等待;当缓冲区为空时,"读线程"需要等待。如果采?Object类中的wait(),notify(),notifyAll()实现该缓冲区,当向缓冲区写?数据之后需要唤醒"读线程"时,不可能通过notify()或notifyAll()明确的指定唤醒"读线程",?只能通过notifyAll唤醒所有线程(但是notifyAll?法区分唤醒的线程是读线程,还是写线程)。 但是,通过Condition,就能明确的指定唤醒读线程。

  (?产者-消费者(producer-consumer)问题,也称作有界缓冲区(bounded-buffer)问题,两个进程共享?个公共的固定??的缓冲区。其中?个是?产者,?于将消息放?缓冲区;另外?个是消费者,?于从缓冲区中取出消息。问题出现在当缓冲区已经满了,?此时?产者还想向其中放??个新的数据项的情形,其解决?法是让?产者此时进?休眠,等待消费者从缓冲区中取?了?个或者多个数据后再去唤醒它。同样地,当缓冲区已经空了,?消费者还想去取消息,此时也可以让消费者进?休眠,等待?产者放入一个或者多个数据时再唤醒它。)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

以上是关于5-2 AQS应用(组件)的主要内容,如果未能解决你的问题,请参考以下文章

AQS 原理以及 AQS 同步组件总结

AQS 同步组件学习

AQS的几个同步组件

聊聊高并发(二十四)解析java.util.concurrent各个组件 深入理解AQS

原来 AQS实现原理还能如此总结

AQS