1、线程上下文切换
线程的上下文切换:任务的状态保存及在加载。
- 上下文:线程切换时CPU寄存器和程序计数器所保存的当前线程信息。
- 寄存器:CPU内部容量较小但速度很快的内存区域。寄存器通过对常用值的快速访问来加快计算机程序运行的速度。
- 程序计数器:一个专门的寄存器,用于表明指令序列中CPU正在执行的位置。存储的值为正在执行的指令的位置或下一个将被执行的指令的位置。
1.1 上下文切换
上下文切换:内核在CPU上对进程或线程进行切换。切换流程如下:‘’
(1)挂起一个进程,将这个进程在CPU中的状态存储于内存的PCB(进程控制块)中。
(2)在PCB中检索下一个进程的上下文并将其在CPU的寄存器中恢复。
(3)跳转到程序计数器所指向的位置并恢复该进程。
1.2 引起线程上下文切换的原因
(1)当前正在执行的任务完成,系统的CPU正常调度下一个任务。
(2)当前正在执行的任务遇到I/O等阻塞操作,调度器挂起此任务,继续调度下一个任务。
(3)多个任务并发抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续调度下一个任务。
(4)用户代码挂起当前任务,如执行sleep方法,让出CPU。
(5)硬件中断。
2、Java阻塞队列
队列是一种只允许在表的前端进行删除操作,在表的后端进行插入操作的线性表。
阻塞是指操作队列的线程的一种状态。线程阻塞有如下两种情况:
- 消费者阻塞:在队列为空时,消费者端的线程都会被自动阻塞,直到数据放入队列,消费者线程会被自动唤醒并消费数据。如图。
- 生产者阻塞:在队列已满且没有可用空间时,生产者端的线程都会被自动阻塞,直到队列中有空的位置腾出,线程会被自动唤醒并生产数据。如图。
2.1 阻塞队列的主要操作
2.1.1 插入操作
(1)public abstract bollean add(E param):指定的元素插入队列中,成功返回true,如果没有可用空间,则抛出IllegalStateException。源码如下:
public boolean add(E e) { if (offer(e)) return true; else throw new IllegalStateException("Queue full"); }
(2)public abstract bollean offer(E param):将指定元素插入队列中,在成功时返回true,如果当前没有可用的空间,则返回false。源码如下:
public boolean offer(E e) { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lock(); try { if (count == items.length) return false; else { enqueue(e); return true; } } finally { lock.unlock(); } }
enqueue(e)方法源码如下:
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(); }
(3)public boolean offer(E e, long timeout, TimeUnit unit ) throws InterruptedException:将指定的元素插入队列中,可以设定等待时间,如果超时,则返回false。源码如下:
public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { checkNotNull(e); long nanos = unit.toNanos(timeout); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) { if (nanos <= 0) return false; nanos = notFull.awaitNanos(nanos); } enqueue(e); return true; } finally { lock.unlock(); } }
(4)public void put(E e) throws InterruptedException:将指定元素插入队列,如果队列已经满了,则阻塞、等待可用队列空间的释放,直到有可用的队列空间释放且插入成功为止,源码如下:
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(); } }
2.1.2 插入操作
(1)poll():取走队列队首的对象,如果取不到,则返回null。源码如下:
public E poll() { final ReentrantLock lock = this.lock; lock.lock(); try { return (count == 0) ? null : dequeue(); } finally { lock.unlock(); } }
dequeue()源码如下:
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; }
(2)poll(long timeout, TimeUnit unit):取走队列队首的对象如果在指定的时间内有数据可取,则返回队列中的数据,若超时,返回null。
(3)take():取走队列队首的对象,如果队列为空,则进入阻塞状态等待,直到队列中有新的数据被加入,取出新加入的数据。源码如下:
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) notEmpty.await(); return dequeue(); } finally { lock.unlock(); } }
(4)drainTo(Collection collection):一次性批量获取所有数据,也可以指定获取数据的个数。源码如下:
public int drainTo(Collection<? super E> c, int maxElements) { checkNotNull(c); if (c == this) throw new IllegalArgumentException(); if (maxElements <= 0) return 0; final Object[] items = this.items; final ReentrantLock lock = this.lock; lock.lock(); try { int n = Math.min(maxElements, count); int take = takeIndex; int i = 0; try { while (i < n) { @SuppressWarnings("unchecked") E x = (E) items[take]; c.add(x); items[take] = null; if (++take == items.length) take = 0; i++; } return n; } finally { // Restore invariants even if c.add() threw if (i > 0) { count -= i; takeIndex = take; if (itrs != null) { if (count == 0) itrs.queueIsEmpty(); else if (i > take) itrs.takeIndexWrapped(); } for (; i > 0 && lock.hasWaiters(notFull); i--) notFull.signal(); } } } finally { lock.unlock(); } }
2.2 Java中的阻塞队列实现
(1)ArrayBlockingQueue:基于数组结构实现的有界阻塞队列,ArrayBlockingQueue队列按照先进先出原则对元素进行排序,在默认情况下不保证元素操作的公平性。
(2)LinkedBlockingQueue:基于链表结构实现的有界阻塞队列,LinkedBlockingQueue按照先进先出原则对元素进行排序。LinkedBlockingQueue对生产者端和消费者端分别采用了两个独立的锁来控制数据同步,队列的并发性能较高。
(3)PriorityBlockingQueue:一个支持优先级的无界队列。元素在默认情况下采用升序排列,可以自定义实现compareTo方法来指定元素进行排序规则。如果两个元素的优先级相同,则不能保证该元素的存储和访问顺序。
class Data implements Comparable<Data> { private String id; private Integer number; public Integer getNumber() { return number; } public void setNumber(Integer number) { this.number = number; } @Override public int compareTo(Data o) { return this.number.compareTo(o.getNumber()); } } final PriorityBlockingQueue<Data> queue = new PriorityBlockingQueue<Data>();
(4)DelayQueue:支持延时获取元素的无界阻塞队列,底层使用PriorityBlockingQueue实现。DelayQueue队列中的元素必须实现Delayed接口,该接口定义了在创建元素是该元素的延迟时间,在内部通过为每个元素的操作加锁来保障数据的一致性。只有在延迟时间到后才能从队列中提取元素。
DelayQueue适用于以下场景:
- 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素,则表示缓存的有效期到了。
- 定时任务调度:使用DelayQueue保存即将执行的任务和执行时间,一旦从DelayQueue中获取元素,就表示任务开始执行。
(5)SynchronousQueue:一个不存储元素的阻塞队列。SynchronousQueue中的每个put操作都必须等待一个take操作完成,否则不能继续添加元素。
(6)LinkedTransferQueue:基于链表结构实现的无界阻塞队列。
相对于其他阻塞队列,LinkedTransferQueue多了transfer、tryTransfer和tryTransfer(E e, long timeout, TimeUnit unit)方法。
- transfer方法:如果当前有消费者正在等待接收元素,transfer方法就会直接把生产者传入的元素投递给消费者并返回true。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的尾部(tail)节点,直到被消费后才返回。
- tryTransfer方法:尝试是否能将生产者传入的元素直接传给消费者,如果没有消费者等待接收元素,则返回false。
- tryTransfer(E e, long timeout, TimeUnit unit)方法:尝试是否能将生产者传入的元素直接传给消费者,如果没有消费者,则等待指定时间,超时返回false。
(7)LinkedBlockingDeque:基于链表结构实现的双向阻塞队列,可以在队列的两端分别执行插入和移除元素操作。可以减少一半的锁资源竞争,提高队列的操作效率。相比于其他队列,LinkedBlockingDeque多了addFirst、addLast、offerFirst、offerLast、peekFirst、peekLast等方法。其中以First结尾的方法表示在队列头部执行插入、获取、移除操作;以Last结尾的方法表示在队列的尾部执行插入、获取、移除操作。