本文代码示例已放入github:请点击我
快速导航------>src.main.java.yq.Thread.MyQueue
在开始讲解队列之前我们先了解一下下面这三个东西
- CountDownLatch(计数器)
- CyclicBarrier(屏障)
- Semaphore(计数信号量)
他们三个是干什么的呢?有什么用?那我们接下来慢慢讲解。
实现方法:CountDownLatch
需求一:现在我们有这样一个需求,在主线程中开了新的线程去执行任务,大家都知道,线程的执行不会影响其他线程,也就是说在我们的主线程中创建的新的线程,可能会在子线程执行完毕之前就执行完毕了,但是我们不想要这样,我们想要等子线程执行完毕之后再进行执行主线程中的部分代码。
CountDownLatch可以实现类似于计数器的作用,也就是说只有指定数量的线程执行完毕之后才会放行。他不会影响到其他线程执行,但是会阻赛调用countDownLatch.await()的线程,在await处会被阻赛。我们接下来上实例代码。
/**
* CountDownLatch j计数器:只有指定书目的子线程执行完毕才,被阻塞的线程才可以执行
*/
static final class MyCountDownLatch extends Thread {
private CountDownLatch countDownLatch;
public MyCountDownLatch(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(4);
try {
System.out.println("主线程开始执行了");
for (int i = 0; i < 5; i++) {
new MyCountDownLatch(countDownLatch).start();
}
// countDownLatch.await();
System.out.println("好了所有线程执行完毕了");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---------我执行了");
// countDownLatch.countDown();
}
}
首先我们创建了CountDownLatch,但是我们并没有使用,我们看看会发生什么情况。
接下来,我们放开注释,使用CountDownLatch,我们再来看执行结果。
为什么我们不使用CountDownLatch的时候就会执行的结果不是我们想要的呢。那是因为我们在主线程中开了新的四个线程,大家都知道线程是一条新的独立的执行的路径,当我们所有的子线程还没有执行完毕的时候,但是我们的主线程就执行完毕了,所以就出现了这种打印样式。
为什么使用了CountDownLatch之后就达到了我们的需求,所有子线程执行完毕主线程后面的打印才开始执行,是因为我们使用了CountDownLatch.await()阻塞了主线程,然后我们在每个子线程执行完毕调用了一次CountDownLatch.countDown()方法,表示当前线程执行完毕了,在创建CountDownLatch对象的时候,需要指定一个数字,每次调用一下CountDownLatch.countDown()方法就会进行减一的操作,当为0的时候才会恢复当前线程。这里我们有四个线程,这也是为啥我们创建CountDownLatch对象传递了参数为4的原因。另外,使用await方法同样可以指定阻塞时间,当时间过了之后就算没有调用countDown方法也会进行执行。
实现方法:CyclicBarrier
需求二:现在有这个场景,我需要达到指定线程数才可以进行执行,就比如我们玩王者,如果没有10个人,那么就不允许开始游戏,只有存在10个人了我们才开始进行游戏。我们就可以把一个玩家看作一个线程。只有达到了10个人,才允许开始游戏。那么就可以使用我们的CyclicBarrier进行实现。
CyclicBarrier说是屏蔽,但是感觉不像,容易让人发生为误解,他的实际作用就是会阻塞所有的线程,只有达到指定线程数量时,那么所有线程才会开始执行,就是感觉可以让所有的线程同时知悉,不知道测试工具jmter是不是用到了这个东西。
那么我们还是使用代码演示:
/**
* CyclicBarrier 屏障:只有达到指定数目线程数达到就绪状态才会进行执行
*/
static final class MyCyclicBarrier extends Thread{
private CyclicBarrier cyclicBarrier;
public MyCyclicBarrier(CyclicBarrier cyclicBarrier){
this.cyclicBarrier = cyclicBarrier;
}
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
try {
System.out.println("开始匹配");
for(int i = 0 ; i < 10 ; i++){
Thread.sleep(500);
new MyCyclicBarrier(cyclicBarrier).start();
}
Thread.sleep(50);
System.out.println("所有玩家准备完毕,游戏开始!!!");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
System.out.println("我是玩家 "+Thread.currentThread().getName()+"我准备好了");
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里我们可以看到,我们创建了CyclicBarrier对象,并且传递了参数为10,那么按照之前的解释,意思就是当达到十个线程在就绪状态之后才会开始执行,为了更好的看到效果,我们在创建线程的时候sleep半秒,结果发现在五秒之后基本上加上主线程都同时进行执行。这便是我得到的结论,CyclicBarrier的作用就是当达到指定的线程数才开始执行,否则会被阻塞。另外CyclicBarrier对象的await方法也可以指定时间,当时间过了之后如果没有达到指定的线程数量,那么被阻塞的线程会被唤醒进行执行。
实现方法:Semaphore
需求三:我们一个方法只允许五个线程同时进行执行,如果有执行完毕,或者退出的,另外的线程就又可以进行补充,就好像是王者荣耀,我们一个房间只允许五个玩家自由加入,一旦有五个人加入了就不允许加入了,当游戏未开始,但是有玩家没有同意,那么这个时候到了30秒之后他就会自动退出房间,那么这个时候又会有新的玩家加入,那么这种就可以使用我们的Semaphore实现。(数据库连接池
Semaphore就是相当一可以设定一个阀值,当未达到阀值的时候线程就可以竞争钥匙(许可信号),当使用完毕之后就需要进行归还钥匙(许可信号),当归还了钥匙之后,其他的线程才可以继续竞争拿到钥匙(许可信号),说白了就是只允许指定数量的线程同时访问资源。
/**
* Semaphore 计数信号量:设置指定资源只允许指定数量线程进行同时执行,其余线程会被阻塞
* 有许可被释放时,被阻塞的线程会发起竞争。
*/
static final class MySemaphore extends Thread{
private Semaphore semaphore;
private CyclicBarrier cyclicBarrier;
public MySemaphore(Semaphore semaphore,CyclicBarrier cyclicBarrier){
this.semaphore = semaphore;
this.cyclicBarrier = cyclicBarrier;
}
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
try {
MySemaphore mySemaphore = null;
for (int i = 0 ; i < 10 ; i++){
mySemaphore = new MySemaphore(semaphore,cyclicBarrier);
mySemaphore.start();
System.out.println(mySemaphore.getName()+"线程就绪");
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
cyclicBarrier.await();
//拿到许可
semaphore.acquire();
System.out.println("我是线程"+Thread.currentThread().getName()+"我拿到许可了");
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}finally {
//释放许可
semaphore.release();
}
}
}
为了方便观察,我们使用了CyclicBarrier,当线程达到10才进行之后后面的内容,我们还使用sleep进行休眠了3秒。通过控制台的打印输出我们可以很清晰的看到,当10个线程创建完毕之后,控制台每三个每三个进行打印的信息,而且这个时候我们的线程的顺序也发生了变法,这完全说明了,我们的Semaphore起到了作用,首先确实该打印方法只有三个线程进行执行,而且线程进行了竞争,不然位置不可能是正确的。
方法名称 | 作用 |
semaphore.acquire(); | 获取许可 |
semaphore.availablePermits(); | 返回剩余可用的许可数量 |
semaphore.drainPermits(); | 得到现在可用的所有许可 |
semaphore.getQueueLength(); | 得到预计正在等待的线程数 |
semaphore.hasQueuedThreads(); | 查询是否还有线程正在进行等待 |
semaphore.release(); | 释放许可 |
总结:
- CountDownLatch:当需要指定数目线程执行完毕之后才进行执行,使用CountDownLatch
- CyclicBarrier:需要指定数量线程都到达就绪状态之后才进行执行,那么就是用CyclicBarrier
- Semaphore:一段程序只允许指定数量线程同时进行执行,那么就使用Semaphore
java并发编程之队列
在java中队列(Queue)跟list和set是同一个级别的,他们都是继承于Collection接口。所以他们比较类似,但是Queue是先进先出原则(FIFO),队列的好处就是处理速度快,性能好,经常被用作处理并发数据,因为处理速度块呗。那么接下来我们就慢慢打开队列的大门。
队列主要分为三大类:
- 阻塞队列
- 非阻塞队列
- 双端队列
阻塞队列:顾名思义,阻塞队列会发生阻塞,下面是阻塞队列被阻塞的两大场景:
- 当阻塞队列中没有数据,这时有线程进行取数据,那么该线程会被阻塞,直到有新的数据被添加,然后被该线程读取。
- 当阻塞队列中存满了数据,这时有线程进行存数据,同样会被阻塞,直到队列中有新的空间,然后该线程才允许存放数据。
非阻塞队列:也是很好理解,就是不会进行阻塞,他的性能更好,底层通过链表实现以及CAS无锁机制,所以当然性能更好。
双端队列:栈的原理是先进后出现,而队列的原理是先进先出(FIFO),但是双端队列,则是头也可以近,尾也可以进,即双向操作,而且可以实现栈的功能。之前如果要实现先进后出的话,可以使用Stack类,Java已不推荐使用Stack,而是推荐使用更高效的ArrayDeque;既然Queue只是一个接口,当需要使用队列时也就首选ArrayDeque了(次选是LinkedList),这里的ArrayDeque和LinkedList就是双端队列(Deque)的直接实现类。如果想深入了解双端链表请点击我:Stack And Queue
下面是阻塞队列的直接实现:
下面我们讲解一下几个主要的阻塞队列:
- ArrayBlockingQueue:底层是数组实现,而且创建时应该指定容量大小
- LinkedBlockingQueue:如果指定大小,那么就是有限的,如果不指定那就是无效的,说是无限其实还是有限的,不指定 则是 0x7fffffff 这么大
- PriorityBlockingQueue:没有边界的一个队列,允许插入null,但是插入的对象必须是实现 java.lang.Comparable 接 口,而排序规则也是在 java.lang.Comparable 中实现的 ----> 可以设置优先级
- SynchronousQueue:内部只允许容纳一个元素,只有该元素被消费了,才允许继续插入
上诉几个队列都是阻塞队列,都可能会被阻塞。
阻塞队列的常用方法:
排序方法 | 平均情况 | 最好情况 |
---|---|---|
add | 增加一个元素 | 如果队列已满,则抛出一个IllegalSlabEepeplian异常 |
remove | 移除并返回队列头部的元素 | 如果队列为空,则抛出一个NoSuchElementException异常 |
element | 返回队列头部的元素 | 如果队列为空,则抛出一个NoSuchElementException异常 |
offer | 添加一个元素并返回true | 如果队列已满,则返回false |
poll | 移除并返问队列头部的元素 | 如果队列为空,则返回null |
peek | 返回队列头部的元素 | 如果队列为空,则返回null |
put | 返回队列头部的元素 | 如果队列满,则阻塞 |
take | 返回队列头部的元素 | 如果队列为空,则阻塞 |
那么下下面我们就使用阻塞队列实现生产者和消费者的例子,还记得在我们的-- Java并发编程之线程之间通讯 -- 之中为了实现生产者和消费者的例子,我们使用了wait,notify以及Cendition,相对比较麻烦,而且性能还没有队列高。
首先我们提取出一些公共的方法,消费者和生产者。到时候只需要传递不同的队列实例,可以达到代码的复用。
private String name;
private String sex;
public MyThreadPool(String name, String sex) {
this.name = name;
this.sex = sex;
}
//生产者
static final class Producer extends Thread{
private Queue<MyThreadPool> queue;
public Producer(Queue<MyThreadPool> queue) {
this.queue = queue;
}
public void create(){
int i = 0 ;
MyThreadPool myThreadPool = null;
while (true){
try {
Thread.sleep(1000);
//永远都是0 或者1 要不是小红 要不是张三
if(i % 2 == 0){
myThreadPool = new MyThreadPool("张三", "男");
}else{
myThreadPool = new MyThreadPool("小红", "女");
}
queue.offer(myThreadPool);
System.out.println("生产完毕:"+myThreadPool.toString());
i++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
create();
}
}
//消费者
static final class Consumer extends Thread{
private Queue<MyThreadPool> queue;
public Consumer(Queue<MyThreadPool> queue) {
this.queue = queue;
}
public void consumer(){
try {
while (true){
Thread.sleep(1000);
MyThreadPool poll = queue.poll();
System.out.println("开始消费:"+poll.toString());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
consumer();
}
}
//开始的方法
public static void start(Queue queue){
new Producer(queue).start();
try {
//因为我们要使生产者先执行,不然获取不到值,会报错
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Consumer(queue).start();
}
ArrayBlockingQueue实例:
/**
*
* LinkedBlockingQueue: 无边界大小阻塞线程,说是无限大 其实也不是,只是底层给定了一个很大的默认值
* ArrAyBlockingQueue:需要指定大小的一个阻塞队列
*/
@Data
static final class MyArrAyBlockingQueue{
public static void main(String[] args) {
ArrayBlockingQueue<MyThreadPool> arrAyBlockingQueues = new ArrayBlockingQueue<>(4);
start(arrAyBlockingQueues);
}
}
执行结果:
这个例子很简单,我们创建了两个线程,一个生产者,一个消费者,很简单的几步,我们就实现了生产者和消费者。是不是很简单呢。其中LinkedBlockingQueue和ArrAyBlockingQueue作用差不多,只是一个需要指定大小,一个可以不用指定,这里就不演示了。
PriorityBlockingQueue实例:
/**
* PriorityBlockingQueue: 该队列也是一个没有边界的阻塞队列,而且可以指定排序规则
*/
static final class MyPriorityBlockingQueue{
public static void main(String[] args) {
PriorityBlockingQueue<MyThreadPool> priorityBlockingQueue = new PriorityBlockingQueue<>();
start(priorityBlockingQueue);
}
}
另外修改MyThreadPool,添加实现接口,添加优先级,
另外添加不同的优先级:
为了更好的效果,我们使消费者先阻塞3秒:Thread.sleep(3000);
接下来 我们看运行及结果:
我们知道阻塞线程是遵循FIFO,也就是先进先出,那为什么我们这里是level为1的元素先出来呢,就是因为我们使用了PriorityBlockingQueue指定了优先级。数字越小,优先级就越高。
主要的非阻塞队列:
-
ConcurrentLinkedQueue:非阻塞队列,底层是通过链表加上CAS无锁机制,所以性能会更好
ConcurrentLinkedQueue的使用方法跟阻塞队列BlockingQeque差不多,但是新增了两个方法,clear()和addAll(Collection<? extends E> c) 很明显一个可以清空队列,一个可以批量添加。
这里就不演示了,跟阻塞队列都差不多。另外双端队列这里也不讲了,内容太多了不好,至于双端队列可以点击我:Stack And Queue
总结:
- CountDownLatch:会阻塞调用await的线程,只有当我们设置的count值为o的时候才会继续执行。
- CyclicBarrier:调用await的线程会被阻塞,只有当线程数目达到了指定线程数量,那么才会继续执行。
- Semaphore:被锁定的资源一次只允许指定数目线程执行,线程会竞争许可,执行完毕之后会释放许可。
- 三大类型队列:BlockingQueue:阻塞队列<-->AbstractQueue:非组赛队列<-->Deque:双端队列
谢谢大家的观看~~
本文代码示例已放入github:请点击我
快速导航------>src.main.java.yq.Thread.MyQueue