多线程基础(二)

1.学习目标
2.掌握内容
2.1 安全容器类
2.1.1 Map实现类
ConcurrentHashMap
ConcurrentSkipListMap
2.1.2 Collection实现类
Vector
HashTable
CopyOnWriteArrayList
CopyOnWriteArraySet
ConcurrentSkipListSet
2.2 原子类
cas及相关原子类
2.3 同步工具类
2.3.1 CountDownLacth
2.3.2 Semaphore
1、Semaphore 是什么
2、使用场景
3、Semaphore常用方法说明
4、用semaphore 实现数据库连接池
5、Semaphore实现原理
(1)、Semaphore初始化。
(2)、获取令牌 (实现方式为aqs共享锁)
(3)、释放令牌(实现方式为aqs共享锁)
2.3.3 CyclicBarrier
1.CyclicBarrier是什么
2、使用场景
3、CyclicBarrier常用方法说明
4、用CyclicBarrier 实现一组线程相互等待
5、CyclicBarrier实现原理
(1)、CyclicBarrier初始化。
(2)、核心方法
2.3.4 CountDownLatch、CyclicBarrier、Semaphore的区别
ForkJoin
2.4 阻塞队列
2.4.1 LinkedBlockedQueue
take():
put():
2.4.2 ArrayListBlockedQueue
构造器:
核心数据结构:
put方法:
take方法
LinkedBlockingQueue和ArrayBlockingQueue的差异:
2.4.3 SynchronousQueue
2.4.4 PriorityBlockingQueue
2.4.5 DelayQueue
非阻塞队列中的几个主要方法:
阻塞队列中的几个主要方法:
3.实践
3.1 基于原子类实现线程安全的计数器操作
3.2 基于CountDownLatch实现多个任务执行结果的汇总
3.3 基于阻塞队列实现2个线程间的数据通信
1.学习目标
1.能说明相关类的基本实现原理
2.能在真实场景中应用
2.掌握内容
2.1 安全容器类
2.1.1 Map实现类
ConcurrentHashMap
https://www.jianshu.com/p/4e03b08dc007
ConcurrentSkipListMap

每次的查找都是从最顶层开始,因为最顶层的节点数最少,如果要查找的节点在list中的两个节点中间,则向下移一层继续查找,最终找到最底层要插入的位置,插入节点,然后再次调用概率函数f,决定是否向上复制节点。
其本质上相当于二分法查找,其查找的时间复杂度是O(logn)。

ConcurrentSkipListMap中有三种结构,base nodes,Head nodes和index nodes。
参考:https://zhuanlan.zhihu.com/p/138021927

