栈
关于栈,有一个贴切的例子:一摞叠在一起的盘子。后进者先出,先进者后出。
栈是一种"操作受限"的线性表,只允许一端插入和删除数据。
当摸个数据集合只涉及在一段插入和删除数据,并且满足后进先出,先进后出的特性,就应该首选"栈"这种数据结构。
用数组实现的栈,叫做顺序栈;用链表实现的栈,叫做链式栈。这两种栈的入栈和出栈只涉及栈顶个别数据的操作,所以时间复杂度都是O(1)。
可以用摊还分析法来分析他的时间复杂度
为了分析的建表,可以实现做一些假设和定义:
- 栈空间不够时,我们重新申请一个是原来大小两倍的数组
- 为了简化分析,假设只有入栈操作,没有出栈操作
- 定义不涉及内存搬移的入栈操作为simple-push操作,时间复杂度为O(1)
如果当前栈的大小为k,并且已经满了,当再有新的数据要入栈时,就需要重新申请2倍大小的内存,并且做k-1次入栈操作,我们都不需要再重新申请内存和搬移数据,所以这k-1次入栈都只需要一个simple-push操作就能完成。
编译器可以使用栈来实现表达式求值
编译器是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。从左向右遍历表达式,当遇上数字,就压入操作数栈;当遇到运算符,就与运算符栈顶元素进行比较。
如果比运算符栈顶元素的优先级高,就将当前运算符压入栈顶;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取出栈顶运算符,从操作数栈的栈顶取出两个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。
队列
当我们向固定大小的线程池中请求一个线程时,如果线程池没有空闲资源了,这个时候线程池应该处理这个请求,是拒绝请求还是排队请求?
关于队列,有一个贴切的例子:排队买票,先来的先买,后来的人只能站在末尾,不能插队。先进者先出。
跟栈一样,队列可以用数组来实现,用数组实现的栈叫顺序栈 ,用数组实现的队列叫顺序队列 ;也可以用链表来实现,用链表实现的是链式栈 ,用链表实现的队列叫链式队列。
对于栈来说,只需要一个栈顶元素就够了,但是队列需要两个指针,一个是head指针,指向对头 ;一个是tail指针,指向队尾。
队列不停地进行入队和出队操作,head和tail都会持续往后移动。当tail移动到最右边,即使数组中还有空闲空间,也不能继续往队列中添加数据了,接下来的循环队列就是解决这个问题的。
循环队列
原本队列有头有尾的,是一条直线。现在我们把首尾相连,形成一个环,最关键的是,确定好队空和队满的判定条件。在非循环队列中,队满的判断条件是tail==n,队空的判断条件是head==tail。而在循环队列中,当队满时:(tail+1)%n=head,队空时:head==tail。
阻塞队列
阻塞队列是在队列基础上增加了阻塞操作。就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,知道队列中有了数据才能返回;如果队列已经满了,那么插入的数据就会被阻塞,知道队列中有空闲位置后再插入数据,然后再返回。
上述的定义就是一个"生产者-消费者模型"。这种基于阻塞队列实现的"生产者-消费者模型",可以有效地协调生产和消费的速度。当"生产者"生成数据的速度过快,"消费者"来不及消费,存储数据的队列很快就满了。这时候,生产者就阻塞等待,知道"消费者"消费了数据,"生产者"才会被唤醒继续"生产"。
基于阻塞队列,还可以通过协调"生产者"和"消费者"的个数,来提高数据的办理效率。比如前面的例子,我们可以多配置几个"消费者",来对应一个"生产者"。
在多线程的情况下,会有多个线程同时操作队列,这时就会存在线程安全问题,那么如何实现一个线程安全的队列呢?
并发队列
线程安全的队列我们叫做并发队列,最简单的实现方式是直接在enqueue()、dequeue()方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或取操作。实际上,基于数据的循环队列比链式队列应用更加广泛的原因。
开篇问题的解答
线程池没有空闲线程时,新的任务请求线程资源时,线程池该如何处理?
一般有两种处理策略,第一种是非阻塞的处理方式 ,直接拒绝任务请求;另一种是阻塞的处理方式,将请求排队,等到有空闲线程时,去除排队的请求继续处理。那如何存储排队的请求呢?
我们希望公平的处理每个排队的请求,先进者先服务,所以队列这种数据结构很适合来存储排队请求。队列有记忆链表和数组这两种实现方式,这两种实现方式对于排队请求有什么区别?
基于链表的实现方式,可以实现一个支持无线排队的无界队列,但是可能会导致过多的请求排队等待,请求处理的响应时间过长。所以,针对响应时间比较敏感的系统,基于链表实现的线程池是不合适的。
基于数组实现的有界队列,队列的大小有限,所以线程池中排队请求超过队列大小时,接下来的请求就会被拒绝,这种方式对响应时间敏感的系统就更加合理。不过,设置一个合理的队列大小也很讲究。队列太小会导致无法充分利用系统资源,发挥最大性能。队列太大导致请求太多。
处理队列应用在线程池请求排队的场景之外,队列可以应用在任何有限资源池中,用于排队请求,比如数据库连接池等。对应大部分资源有限的场景,当没有空闲资源时,基本上都可以通过"队列"这种数据结构来实现请求排队。