栈和队列基本概念:
栈 (Stack)是一种后进先出(last in first off,LIFO)的数据结构,而队列(Queue)则是一种先进先出 (fisrt in first out,FIFO)的结构。
java栈实现:
我本地是jdk1.6,实现方式是数组,他继承Vector,Vector其实很像ArrayList,只是他在很多关键方法都加了synchronized关键字,导致每次add或者remove都会获取对象锁阻塞。
看一下栈的三个方法:
public E push(E item),将一个元素push到栈顶
public E push(E item) {
addElement(item);
return item;
}
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);//确保长度够,不够就迁移数组扩容
elementData[elementCount++] = obj;
}
private void ensureCapacityHelper(int minCapacity) {
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object[] oldData = elementData;
int newCapacity = (capacityIncrement > 0) ?
(oldCapacity + capacityIncrement) : (oldCapacity * 2);
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
整体实现还是比较简单,没有太多需要说明的,需要注意的是他的底层数据结构是数组实现的,另外一种实现是链表。
public synchronized E pop()
public synchronized E peek()
两个方法唯一的差别就是peek只取不删,pop取了之后删掉,看了源码你会很清楚,另外,我们pop和peek方法都是synchronized修饰的,为啥push没有呢?其实你看他的实现,其实vector的addElement就是synchronized方法修饰的,其实他们都是线程安全的,看实现:
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
public synchronized E elementAt(int index) {
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
}
return (E)elementData[index];
}
public synchronized void removeElementAt(int index) {
modCount++;
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " +
elementCount);
}
else if (index < 0) {
throw new ArrayIndexOutOfBoundsException(index);
}
int j = elementCount - index - 1;
if (j > 0) {
System.arraycopy(elementData, index + 1, elementData, index, j);
}
elementCount--;
elementData[elementCount] = null; /* to let gc do its work */
}
我们可以看到,peek是找到length-1索引处的值,就是从后往前取,而pop方法则调用peek,只是在取到数据之后调用了remove方法,通过索引去快速定位并删除。
再来一个例子演示一下:
private static void stackTest() {
Stack<Integer> stack = new Stack<Integer>();
stack.push(6);
stack.push(5);
stack.push(4);
stack.push(3);
System.out.println(stack.peek());
System.out.println(stack.pop());
System.out.println(stack.pop());
}
可以看到结果:
Java队列实现:
队列是一种先进先出的数据结构,他一般用于多线程的执行队列,他的底层实现也有两种,一种是数组方式,一种是链表方式,由于栈是由数组实现。
add 增加一个元索 如果队列已满,则抛出一个IIIegaISlabEepeplian异常
remove 移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
element 返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
offer 添加一个元素并返回true 如果队列已满,则返回false
poll 移除并返问队列头部的元素 如果队列为空,则返回null
peek 返回队列头部的元素 如果队列为空,则返回null
put 添加一个元素 如果队列满,则阻塞
take 移除并返回队列头部的元素 如果队列为空,则阻塞
先来个例子热热身:
private static void queueTest() {
Queue<Integer> queue =new ArrayBlockingQueue<Integer>(1024*2);
queue.add(1);
queue.add(2);
queue.add(3);
System.out.println(queue);
System.out.println(queue.poll());
System.out.println(queue);
System.out.println(queue.peek());
System.out.println(queue);
}
结果:
可以看出,peek方法依然只是取不删除,poll取出并删除。
看一下类图
画了这么多集合的类图,我发现每个数据结构都是一个接口,然后下面有一个抽象父类实现接口的部分方法,然后具体的每个数据结构都继承这个父类,而且每个集合数据结构的父接口都是Collection,而Collection继承Iterator接口。这就是传说中的集合框架。
看一下方法:
boolean add(E e)
boolean offer(E e)
public boolean add(E e) {
return super.add(e);
}
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
insert(e);
return true;
}
} finally {
lock.unlock();
}
}
private void insert(E x) {
items[putIndex] = x;
putIndex = inc(putIndex);
++count;
notEmpty.signal();
}
我们可以看到add的实现就是调用了offer,两个都不能插入null,不一样的是add失败会抛异常,offer插入前检查容量返回false。他会先检查null,还需要知道的是这里采用了和vector不一样的锁方式,采用了重进锁,和ConcurrentHashMap的put和remove方法使用了一样的并发机制,后面专门研究一下锁,这里只需要知道ArrayBlockingQueue是线程安全的。
public E poll()
public E take()
public E peek()
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == 0)
return null;
E x = extract();
return x;
} finally {
lock.unlock();
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
try {
while (count == 0)
notEmpty.await();
} catch (InterruptedException ie) {
notEmpty.signal(); // propagate to non-interrupted thread
throw ie;
}
E x = extract();
return x;
} finally {
lock.unlock();
}
}
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : items[takeIndex];
} finally {
lock.unlock();
}
}
private E extract() {
final E[] items = this.items;
E x = items[takeIndex];
items[takeIndex] = null;
takeIndex = inc(takeIndex);
--count;
notFull.signal();
return x;
}
final int inc(int i) {
return (++i == items.length)? 0 : i;
}
poll翻译过来是“民意调查”,peek翻译为“偷看”,take有“取”的含义,extract有”提取“的意思,我们再看源码,他们都有取出的含义,具体区别:
peek只取不删,
take和poll都调用extract方法取出元素,并且将takeIndex向后移动一位,虽然没有将元素置空,但是该元素已经不再数组的处理范围内了,他们唯一不一样的地方就是使用锁的方式不同:
poll加锁时候调用lock.lock
take加锁时候调用lock.lockInterruptibly
lock 与 lockInterruptibly比较区别在于:
lock 优先考虑获取锁,待获取锁成功后,才响应中断。
lockInterruptibly 优先考虑响应中断,而不是响应锁的普通获取或重入获取。ReentrantLock.lockInterruptibly允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。 ReentrantLock.lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程。
网上找了一些例子:
public static void main(String[] args) throws InterruptedException {
final Lock lock = new ReentrantLock();
lock.lock();
Thread.sleep(1000);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// lock.lock();
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " interrupted.");
}
});
t1.start();
Thread.sleep(1000);
t1.interrupt();
Thread.sleep(5000);
System.out.println("aaa");
}
执行之后可以稍微清楚的了解他们的差别,就是lock会等待着,当中断的时候还是继续等待,但是lockInterruptibly当检测到中断之后会抛出一个异常。