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中声明释放锁。
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,就能明确的指定唤醒读线程。
以上是关于5-2 AQS应用(组件)的主要内容,如果未能解决你的问题,请参考以下文章