2.1.2 Collection实现类
Vector
Vector与ArrayList类似, 内部同样维护一个数组, Vector是线程安全的. 方法与ArrayList大体一致, 只是加上 synchronized 关键字, 保证线程安全
HashTable
HashTable是较为远古的使用Hash算法的容器结构了,现在基本已被淘汰,单线程转为使用HashMap,多线程使用ConcurrentHashMap。
HashTable与HashMap对比
(1)线程安全:HashMap是线程不安全的类,多线程下会造成并发冲突,但单线程下运行效率较高;HashTable是线程安全的类,很多方法都是用synchronized修饰,但同时因为加锁导致并发效率低下,单线程环境效率也十分低;
(2)插入null:HashMap允许有一个键为null,允许多个值为null;但HashTable不允许键或值为null;
(3)容量:HashMap底层数组长度必须为2的幂,这样做是为了hash准备,默认为16;而HashTable底层数组长度可以为任意值,这就造成了hash算法散射不均匀,容易造成hash冲突,默认为11;
(4)扩容机制:HashMap创建一个为原先2倍的数组,然后对原数组进行遍历以及rehash;HashTable扩容将创建一个原长度2倍的数组,再使用头插法将链表进行反序;
(5)结构区别:HashMap是由数组+链表形成,在JDK1.8之后链表长度大于8时转化为红黑树;而HashTable一直都是数组+链表;
CopyOnWriteArrayList
(1)CopyOnWriteArrayList,写数组的拷贝,支持高效率并发且是线程安全的,读操作无锁的ArrayList。所有可变操作都是通过对底层数组进行一次新的复制来实现。
(2)CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。它不存在扩容的概念,每次写操作都要复制一个副本,在副本的基础上修改后改变Array引用。CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差。
(3)CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用 ,因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。
原理的通俗理解:写时复制
CopyOnWriteArrayList这是一个ArrayList的线程安全的变体,其原理大概可以通俗的理解为:初始化的时候只有一个容器,很长一段时间,这个容器数据、数量等没有发生变化的时候,大家(多个线程),都是读取(假设这段时间里只发生读取的操作)同一个容器中的数据,所以这样大家读到的数据都是唯一、一致、安全的,但是后来有人往里面增加了一个数据,这个时候CopyOnWriteArrayList 底层实现添加的原理是先copy出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。
CopyOnWriteArraySet
它是线程安全的无序的集合,可以将它理解成线程安全的HashSet。有意思的是,CopyOnWriteArraySet和HashSet虽然都继承于共同的父类AbstractSet;但是,HashSet是通过(HashMap)”实现的,而CopyOnWriteArraySet则是通过CopyOnWriteArrayList实现的,并不是散列表。和CopyOnWriteArrayList类似,其实CopyOnWriteSet底层包含一个CopyOnWriteList,几乎所有操作都是借助CopyOnWriteList,就像HashSet包含HashMap
CopyOnWriteArraySet具有以下特性:1. 它最适合于具有以下特征的应用程序:Set 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。2. 它是线程安全的。3. 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。4. 迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等 操作。5. 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
ConcurrentSkipListSet
ConcurrentSkipListSet是线程安全的有序的集合,适用于高并发的场景。
TConcurrentSkipListSet和TreeSet,它们虽然都是有序的集合。但是,第一,它们的线程安全机制不同,TreeSet是非线程安全的,而ConcurrentSkipListSet是线程安全的。第二,ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,而TreeSet是通过TreeMap实现的
2.2 原子类
cas及相关原子类
AtomicInteger
AtomicLong
AtomicBoolean
AtomicReference
AtomicStampedReference
AtomicMarkableReference
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
Striped64
LongAdder
https://blog.csdn.net/weixin_38003389/article/details/88569336
2.3 同步工具类
2.3.1 CountDownLacth
CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行。它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须大于0。另外它还提供了一个countDown方法来操作计数器的值,每调用一次countDown方法计数器都会减1,直到计数器的值减为0时就代表条件已成熟,所有因调用await方法而阻塞的线程都会被唤醒。
构造方法
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException(“count < 0”);
this.sync = new Sync(count);
}
CountDownLatch只有一个带参构造器,必须传入一个大于0的值作为计数器初始值,否则会报错。可以看到在构造方法中只是去new了一个Sync对象并赋值给成员变量sync。和其他同步工具类一样,CountDownLatch的实现依赖于AQS,它是AQS共享模式下的一个应用。CountDownLatch实现了一个内部类Sync并用它去继承AQS,这样就能使用AQS提供的大部分方法了。
内部类Sync的代码:
//同步器
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
//构造器
Sync(int count) {
setState(count);
}
//获取当前同步状态
int getCount() {
return getState();
}
//尝试获取锁
//返回1表示成功获取到锁
//返回-1标识未获取到锁
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
//尝试释放锁
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;😉 {
//获取同步状态
int c = getState();
if (c == 0)//如果为0表示已经释放完了
return false;
int nextc = c-1; //否则同步状态-1
//cas 更新同步状态
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}

在平时使用CountDownLatch工具类时最常用的两个方法就是await方法和countDown方法。调用await方法会阻塞当前线程直到计数器为0,调用countDown方法会将计数器的值减1直到减为0。
await():
//将当前线程置为等待,直到同步状态变为0,或者线程被中断
public void await() throws InterruptedException {
//以响应中断的方式获取锁 共享
sync.acquireSharedInterruptibly(1);
}

public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//如果线程已被中断抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//1.尝试获取锁
if (tryAcquireShared(arg) < 0)
//2.获取失败,进入该方法自旋直到获取到锁,或者响应中断
doAcquireSharedInterruptibly(arg);
}

countDown():
//减少同步状态的值
public void countDown() {
sync.releaseShared(1);
}
//释放锁 共享模式
public final boolean releaseShared(int arg) {
//尝试释放锁
if (tryReleaseShared(arg)) {
//全部释放完。唤醒其他线程
doReleaseShared();
return true;
}
return false;
}

