一、AQS并发工具类
1.CountDownLatch:倒计时门栓
应用场景:
A synchronization aid that allows one or more threads to wait until
a set of operations being performed in other threads completes.
(大意就是:可以控制一个或者多个特定的线程在若干个线程执行完毕之后才执行)
来个土味社会语录:先穿袜子再穿鞋,先当孙子再当爷~
这里的穿鞋,必须等到所有袜子穿好之后,才可以进行。同理,想成为大佬,假设必须经过一千次向大佬请教。那么想成为大佬就必须等到请教一千次大佬动作完成之后才能进行。 类似这种需求可以考虑使用CountDownLatch实现
CountDownLatch的构造器: 参数为计数器的初始值
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
举例:
下面用穿袜子来举例,必须穿好两只袜子才可以穿鞋。
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(2);
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 2; i++) {
service.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t穿上了一只袜子");
countDownLatch.countDown();//计数器减一,减到零之后,调用了await()方法而在等待的线程就会被唤醒。
});
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t穿鞋");
service.shutdown();
}
}
运行结果:
pool-1-thread-1 穿上了一只袜子
pool-1-thread-2 穿上了一只袜子
main 穿鞋
若没有使用CountDownLatch,可能出现:
main 穿鞋
pool-1-thread-2 穿上了一只袜子
pool-1-thread-1 穿上了一只袜子
或者:
pool-1-thread-1 穿上了一只袜子
main 穿鞋
pool-1-thread-2 穿上了一只袜子
这样先穿了鞋再穿袜子,明显错误。
原理:
每次CountDownLatch对象的countDown()方法,就会使设定好的计数器的值减一(类似于倒计时),若设定好的计数器减到零(倒计时结束,await()的线程放行):那么调用了CountDownLatch对象的await()方法的线程就会执行。
总结:
//实例化方式
CountDownLatch countDownLatch = new CountDownLatch(int count);
//主要方法
countDownLatch.countDown();//计数器count减一
countDownLatch.await();//线程等待,直到计数器count归零
2.CyclicBarrier:循环障碍
应用场景:
A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. CyclicBarriers are useful in programs involving a fixed sized party of threads that must occasionally wait for each other. The barrier is called cyclic because it can be re-used after the waiting threads are released.
和CountDownLatch类似:也是一些线程要等待另一些线程执行完毕后,才能执行的场景。
与CountDownLatch不同的是:CyclicBarirer可以通过reset()方法再次循环使用,并不是像CountDownLatch一样用过就当垃圾丢掉的。
CyclicBarrier的构造器:
public CyclicBarrier(int parties, Runnable barrierAction)
//parties:the number of threads that must invoke {@link #await} before the barrier is tripped(在障碍物消失之前,必须执行的线程的个数,即执行第二个参数中的线程之前必须执行的线程的数量)
//the command to execute when the barrier is tripped(障碍物消失之后,要执行的命令)
举例:
葫芦娃合体,必须等七个葫芦娃全都准备就绪才可以合体
public class CyclicBarrierDemo {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("葫芦娃合体!");
});
for (int i = 1; i <=7 ; i++) {
final int temp = i;
service.execute(() -> {
System.out.println(Thread.currentThread().getName()+"\t"+temp+"娃来了,准备合体");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
}
service.shutdown();
}
}
运行结果:
pool-1-thread-1 1娃来了,准备合体
pool-1-thread-5 5娃来了,准备合体
pool-1-thread-2 2娃来了,准备合体
pool-1-thread-4 4娃来了,准备合体
pool-1-thread-3 3娃来了,准备合体
pool-1-thread-7 7娃来了,准备合体
pool-1-thread-6 6娃来了,准备合体
葫芦娃合体!
原理:
同样通过计数器来计算执行过的线程数,只不过每次变为加1而不是减1。每调用一次CyclicBarrier对象的await()方法,标示着一个线程到达了屏障,计数器加一。
当计数器的值等于入参中的parties的值时,barrierAction会执行一次。
总结:
//实例化方式
CyclicBarrier cyclicBarrier = new CyclicBarrier(int parties, Runnable barrierAction);
//主要方法
cyclicBarrier.await();//计数器加一,计数器的值等于parties时,barrierAction执行
3.Semaphore:
应用场景:
A counting semaphore. Conceptually, a semaphore maintains a set of permits. Each {@link #acquire} blocks if necessary until a permit is
available, and then takes it. Each {@link #release} adds a permit,
potentially releasing a blocking acquirer.
However, no actual permit objects are used; the {@code Semaphore} just keeps a count of the number available and acts accordingly.
如果有些资源数量是有限的,但是需要这个资源的线程数量比资源数量多,那么为了资源的并发安全,我们可以使用Semaphore ,最多只允许等于资源数的线程数量进入,并且使用完资源可以释放使用权给其他线程(release),其他线程一旦进入就占领一个资源(acquire)
Semaphore的构造器:
public Semaphore(int permits)
//permits:最大资源数量的初始值,不能为负数
举例 :
这里用食堂抢桌子座位来模拟,食堂一个桌子有四个座位,十个人要吃饭。要保证一个座位一个人,不能有四个人以上同时吃饭,并且吃完释放座位给其他人用。
public class SemaphoreDemo {
//多个线程抢多个资源
public static void main(String[] args) {
//八个人食堂抢座位,最多四个人入座,不关心是谁
Semaphore semaphore = new Semaphore(4);
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
service.execute(() -> {
try {
semaphore.acquire();//perimit不为0时,才能进来,每进来一个线程permit减1
System.out.println(Thread.currentThread().getName()+"抢到了一个座位");
//暂停一会线程以便观察
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"吃完饭了,离开了座位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//吃完饭释放座位
semaphore.release();//每跑完一个线程permit加1
}
});
}
service.shutdown();
}
}
运行结果:
pool-1-thread-1抢到了一个座位
pool-1-thread-2抢到了一个座位
pool-1-thread-3抢到了一个座位
pool-1-thread-4抢到了一个座位
pool-1-thread-2吃完饭了,离开了座位
pool-1-thread-3吃完饭了,离开了座位
pool-1-thread-1吃完饭了,离开了座位
pool-1-thread-4吃完饭了,离开了座位
pool-1-thread-9抢到了一个座位
pool-1-thread-10抢到了一个座位
pool-1-thread-6抢到了一个座位
pool-1-thread-5抢到了一个座位
pool-1-thread-9吃完饭了,离开了座位
pool-1-thread-5吃完饭了,离开了座位
pool-1-thread-10吃完饭了,离开了座位
pool-1-thread-6吃完饭了,离开了座位
pool-1-thread-8抢到了一个座位
pool-1-thread-7抢到了一个座位
pool-1-thread-7吃完饭了,离开了座位
pool-1-thread-8吃完饭了,离开了座位
原理:
创建 Semaphore 对象的时候,需要一个参数 permits,这个permits代表最大的资源数,然后每个线程调用 acquire 的时候,执行 permits = permits - 1,release 的时候执行 permits = permits + 1,当然,acquire 的时候,如果 permits = 0,说明没有资源了,需要等待其他线程 release。
总结
//实例化方式
Semaphore semaphore = new semaphore(int permits);
//主要方法
semaphore.aquire(); //permit不为零的情况下,占领一个资源,permit-1
semaphore.release();//释放一个资源,permit+1
二、读写锁 ReadWriteLock
特点:
读写锁的特点是给读写操作的线程进行定制化处理,给每一个写操作的线程赋予唯一一把write锁。但是对只读操作的read锁可以由多个线程获得。
举例:
5个线程负责向一个map写入数据,另外五个线程负责从map读数据:
给读方法内加上read锁,给写方法内加上write锁。
public class ReadWriteLockDemo {
//5个写线程,5个读线程
public static void main(String[] args) {
CacheMethods cacheMethods = new CacheMethods();
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
final int temp = i;
service.execute(() -> {
cacheMethods.write(temp+"", temp+"");
});
}
for (int i = 0; i < 5; i++) {
final int temp = i;
service.execute(() -> {
cacheMethods.read(temp+"");
});
}
service.shutdown();
}
}
class CacheMethods{
private volatile Map<String, Object> map = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void write(String key, Object value){
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" : 写入数据 : "+key);
//模拟网路延迟写入数据暂停一会
TimeUnit.MILLISECONDS.sleep(300);
map.put(key, value);
System.out.println(Thread.currentThread().getName()+" : 写入完成");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();
}
}
public void read(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" : 读取数据");
//模拟网路延迟读取数据暂停一会
TimeUnit.MILLISECONDS.sleep(300);
Object result = map.get(key);
System.out.println(Thread.currentThread().getName()+" : 读取完成 : " + result);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
readWriteLock.readLock().unlock();
}
}
}
运行结果:
pool-1-thread-1 : 写入数据 : 0
pool-1-thread-1 : 写入完成
pool-1-thread-3 : 写入数据 : 2
pool-1-thread-3 : 写入完成
pool-1-thread-2 : 写入数据 : 1
pool-1-thread-2 : 写入完成
pool-1-thread-4 : 写入数据 : 3
pool-1-thread-4 : 写入完成
pool-1-thread-5 : 写入数据 : 4
pool-1-thread-5 : 写入完成
pool-1-thread-6 : 读取数据
pool-1-thread-7 : 读取数据
pool-1-thread-8 : 读取数据
pool-1-thread-9 : 读取数据
pool-1-thread-10 : 读取数据
pool-1-thread-9 : 读取完成 : 3
pool-1-thread-10 : 读取完成 : 4
pool-1-thread-6 : 读取完成 : 0
pool-1-thread-7 : 读取完成 : 1
pool-1-thread-8 : 读取完成 : 2
可以看出,写操作在进行的时候,其他的写操作并没有进来。读写锁实现了并发下读写分离的定制化处理。
总结:
//实例化方法
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//读操作的加锁和释放
readWriteLock.writeLock().lock();
readWriteLock.writeLock().unlock();
//写操作的加锁和释放
readWriteLock.readLcok().lock();
readWriteLock.readLcok().unlock();
三、阻塞队列BlockingQueue
阻塞队列的概念:
所谓阻塞队列,其实本身就是数据结构概念中的队列,遵循先入队的元素先出队。所谓阻塞,就是如果队列是有界的,那么在队列元素已满时,入队操作将会被阻塞;同理,在队列中没有任何元素时,出队操作将会被阻塞。
BlockingQueue的继承关系:
BlockingQueue接口的主要实现类:
-
ArrayBlockingQueue :底层是数组,有界队列,如果我们要使用生产者-消费者模式,这是非常好的选择。
-
LinkedBlockingQueue :底层是链表,可以当做无界和有界队列来使用,所以大家不要以为它就是无界队列。
-
SynchronousQueue :本身不带有空间来存储任何元素,使用上可以选择公平模式和非公平模式。
-
PriorityBlockingQueue: 是无界队列,基于数组,数据结构为二叉堆,数组第一个也是树的根节点总是最小值。
阻塞队列的入队,出队,peek方法:
具体方法可见如下表格:
-
Throws exception:假设指定了阻塞队列的size(),若add()超过size数量的元素,会直接报
java.lang.IllegalStateException: Queue full
。同理,若队列无元素时调用了remove(),会报java.util.NoSuchElementException
的错误,调用element(),会报java.util.NoSuchElementException
。 -
Special value:同理,若执行上面描述中会报错的行为,这里的方法不会报异常,而会返回特定的值:offer返回true或者false;poll返回出队的元素或者null,peek返回队列最先一个能出队的元素或者null.
-
Blocks:是实现了真正的阻塞队列的概念,在队列元素已满时,入队操作将会被阻塞;同理,在队列中没有任何元素时,出队操作将会被阻塞。不会返回任何值,程序会等待其他操作来结束阻塞状态。所以所有方法当中比较重要的是【put(),take()】这两个方法。
-
Times out:阻塞等待此操作,直到成功或者超时指定时间。时间到达后返回的类型规则同Special value。
举例:
下面贴一个小的demo来验证,方法太多结果就不一一贴了,在每个方法的注释处有运行的结果:
public class BlockQueueDemo {
public static void main(String[] args) throws InterruptedException {
//指定队列的容量为3
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
// System.out.println(blockingQueue.add("a"));
// System.out.println(blockingQueue.add("b"));
// System.out.println(blockingQueue.add("c"));
System.out.println(blockingQueue.add("d"));//java.lang.IllegalStateException: Queue full
//
// System.out.println(blockingQueue.remove());//a
// System.out.println(blockingQueue.remove());//b
// System.out.println(blockingQueue.remove());//c
System.out.println(blockingQueue.remove());//java.util.NoSuchElementException
// System.out.println(blockingQueue.add("a"));
// System.out.println(blockingQueue.add("b"));
// System.out.println(blockingQueue.element());//a,取队列能最先出队的元素,相当于数据结构队列中的peek
// System.out.println(blockingQueue.offer("a"));
// System.out.println(blockingQueue.offer("b"));
// System.out.println(blockingQueue.offer("c"));
// System.out.println(blockingQueue.offer("d"));//不报异常,但是返回false
// System.out.println(blockingQueue.poll());//a
// System.out.println(blockingQueue.poll());//b
// System.out.println(blockingQueue.poll());//c
// System.out.println(blockingQueue.poll());//null
blockingQueue.put("a");
blockingQueue.put("a");
blockingQueue.put("a");
blockingQueue.put("a");//一直等待入队,被阻塞。不报异常,也没有返回值
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());//一直等待出队,被阻塞。不报异常,也没有返回值
// System.out.println(blockingQueue.offer("a"));
// System.out.println(blockingQueue.offer("b"));
// System.out.println(blockingQueue.offer("c"));
// System.out.println(blockingQueue.offer("a", 3L, TimeUnit.SECONDS));//3秒后输出false
}
}