JDK,常见数据结构解读
一。情有独钟
对数据结构情有独钟,打算慢慢把jdk里的实现都读一遍,发现其中的亮点,持续更新。
二。ArrayList
这应该是我们学习java最早接触的到的数据结构,众所周知,数组在申请了内存之后,无法扩展;而数组队列,是实现了动态扩容的功能,意义上是为动态数组,实际上的数组扩容是不允许在原地址上伸长的,很简单,因为在你申请的数组空间之后,可能存在别的被申请掉的内存;要实现动态数组,必然是新申请一个更大的连续内存空间,并替换到原来的引用中。
从构造函数,可以清楚看到,elementData,就是这个存储数据的内存地址。
然后,找到添加的接口,add;在真正赋值之前,会进行grow方法。
可以看到,真正干活的是这个copyof,找到最后,就是这个方法。
首先这个泛型数组,会先判断一下如果是Object父类,则直接new Object,如果不是则调用Arrays的接口创建,才去新建一个数组,然后就会去拷贝数组到新的数组,并返回这个被拷贝的数组。
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
它的get方法,简单判断一下是否大于元素容量,防止内存泄漏的操作。
public E get(int index) {
rangeCheck(index); return elementData(index);
}
它的remove方法,是将这个位置之后的所有元素,前移一个位置,并将最后的元素设置为null。
public E remove(int index) {
rangeCheck(index); modCount++;
E oldValue = elementData(index); int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work return oldValue;
}
它提供的排序接口,设计的是传入一个比较器,可以自定升序还是降序,最终一个分支使用的是mergeSort。最后还校验了一下modcount,前后是否相等,如果不相等抛出并发异常,有点CAS的思想。
@Override
@SuppressWarnings("unchecked")
public void sort(Comparator<? super E> c) {
final int expectedModCount = modCount;
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
public static void sort(Object[] a) {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
长度小于7插入排序,反正是个n平方的排序,
private static void mergeSort(Object[] src,
Object[] dest,
int low,
int high,
int off) {
int length = high - low; // Insertion sort on smallest arrays
if (length < INSERTIONSORT_THRESHOLD) {
for (int i=low; i<high; i++)
for (int j=i; j>low &&
((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)
swap(dest, j, j-1);
return;
} // Recursively sort halves of dest into src
int destLow = low;
int destHigh = high;
low += off;
high += off;
int mid = (low + high) >>> 1;
mergeSort(dest, src, low, mid, -off);
mergeSort(dest, src, mid, high, -off); // If list is already sorted, just copy from src to dest. This is an
// optimization that results in faster sorts for nearly ordered lists.
if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) {
System.arraycopy(src, low, dest, destLow, length);
return;
} // Merge sorted halves (now in src) into dest
for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
if (q >= high || p < mid && ((Comparable)src[p]).compareTo(src[q])<=0)
dest[i] = src[p++];
else
dest[i] = src[q++];
}
}
三。PriorityQueue
优先队列,读作优先写作二叉树,也叫堆(大顶堆,小顶堆)。
它的实现方法是数组,使用数组做二叉树,每个元素e[i]的孩子为e[2*i+1],e[2*i+2]。
找到添加元素的方法;比较器为空的时候;它从末尾插入,先找出父亲,如果父节点比自己大,则继续往上,将父节点往下移动,直到找到比它小的位置插入,默认是一个小顶堆。
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
} private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
弹出操作就是把堆定元素拿走,然后从末尾拿出一个元素,放在堆顶,不断地下沉。
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
return result;
}
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>)x;
int half = size >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = queue[child];
int right = child + 1;
if (right < size &&
((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
四。ArrayBlockingQueue
看腻了数组队列,我们来看多线程的阻塞队列是怎么实现的;
粗浅的看,它是在多线程中保持一致性的一种数据结构,保持一致性只有两种思路:(1)假设它发生了冲突,则必然加锁(悲观)(2)假设他不一定产生冲突,CAS无锁实现(乐观);
当然,它最基本的数据都是数组;
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();
}
}
从以上的代码,非常直白,首先只能有一个线程进入这个数据操作的代码,并且队列是不扩容的,一旦达到最大容量,则直接拒绝,返回false;
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();
}
正如我们认知的一样,它是一个先进先出的队列,所以在下标达到最大长度之后,会reset成0,并且入队之后,还会唤醒一个事件,就是非空;
我们还有一个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();
}
}
这个队列相当于不是快速失败,而是将当前线程park,使用一个condition的await,让线程等待;
它的获取方法take,我们来阅读以下;
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
使用的是一个线程中断锁,并且在队列为空的时候,park当前线程;与入队方法enqueue成对应,有元素进来的时候会signal阻塞在此的线程;
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;
}
这个操作,在结束的时候会唤醒阻塞在put的线程,告诉他有位置可以进来了。
而此外,它还提供了带等待时间的阻塞方法。
public boolean offer(E e, long timeout, TimeUnit unit)
public E poll(long timeout, TimeUnit unit)
然后它的size,也是需要获取重入锁的,不是非阻塞的。
这么看,它有点像--消息队列。
五。ConcurrentLinkedQueue
并发无锁链表队列,因为线程不会被park,所以效率较高,但是可能引起cpu运算过高。它是在普通链表的基础上,添加了并发的控制, 并采用CAS原子操作保证内存的有序写入。
它的基本元素,Node,只有两个属性,原子的item和next。
java.util.concurrent.ConcurrentLinkedQueue.Node
Node<E>
volatile E item;
volatile Node<E> next;
初始化的时候,head和tail都指向new出来的一个Node上。
head = tail = new Node<E>(null);
我们只需要关心它的添加(offer)和获取(poll)接口是什么样的流程就可以了。
offer接口
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e); // 快照
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {// 如果是队尾 则尝试CAS插入
if (p.casNext(null, newNode)) {
// 如果tail节点在插入前不是指向末尾节点,则更新tail
// 延迟更新也没事,始终会有一个线程更新成功
if (p != t)
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
// 由于poll方法,会将节点自引用以便gc,所以要从头节点开始找
p = (t != (t = tail)) ? t : head;
else
// 如果t的引用地址和tail的一致,则p往下找(p=p.next的意思)
// 如果不一致,则直接拿到tail并赋值给p
p = (p != t && t != (t = tail)) ? t : q;
}
}
在单线程插入的时候,插入完成之后如上图。如果继续插入,则p和t不相等,会更新tail的值,这就是快照时候tail不是指向最后一个节点才会执行的逻辑。
如果是多线程插入,在上一个线程没有更新tail的时候,它可能会一直p=p.next的流程,这时候另一个线程更新了tail的地址,这时候需要刷新t的位置。
offer和offer方法的多线程冲突,主要在于tail指针的位置问题。
接下来我们看看poll方法。
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
// 出队是更新节点data为null
if (item != null && p.casItem(item, null)) {
// 如果p节点的下一个不为空则head指向下一个,否则指向p
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) {// 如果下一个是null 则更新头节点为自引用
updateHead(h, p);
return null;
}
else if (p == q)// 撞到了自引用 则跳出循环重新copy快照
continue restartFromHead;
else
p = q;// p = p.next的意思
}
}
}
我们假设队列的情况是。h还是指向head原地址,p经过一步之后会指向h的next。这时候要将p的Node的item更新为null,并设置head指针,而且p.next不为空,则head会更新到p.next上。
更新之后的状态是。
如果这个时候tail没有更新,还是指向最初的那个节点,也就是offer与poll的冲突。这时候offer就会走第二个else if条件,拿到head。
只有三四行代码,却那么多场景,真是大师作品。写得这么难懂,是因为可以节省CAS指令,我们自己写的CAS操作是util success,这样可能会执行很多条,它这里的head和tail更新不强制一定成功。
JDK,常见数据结构解读的更多相关文章
- 常见数据结构之JavaScript实现
常见数据结构之JavaScript实现 随着前端技术的不断发展,投入到前端开发的人数也越来越多,招聘的前端职位也越来越火,大有前几年iOS开发那阵热潮.早两年,前端找工作很少问到关于数据结构和算法的, ...
- [py]软件编程知识骨架+py常见数据结构
认识算法的重要性 - 遇到问题? 学完语言,接到需求,没思路? 1.学会了语言,能读懂别人的代码, 但是自己没解决问题的能力,不能够把实际问题转换为代码,自己写出来.(这是只是学会一门语言的后果),不 ...
- Java进阶7并发优化4——JDK并发数据结构
Java进阶7并发优化4——JDK并发数据结构20131114 由于并发程序和串行程序的不同特点,在串行程序中使用的数据结构可能无法在并行程序中直接的正常使用,因为这些数据结构可能不是线程安全的,所以 ...
- 转 Python常见数据结构整理
http://www.cnblogs.com/jeffwongishandsome/archive/2012/08/05/2623660.html Python常见数据结构整理 Python中常见的数 ...
- unity_数据结构(常见数据结构及适用场景)
常见的数据结构: 1.Array: 最简单的数据结构 特点:数组存储在连续的内存上.数组的内容都是相同类型.数组可以直接通过下标访问.优点:由于是在连续内存上存储的,所以它的索引速度非常快,访问一个元 ...
- 8种常见数据结构及其Javascript实现
摘要: 面试常问的知识点啊... 原文:常见数据结构和Javascript实现总结 作者:MudOnTire Fundebug经授权转载,版权归原作者所有. 做前端的同学不少都是自学成才或者半路出家, ...
- 常见数据结构的 Python 实现(建议收藏)
数据结构作为计算机基础的必修内容,也是很多大型互联网企业面试的必考题.可想而知,它在计算机领域的重要性. 然而很多计算机专业的同学,都仅仅是了解数据结构的相关理论,却无法用代码实现各种数据结构. 今日 ...
- 数据结构和算法(Golang实现)(11)常见数据结构-前言
常见数据结构及算法 数据结构主要用来组织数据,也作为数据的容器,载体. 各种各样的算法,都需要使用一定的数据结构来组织数据. 常见的典型数据结构有: 链表 栈和队列 树 图 上述可以延伸出各种各样的术 ...
- 数据结构和算法(Golang实现)(12)常见数据结构-链表
链表 讲数据结构就离不开讲链表.因为数据结构是用来组织数据的,如何将一个数据关联到另外一个数据呢?链表可以将数据和数据之间关联起来,从一个数据指向另外一个数据. 一.链表 定义: 链表由一个个数据节点 ...
随机推荐
- linq group by / distinct
https://www.cnblogs.com/qixu/p/6033532.html http://www.cnblogs.com/A_ming/archive/2013/05/24/3097062 ...
- part1:8-远程登录Linux
Linux远程登录 Linux系统中是通过ssh服务实现的远程登录功能.默认ssh服务开启了22端口,而且在安装完成系统时,这个服务已经安装,并且是开机启动的.所以不需要额外配置就能直接远程登录Lin ...
- 2018.07.06 POJ1698 Alice's Chance(最大流)
Alice's Chance Time Limit: 1000MS Memory Limit: 10000K Description Alice, a charming girl, have been ...
- IntelliJ IDEA 2017版 spring-boot加载jsp配置详解(详细图文实例)
一.创建项目 (File--->New-->Project) 2.项目配置内容 3.选择配置项目的Group包名,Artifact项目名称 4.选择项目类型为web类型 5.创建成功,点击 ...
- UVa 11039 Building designing (贪心+排序+模拟)
题意:给定n个非0绝对值不相同的数,让他们排成一列,符号交替但绝对值递增,求最长的序列长度. 析:我个去简单啊,也就是个水题.首先先把他们的绝对值按递增的顺序排序,然后呢,挨着扫一遍,只有符号不同才计 ...
- go指针的一个小坑
几乎可以肯定的说,go语言中除了闭包在引用外部变量的时候是传引用的,其他的时候都是传值的.如果你说形参可以定义为指针.好吧,那么告诉你这个指针的值其实是按照传值的方式使用的. 下面看个很浅显的例子: ...
- 2015 - 4- 21 iOS开发越狱环境的搭建1
2015 - 4- 20 1. 越狱环境的搭建 http://www.iduuke.com/2030.html http://www.cnblogs.com/xiongwj0910/archi ...
- Structure From Motion(二维运动图像中的三维重建)
SfM(Structure from Motion)简介 Structure from motion (SfM) is a photogrammetric range imaging techniqu ...
- (转载)从Java角度理解Angular之入门篇:npm, yarn, Angular CLI
本系列从Java程序员的角度,带大家理解前端Angular框架. 本文是入门篇.笔者认为亲自动手写代码做实验,是最有效最扎实的学习途径,而搭建开发环境是学习一门新技术最需要先学会的技能,是入门的前提. ...
- UT源码+019
设计三角形问题的程序 输入三个整数a.b.c,分别作为三角形的三条边,现通过程序判断由三条边构成的三角形的类型为等边三角形.等腰三角形.一般三角形(特殊的还有直角三角形),以及不构成三角形.(等腰直角 ...