2.3.2 Semaphore
1、Semaphore 是什么
Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
可以把它简单的理解成我们停车场入口立着的那个显示屏,每有一辆车进入停车场显示屏就会显示剩余车位减1,每有一辆车从停车场出去,显示屏上显示的剩余车辆就会加1,当显示屏上的剩余车位为0时,停车场入口的栏杆就不会再打开,车辆就无法进入停车场了,直到有一辆车从停车场出去为止。
2、使用场景
通常用于那些资源有明确访问数量限制的场景,常用于限流 。
比如:数据库连接池,同时进行连接的线程有数量限制,连接不能超过一定的数量,当连接达到了限制数量后,后面的线程只能排队等前面的线程释放了数据库连接才能获得数据库连接。
比如:停车场场景,车位数量有限,同时只能容纳多少台车,车位满了之后只有等里面的车离开停车场外面的车才可以进入。
3、Semaphore常用方法说明
acquire()
获取一个令牌,在获取到令牌、或者被其他线程调用中断之前线程一直处于阻塞状态。

acquire(int permits)
获取一个令牌,在获取到令牌、或者被其他线程调用中断、或超时之前线程一直处于阻塞状态。

acquireUninterruptibly()
获取一个令牌,在获取到令牌之前线程一直处于阻塞状态(忽略中断)。

tryAcquire()
尝试获得令牌,返回获取令牌成功或失败,不阻塞线程。

tryAcquire(long timeout, TimeUnit unit)
尝试获得令牌,在超时时间内循环尝试获取,直到尝试获取成功或超时返回,不阻塞线程。

release()
释放一个令牌,唤醒一个获取令牌不成功的阻塞线程。

hasQueuedThreads()
等待队列里是否还存在等待线程。

getQueueLength()
获取等待队列里阻塞的线程数。

drainPermits()
清空令牌把可用令牌数置为0,返回清空令牌的数量。

availablePermits()
返回可用的令牌数量。
4、用semaphore 实现数据库连接池
业务场景:
1.连接池大小与信号量的值相同
2.每次有线程获取到连接,连接池活跃数-1,信号量-1
3.每次有线程释放连接,连接池活跃数+1,信号量+1
4.信号线=0是,其他线程无法获取连接
代码:
public class ConnectPool {

//连接池大小
private int size;
//数据库连接集合
private Connect[] connects;
//连接状态标志
private boolean[] connectFlag;
//剩余可用连接数
private  int available;
//信号量
private Semaphore semaphore;

//构造器
public ConnectPool(int size) {
    this.size = size;
    this.available = size;
    semaphore = new Semaphore(size, true);
    connects = new Connect[size];
    connectFlag = new boolean[size];
    initConnects();
}

//初始化连接
private void initConnects() {
    //生成指定数量的数据库连接
    for (int i = 0; i < this.size; i++) {
        connects[i] = new Connect();
    }
}

//获取数据库连接
private synchronized Connect getConnect() {
    for (int i = 0; i < connectFlag.length; i++) {
        //遍历集合找到未使用的连接
        if (!connectFlag[i]) {
            //将连接设置为使用中
            connectFlag[i] = true;
            //可用连接数减1
            available--;
            System.out.println("【" + Thread.currentThread().getName() + "】以获取连接      剩余连接数:" + available);
            //返回连接引用
            return connects[i];
        }
    }
    return null;
}

//获取一个连接
public Connect openConnect() throws InterruptedException {
    //获取许可证
    semaphore.acquire();
    //获取数据库连接
    return getConnect();
}

//释放一个连接
public synchronized void release(Connect connect) {
    for (int i = 0; i < this.size; i++) {
        if (connect == connects[i]) {
            //将连接设置为未使用
            connectFlag[i] = false;
            //可用连接数加1
            available++;
            System.out.println("【" + Thread.currentThread().getName() + "】以释放连接      剩余连接数:" + available);
            //释放许可证
            semaphore.release();
        }
    }
}

//剩余可用连接数
public int available() {
    return available;
}

}

测试代码:
public class TestThread extends Thread {

private static ConnectPool pool = new ConnectPool(3);

@Override
public void run() {
try {
Connect connect = pool.openConnect();
Thread.sleep(100); //休息一下
pool.release(connect);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
for(int i = 0; i < 10; i++) {
new TestThread().start();
}
}

}

5、Semaphore实现原理
(1)、Semaphore初始化。
//指定信号量 默认非公平模式
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}

//指定信号量,选择是否公平模式
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

