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)问题,两个进程共享⼀个公共的固定⼤⼩的缓冲区。其中⼀个是⽣产者,⽤于将消息放⼊缓冲区;另外⼀个是消费者,⽤于从缓冲区中取出消息。问题出现在当缓冲区已经满了,⽽此时⽣产者还想向其中放⼊⼀个新的数据项的情形,其解决⽅法是让⽣产者此时进⾏休眠,等待消费者从缓冲区中取⾛了⼀个或者多个数据后再去唤醒它。同样地,当缓冲区已经空了,⽽消费者还想去取消息,此时也可以让消费者进⾏休眠,等待⽣产者放入一个或者多个数据时再唤醒它。)

 

 

 

 

 

 

 

 

原文地址:https://www.cnblogs.com/qmillet/p/12088512.html