该篇学习自我非常喜欢的博主四火
同时用代码来学习和理解,整理到我的github项目中了。
阻塞队列
名称 | 功能 |
---|---|
BlockingQueue.class | 阻塞队列接口 |
BlockingDeque.class | 双端阻塞队列接口 |
ArrayBlockingQueue.class | 阻塞队列,数组实现 |
LinkedBlockingDeque.class | 阻塞双端队列,链表实现 |
LinkedBlockingQueue.class | 阻塞队列,链表实现 |
DelayQueue.class | 阻塞队列,并且元素是 Delay 的子类,保证元素在达到一定时间后才可以取得到 |
PriorityBlockingQueue.class | 优先级阻塞队列 |
SynchronousQueue.class | 同步队列,但是队列长度为 0,生产者放入队列的操作会被阻塞,直到消费者过来取,所以这个队列根本不需要空间存放元素;有点像一个独木桥,一次只能一人通过,还不能在桥上停留 |
ArrayBlockingQueue 阻塞队列(数组结构)
阻塞队列最主要的特点是阻塞,当队列没元素时获取会阻塞,当队列元素满了添加会阻塞。
举例:这里在队列为空时获取,结果应当是阻塞,我们用poll来获取元素并设置3秒的时限,并打印出前后时间证明确实阻塞了(虽然肉眼可见)。
public class ArrayBlockingQueueDemo {
final static int TRY_TIME = 3;
public static void main(String[] args) throws InterruptedException {
BlockingQueue blockingQueue = new ArrayBlockingQueue(5);
long beforeTime = System.currentTimeMillis();
blockingQueue.poll(TRY_TIME, TimeUnit.SECONDS);
System.out.println((System.currentTimeMillis()-beforeTime)/1000+"秒");
}
}
LinkedBlockingQueue阻塞队列(链表结构)
链表实现的阻塞队列相比于数组实现的阻塞队列最大优势就是插入删除效率高
举例:我们分别进行10次插入,5次删除,1000个循环,比较两种数据结构的速度
public class LinkedBlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue<>();
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(500005);
long before_time_linked = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
for (int j = 0; j < 10; j++) {
linkedBlockingQueue.put(1);
}
for (int j = 0; j < 5; j++) {
linkedBlockingQueue.take();
}
}
System.out.println((System.currentTimeMillis()-before_time_linked) + "毫秒");
long before_time_array = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
for (int j = 0; j < 10; j++) {
arrayBlockingQueue.put(1);
}
for (int j = 0; j < 5; j++) {
arrayBlockingQueue.take();
}
}
System.out.println((System.currentTimeMillis()-before_time_array) + "毫秒");
}
}
DelayQueue 延迟队列
延迟队列是比较特殊的一种,它对队列元素有要求,元素必须实现Delayed接口。而且队列本身是有序的。
接口方法一getDelay()需要返回剩余过期时间,这个值到0则过期并可以返回。
接口方法二compareTo()就是实现内部排序的,必须让过期元素排在队首才能返回,若队首没过期则返回null。
应用场景:定时任务调度。
public class DelayQueueDemo {
public static void main(String[] args) {
DelayQueue- delayQueue = new DelayQueue
- ();
Item item1 = new Item(3,TimeUnit.SECONDS);
Item item2 = new Item(6,TimeUnit.SECONDS);
delayQueue.add(item1);
delayQueue.add(item2);
while(true){
Item item = delayQueue.poll();
if(item==null)
break;
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(item.time)));
}
}
static class Item implements Delayed {
private long time;
public Item(long time, TimeUnit unit) {
this.time = unit.toMillis(time) + System.currentTimeMillis();
}
/**
* item的剩余到期时间
* @param unit
* @return 返回值小于等于0才可以被返回(表示过期)
*/
@Override
public long getDelay(TimeUnit unit) {
return System.currentTimeMillis() - time;
}
/**
* 比较剩余到期时间,越小越靠近队列头部
* @param o
* @return
*/
@Override
public int compareTo(Delayed o) {
Item item = (Item) o;
long diff = this.time - item.time;
if (diff <= 0) {// 改成>=会造成问题
return -1;
} else {
return 1;
}
}
}
}
SynchronousQueue 同步队列
同步队列,可以把它想像成长度为1的ArrayBlockingQueue,但是实际上它的容量大小为0,必须有一个take才能put进去。
举例:次线程睡五秒take一个元素,主线程put一个元素但是得一直等5s到take执行后才能顺利进行。
public class SynchronousQueueDemo {
public static void main(String[] args) throws InterruptedException {
SynchronousQueue queue = new SynchronousQueue();
System.out.println("放入一个元素");
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("五秒后take一个元素");
Thread.sleep(5000);
queue.take();
System.out.println("take完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
System.out.println("开始put,但因为没有take所以阻塞了");
queue.put(1);
System.out.println("put结束");
System.out.println("主线程顺利执行到此,结束");
}
}
非阻塞队列
它们与阻塞队列的不同之处在于底层由CAS实现线程安全,吞吐量大,并发环境表现优异。
名称 | 功能 |
---|---|
ConcurrentLinkedDeque.class | 非阻塞双端队列,链表实现 |
ConcurrentLinkedQueue.class | 非阻塞队列,链表实现 |
转移队列
名称 | 功能 |
---|---|
TransferQueue.class | 转移队列接口,生产者要等消费者消费的队列,生产者尝试把元素直接转移给消费者 |
LinkedTransferQueue.class | 转移队列的链表实现,它比 SynchronousQueue 更快 |
TransferQueue 转移队列
转移队列,它和SynchronousQueue很像,不同的是它可以用tryTransfer方法来尝试转移元素,所以在操作流程上有所不同。
举例:次线程睡五秒take一个元素,主线程循环转移一个元素。
public class TransferQueueDemo {
public static void main(String[] args) throws InterruptedException {
LinkedTransferQueue queue = new LinkedTransferQueue();
System.out.println("放入一个元素");
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("五秒后take一个元素");
Thread.sleep(5000);
queue.take();
System.out.println("take完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
System.out.println("开始put,但因为没有take所以阻塞了");
while (!queue.tryTransfer(1)) {
System.out.println("转移失败,下一秒继续尝试");
Thread.sleep(1000);
}
System.out.println("转移结束");
System.out.println("主线程顺利执行到此,结束");
}
}
特殊容器
名称 | 功能 |
---|---|
ConcurrentMap.class | 并发 Map 的接口,定义了 putIfAbsent(k,v)、remove(k,v)、replace(k,oldV,newV)、replace(k,v) 这四个并发场景下特定的方法 |
ConcurrentHashMap.class | 并发 HashMap |
ConcurrentNavigableMap.class | NavigableMap 的实现类,返回最接近的一个元素 |
ConcurrentSkipListMap.class | 它也是 NavigableMap 的实现类(要求元素之间可以比较),同时它比 ConcurrentHashMap 更加 scalable——ConcurrentHashMap 并不保证它的操作时间,并且你可以自己来调整它的 load factor;但是 ConcurrentSkipListMap 可以保证 O(log n) 的性能,同时不能自己来调整它的并发参数,只有你确实需要快速的遍历操作,并且可以承受额外的插入开销的时候,才去使用它 |
ConcurrentSkipListSet.class | 和上面类似,只不过 map 变成了 set |
CopyOnWriteArrayList.class | copy-on-write 模式的 array list,每当需要插入元素,不在原 list 上操作,而是会新建立一个 list,适合读远远大于写并且写时间并苛刻的场景 |
CopyOnWriteArraySet.class | 和上面类似,list 变成 set 而已 |
CopyOnWriteArrayList 写时拷贝ArrayList
写时复制List,它的特点是写的时候copy一份出去单独写,再拷贝回来。写的时候线程安全,写写互斥,读不加锁,效率高。
缺点有二:
- 写时拷贝一份,占用内存大。
- 读写一致性,读取的并不一定是最新的数据。
举例:多个线程同时先写后读,读取的并不是自己刚写的。(用CountDownLatch模拟并发环境)。
区别和联系(与ReentrantReadWriteLock):
- 二者都是应用在读多写少的情况下,而且写操作加锁,都是线程安全的。
- 不同的是写时复制List可以在写的时候支持读,它的读效率更高一些。而ReentrantReadWriteLock写时不可以读。
public class CopyOnWriteArrayListDemo {
public final static int N = 1000;
static CountDownLatch latch = new CountDownLatch(N);
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
for (int i = 0; i < N; i++) {
new Thread(new Runnable() {
@Override
public void run() {
latch.countDown();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(Integer.valueOf(Thread.currentThread().getName()));
int n = list.get(list.size()-1);
if(Integer.valueOf(Thread.currentThread().getName())!=n)
System.out.println("写的是"+Thread.currentThread().getName()+" 读的是"+n);
}
}, String.valueOf(i)).start();
}
}
}
同步工具
名称 | 功能 |
---|---|
CountDownLatch.class | 一个线程调用 await 方法以后,会阻塞地等待计数器被调用 countDown 直到变成 0,功能上和下面的 CyclicBarrier 有点像。 |
CyclicBarrier.class | 也是计数等待,只不过它是利用 await 方法本身来实现计数器“+1” 的操作,一旦计数器上显示的数字达到 Barrier 可以打破的界限,就会抛出 BrokenBarrierException,线程就可以继续往下执行; |
Semaphore.class | 功能上很简单,acquire() 和 release() 两个方法,一个尝试获取许可,一个释放许可,Semaphore 构造方法提供了传入一个表示该信号量所具备的许可数量。 |
Exchanger.class | 这个类的实例就像是两列飞驰的火车(线程)之间开了一个神奇的小窗口,通过小窗口(exchange 方法)可以让两列火车安全地交换数据。 |
CountDownLatch
CountDownLatch是让一个线程等待其他线程完成后(countdown)才可以继续执行。但是实际上一个线程也可以变成多个线程,多等多的关系,不过我们要理解它设计时的目的,通常会让主线程成为等待的线程,其他线程执行后主线程再执行。
举例:做菜要等待所有食材都准备好才可以进行。
public class CountDownLatchDemo {
final static int N = 10;
static CountDownLatch countDownLatch = new CountDownLatch(N);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < N; i++) {
new Thread(new MyRunnable(), "食物"+i).start();
}
countDownLatch.await();
System.out.println("食物准备就绪,开始做菜");
}
public static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 准备就绪");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
}
}
}
CyclicBarrier
CyclicBarrier是等所有线程都执行到await后,大家再一起继续下去,有一种众线程平等的感觉,和CountDownLatch设计目的有所不同。但是我写的这两个demo的使用场景其实两个方法都能实现的,那么实际上最大的区别就是CyclicBarrier是可以复用的。
举例:十个英雄联盟玩家必须全部准备完毕才能进入游戏。
解释:这个例子countdownlatch也可以实现,相比之下cyclicbarrier是累加的,不用像countdownlatch手动countDown()。
public class CyclicBarrierDemo {
final static int N = 10;
static CyclicBarrier cyclicBarrier = new CyclicBarrier(N);
public static void main(String[] args) {
for (int i = 0; i < N; i++) {
new Thread(new MyRunnable(), "线程"+i).start();
}
}
public static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 准备就绪");
try {
Thread.sleep(2000);
cyclicBarrier.await();
} catch (BrokenBarrierException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 进入游戏");
}
}
}
Semaphore 信号量
信号量:允许N个线程进行并发,如果N=1则等价于互斥锁。
举例:信号量为2,有20个线程尝试操作,但信号量有限,每次只有两个线程进行并发。
public class SemaphoreDemo {
static Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + " 尝试获取信号量");
try {
// 获取信号量
semaphore.acquire();
System.out.println("线程" + Thread.currentThread().getName() + " 获取成功,进行操作");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + " 操作完成,释放信号量");
// 释放信号量
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}
原子对象
名称 | 功能 |
---|---|
AtomicBoolean.class | null |
AtomicInteger.class | null |
AtomicIntegerArray.class | null |
AtomicIntegerFieldUpdater.class | null |
AtomicLong.class | null |
AtomicLongArray.class | null |
AtomicLongFieldUpdater.class | null |
AtomicMarkableReference.class | 它是用来高效表述 Object-boolean 这样的对象标志位数据结构的,一个对象引用+一个 bit 标志位 |
AtomicReference.class | null |
AtomicReferenceArray.class | null |
AtomicReferenceFieldUpdater.class | null |
AtomicStampedReference.class | null |
锁
名称 | 功能 |
---|---|
AbstractOwnableSynchronizer.class | 这三个 AbstractXXXSynchronizer 都是为了创建锁和相关的同步器而提供的基础,锁,还有前面提到的同步设备都借用了它们的实现逻辑 |
AbstractQueuedLongSynchronizer.class | AbstractOwnableSynchronizer 的子类,所有的同步状态都是用 long 变量来维护的,而不是 int,在需要 64 位的属性来表示状态的时候会很有用 |
AbstractQueuedSynchronizer.class | 为实现依赖于先进先出队列的阻塞锁和相关同步器(信号量、事件等等)提供的一个框架,它依靠 int 值来表示状态Lock.class,Lock 比 synchronized 关键字更灵活,而且在吞吐量大的时候效率更高,根据 JSR-133 的定义,它 happens-before 的语义和 synchronized 关键字效果是一模一样的,它唯一的缺点似乎是缺乏了从 lock 到 finally 块中 unlock 这样容易遗漏的固定使用搭配的约束,除了 lock 和 unlock 方法以外,还有这样两个值得注意的方法:lockInterruptibly:如果当前线程没有被中断,就获取锁;否则抛出 InterruptedException,并且清除中断tryLock,只在锁空闲的时候才获取这个锁,否则返回 false,所以它不会 block 代码的执行ReadWriteLock.class,读写锁,读写分开,读锁是共享锁,写锁是独占锁;对于读-写都要保证严格的实时性和同步性的情况,并且读频率远远大过写,使用读写锁会比普通互斥锁有更好的性能。 |
ReentrantLock.class | 可重入锁(lock 行为可以嵌套,但是需要和 unlock 行为一一对应),有几点需要注意:构造器支持传入一个表示是否是公平锁的 boolean 参数,公平锁保证一个阻塞的线程最终能够获得锁,因为是有序的,所以总是可以按照请求的顺序获得锁;不公平锁意味着后请求锁的线程可能在其前面排列的休眠线程恢复前拿到锁,这样就有可能提高并发的性能还提供了一些监视锁状态的方法,比如 isFair、isLocked、hasWaiters、getQueueLength 等等 |
ReentrantReadWriteLock.class | 可重入读写锁 Condition.class,使用锁的 newCondition 方法可以返回一个该锁的 Condition 对象,如果说锁对象是取代和增强了 synchronized 关键字的功能的话,那么 Condition 则是对象 wait/notify/notifyAll 方法的替代。在下面这个例子中,lock 生成了两个 condition,一个表示不满,一个表示不空:在 put 方法调用的时候,需要检查数组是不是已经满了,满了的话就得等待,直到“ 不满” 这个 condition 被唤醒(notFull.await());在 take 方法调用的时候,需要检查数组是不是已经空了,如果空了就得等待,直到“ 不空” 这个 condition 被唤醒(notEmpty.await() |
Fork Join
名称 | 功能 |
---|---|
ForkJoinPool.class | ForkJoin 框架的任务池,ExecutorService 的实现类 |
ForkJoinTask.class | Future 的子类,框架任务的抽象ForkJoinWorkerThread.class,工作线程 |
RecursiveTask.class | ForkJoinTask 的实现类,compute 方法有返回值,下文中有例子 |
RecursiveAction.class | ForkJoinTask 的实现类,compute 方法无返回值,只需要覆写 compute 方法,对于可继续分解的子任务,调用 coInvoke 方法完成 |
ForkJoin
ForkJoin:将任务划分为小任务,添加到队列里由ForkJoinPool来执行
举例:计算1到1000的和,任务的划分是程序自主判断的,这里设定区间长度小于200就可以进行计算,否则细分任务。
说明:本例只是简单描述ForkJoin的使用方法。
fork():将新创建的子任务放入当前线程的工作队列中。
join():和线程的join方法类似,就是让该任务先执行然后再回到主线程,它可以返回一个结果。
RecursiveTask是ForkJoinTask的实现类,可以重写。
RecursiveAction也是ForkJoinTask的实现类,不同的是它的compute方法/没有返回值。
public class ForkJoinDemo {
public static class MyForkJoinTask extends RecursiveTask {
public final static int MAX = 200;
private int start;
private int end;
public MyForkJoinTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
// 如果累加区间小于MAX,则可以直接计算,否则继续细分
if(end-start task = forkJoinPool.submit(new MyForkJoinTask(1,1000));
int n = task.join();
System.out.println("最终结果:"+n);
}
}