(2)、获取令牌 (实现方式为aqs共享锁)
1、当前线程会尝试去同步队列获取一个令牌,获取令牌的过程也就是使用原子的操作去修改同步队列的state ,获取一个令牌则修改为state=state-1。
2、 当计算出来的state<0,则代表令牌数量不足,此时会创建一个Node节点加入阻塞队列,挂起当前线程。
3、当计算出来的state>=0,则代表获取令牌成功。
(3)、释放令牌(实现方式为aqs共享锁)
当调用semaphore.release() 方法时
1、线程会尝试释放一个令牌,释放令牌的过程也就是把同步队列的state修改为state=state+1的过程
2、释放令牌成功之后,同时会唤醒同步队列中的一个线程。
3、被唤醒的节点会重新尝试去修改state=state-1 的操作,如果state>=0则获取令牌成功,否则重新进入阻塞队列,挂起线程
2.3.3 CyclicBarrier
1.CyclicBarrier是什么
从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障。

它的作用就是会让所有线程都等待完成后才会继续下一步行动。

举个例子,就像生活中我们会约朋友们到某个餐厅一起吃饭,有些朋友可能会早到,有些朋友可能会晚到,但是这个餐厅规定必须等到所有人到齐之后才会让我们进去。这里的朋友们就是各个线程,餐厅就是 CyclicBarrier。
2、使用场景
利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作,例如赛马,赛跑,斗地主等均需要所有人都准备完成后才可以进行后续操作。
3、CyclicBarrier常用方法说明
//非定时等待
await()
//定时等待
await(long timeout, TimeUnit unit)
//核心等待方法
dowait(boolean timed, long nanos)
4、用CyclicBarrier 实现一组线程相互等待
public class TestCyclicBarrier{

public static void main(String[] args){
int N = 4;
CyclicBarrier barrier = new CyclicBarrier(N, new Runnable() {
@Override
public void run() {
System.out.println(1);
}
});
for(int i=0;i<N;i++) {
new Writer(barrier).start();
}
}

static class Writer extends Thread{
private CyclicBarrier cyclicBarrier;
public Writer(CyclicBarrier cyclicBarrier){
this.cyclicBarrier = cyclicBarrier;
}

  @Override
  public void run(){
     System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
     try{
        Thread.sleep(5000);   //以睡眠来模拟写入数据操作
        System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
        cyclicBarrier.await();
     }catch(InterruptedException e){
        e.printStackTrace();
     }catch(BrokenBarrierException e){
        e.printStackTrace();
     }
     System.out.println("所有线程写入完毕,继续处理其他任务...");
  }

}
}

输出结果:所有线程都写完才会进行后续操作
线程Thread-1正在写入数据…
线程Thread-0正在写入数据…
线程Thread-3正在写入数据…
线程Thread-2正在写入数据…
线程Thread-0写入数据完毕,等待其他线程写入完毕
线程Thread-3写入数据完毕,等待其他线程写入完毕
线程Thread-1写入数据完毕,等待其他线程写入完毕
线程Thread-2写入数据完毕,等待其他线程写入完毕
1
所有线程写入完毕,继续处理其他任务…
所有线程写入完毕,继续处理其他任务…
所有线程写入完毕,继续处理其他任务…
所有线程写入完毕,继续处理其他任务…

5、CyclicBarrier实现原理
(1)、CyclicBarrier初始化。
//指定屏障的size
public CyclicBarrier(int parties) {
this(parties, null);
}
//指定屏障的size,达到屏障后执行的操作
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
(2)、核心方法
//非定时等待
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
//定时等待
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
可以看到不管是定时等待还是非定时等待,它们都调用了dowait方法,只不过是传入的参数不同而已
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
//检查当前栅栏是否被打翻
if (g.broken)
throw new BrokenBarrierException();
//判断线程是否已被中断,是打翻栅栏,抛出异常
if (Thread.interrupted()) {
//打翻栅栏,唤醒拦截的所有线程
breakBarrier();
throw new InterruptedException();
}
//计数器减一
int index = --count;
 //计数器的值减为0则需唤醒所有线程并转换到下一代
if (index == 0) { // tripped
boolean ranAction = false;
try {
 //唤醒所有线程前先执行指定的任务
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
//唤醒所有线程并转到下一代
nextGeneration();
return 0;
} finally {
//确保在任务未成功执行时能将所有线程唤醒
if (!ranAction)
breakBarrier();
}
}

    // loop until tripped, broken, interrupted, or timed out
     //如果计数器不为0则执行此循环
    for (;;) {
        try {
            //根据传入的参数来决定是定时等待还是非定时等待
            if (!timed)
                trip.await();
            else if (nanos > 0L)
                nanos = trip.awaitNanos(nanos);
        } catch (InterruptedException ie) {
            //若当前线程在等待期间被中断则打翻栅栏唤醒其他线程
            if (g == generation && ! g.broken) {
                breakBarrier();
                throw ie;
            } else {
                // We're about to finish waiting even if we had not
                // been interrupted, so this interrupt is deemed to
                // "belong" to subsequent execution.
                //若在捕获中断异常前已经完成在栅栏上的等待,
                 //则直接调用中断操作
                Thread.currentThread().interrupt();
            }
        }
	 //如果线程因为打翻栅栏操作而被唤醒则抛出异常
        if (g.broken)
            throw new BrokenBarrierException();
	 //如果线程因为换代操作而被唤醒则返回计数器的值
        if (g != generation)
            return index;
	//如果线程因为时间到了而被唤醒则打翻栅栏并抛出异常
        if (timed && nanos <= 0L) {
            breakBarrier();
            throw new TimeoutException();
        }
    }
} finally {
    lock.unlock();
}

}
2.3.4 CountDownLatch、CyclicBarrier、Semaphore的区别
1.CountDownLatch和CyclicBarrier主要用于实现线程直接的等待,Semaphore用于控制对某组资源的访问权限。
2.CountDownLatch更倾向于某个线程等待其他线程完成后去执行某个操作,CyclicBarrier更倾向于同一组线程之间的等待。
3.CountDownLatch的计数器需要显示的调用countDown()去减一,CyclicBarrier的await()会自动更新计数器。
ForkJoin
2.4 阻塞队列
2.4.1 LinkedBlockedQueue
LinkedBlockingQueue:基于链表实现的一个阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE。
LinkedBlockingQueue是一种基于单向链表的阻塞队列。因为队头和队尾是2个指针分开操作的,所以用了2把锁+2个条件,同时有1个AtomicInteger的原子变量记录count数。
核心数据结构:
public class LinkedBlockingQueue extends AbstractQueue
implements BlockingQueue, java.io.Serializable {
//…
private final int capacity;

private final AtomicInteger count = new AtomicInteger();
//单向链表的头部和尾部
transient Node head;
private transient Node last;
//两把锁和两个条件
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();

take():
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await(); //队列为空 阻塞
}
x = dequeue(); //获取元素
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal(); //如果还有元素,通知其他take线程
} finally {
takeLock.unlock();
}
//判断是否由满变成了未满,发送未满通知
if (c == capacity)
signalNotFull();
return x;
}
put():
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node node = new Node(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await(); //队列满了 阻塞
}
enqueue(node); //放入元素
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal(); //如果还有空位,通知其他put线程
} finally {
putLock.unlock();
}
//如果队列有空变成了非空,发送非空通知
if (c == 0)
signalNotEmpty();
}
2.4.2 ArrayListBlockedQueue
ArrayBlockingQueue:基于数组实现的一个阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。
构造器:
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {

}
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {

}
核心数据结构:
public class ArrayBlockingQueue extends AbstractQueue implements BlockingQueue, java.io.Serializable {
//…
final Object[] items;
//队头指针
int takeIndex;
//队尾指针
int putIndex;
//核心为1个锁外加两个条件
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
}

put方法:
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); //可中断
try {
while (count == items.length)
notFull.await();//队列满,则阻塞
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal(); //通知非空条件
}
take方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await(); //队列空则阻塞
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings(“unchecked”)
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count–;
if (itrs != null)
itrs.elementDequeued();
notFull.signal(); //通知队列未满条件
return x;
}
LinkedBlockingQueue和ArrayBlockingQueue的差异:

  1. 为了提高并发度,用2把锁,分别控制队头、队尾的操作。意味着在put(…)和put(…)之间、take()与take()之间是互斥的,put(…)和take()之间并不互斥。但对于count变量,双方都需要操作,所以必须是原子类型。

  2. 因为各自拿了一把锁,所以当需要调用对方的condition的signal时,还必须再加上对方的锁,就是signalNotEmpty()和signalNotFull()方法。

  3. 不仅put会通知 take,take 也会通知 put。当put 发现非满的时候,也会通知其他 put线程;当take发现非空的时候,也会通知其他take线程。

2.4.3 SynchronousQueue
SynchronousQueue是一种特殊的BlockingQueue,它本身没有容量。先调put(…),线程会阻塞;
直到另外一个线程调用了take(),两个线程才同时解锁,反之亦然。

2.4.4 PriorityBlockingQueue
PriorityBlockingQueue:以上2种队列都是先进先出队列,而PriorityBlockingQueue却不是,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即容量没有上限(通过源码就可以知道,它没有容器满的信号标志)
在阻塞的实现方面,和ArrayBlockingQueue的机制相似,主要区别是用数组实现
了一个二叉堆,从而实现按优先级从小到大出队列。另一个区别是没有notFull条件,当元素个数超出数
组长度时,执行扩容操作
2.4.5 DelayQueue
DelayQueue:基于PriorityQueue,一种延时阻塞队列,DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
关于take()方法:

  1. 不同于一般的阻塞队列,只在队列为空的时候,才阻塞。如果堆顶元素的延迟时间没到,也会
    阻塞。
  2. 在上面的代码中使用了一个优化技术,用一个Thread leader变量记录了等待堆顶元素的第1个
    线程。为什么这样做呢?通过 getDelay(…)可以知道堆顶元素何时到期,不必无限期等待,可
    以使用condition.awaitNanos()等待一个有限的时间;只有当发现还有其他线程也在等待堆顶
    元素(leader!=NULL)时,才需要无限期等待。

关于put()方法:
不是每放入一个元素,都需要通知等待的线程。放入的元素,如果其延迟时间大于当前堆顶
的元素延迟时间,就没必要通知等待的线程;只有当延迟时间是最小的,在堆顶时,才有必要通知等待
的线程
非阻塞队列中的几个主要方法:
add(E e):将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则会抛出异常;
remove():移除队首元素,若移除成功,则返回true;如果移除失败(队列为空),则会抛出异常;
offer(E e):将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则返回false;
poll():移除并获取队首元素,若成功,则返回队首元素;否则返回null;
peek():获取队首元素,若成功,则返回队首元素;否则返回null
对于非阻塞队列,一般情况下建议使用offer、poll和peek三个方法,不建议使用add和remove方法。
因为使用offer、poll和peek三个方法可以通过返回值判断操作成功与否,而使用add和remove方法却不能达到这样的效果。
注意,非阻塞队列中的方法都没有进行同步措施。
阻塞队列中的几个主要方法:
阻塞队列包括了非阻塞队列中的大部分方法,上面列举的5个方法在阻塞队列中都存在,但是要注意这5个方法在阻塞队列中都进行了同步措施。
除此之外,阻塞队列提供了另外4个非常有用的方法:
put(E e) //put方法用来向队尾存入元素,如果队列满,则等待;
take() //take方法用来从队首取元素,如果队列为空,则等待;
//offer方法用来向队尾存入元素,如果队列满,则等待一定的时间,当时间期限达到时,如果还没有插入成功,则返回false;否则返回true;
offer(E e,long timeout, TimeUnit unit)
//poll方法用来从队首取元素,如果队列空,则等待一定的时间,当时间期限达到时,如果取到,则返回null;否则返回取得的元素;
poll(long timeout, TimeUnit unit)
3.实践
3.1 基于原子类实现线程安全的计数器操作
public class Test2 {

public static AtomicInteger count = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 100; i++) {
        new Thread() {
            public void run() {
                for (int j = 0; j < 100; j++) {
                    count.getAndIncrement();
                }
            }
        }.start();
    }
    Thread.sleep(1000);
    System.out.println("AtomicInteger count: " + count);
}

}
3.2 基于CountDownLatch实现多个任务执行结果的汇总
public class TestThreadPool {
public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
CyclicBarrier barrier = new CyclicBarrier(5);
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 6; i++) {
Integer sout = i;
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + sout);
try {
Thread.sleep(1000);
latch.await();
// barrier.await();
System.out.println(“凑齐了”);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
latch.countDown();

    }

}

}

3.3 基于阻塞队列实现2个线程间的数据通信
public class ConTest3 {
public static void main(String[] args) throws InterruptedException {
int queueSize = 10;
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(queueSize);
// LinkedList queue = new LinkedList();
new Thread(
new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
queue.put(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“向队列取中插入一个元素,队列剩余空间:” + (queueSize - queue.size()));
}
}
}
).start();
new Thread(
new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“从队列取走一个元素,队列剩余” + queue.size() + “个元素”);
}
}
}
).start();
}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值