从数据结构,特性,项目中的实用程度来剖析
数据结构 List -->ArrayList LinkedList Vector
下面从实用中着重各个分析
ArrayList,Vector都是实现了List接口,其底层数据结构是数组,数组很重要一点的是下标(index)
他俩都是有序的,并且可以存null。
ArrayList,并不是线程安全的,现在的高并发项目中已然不使用了。
为啥?下面从代码层面来剖析它
// 序列版本号,说明他可以支持序列化,能通过序列化去传输。
private static final long serialVersionUID = 8683452581122892189L;
//下面这俩属性便是Arraylist的核心
private transient Object[] elementData;
private int size;
上面说底层数据结构是数组 ,elementData 便是实锤,size便是该数组大小
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
add方法便更是诠释了其数据结构,而且在这方法中也说明他是一个动态的数组
从许多的方法来看,其index也置为重要,例如:
初级面试经常会问:遍历List有几种方式,删除某个指定元素咋整?
1,索引访问,if(e.equals(get(i)))list.remove(); i--
注意得i--,前面说了,他是一个动态数组,如果不i--,那么遍历是会报越界异常
2,迭代器 iterator
while(list.hasNext()) E e = list.next();
if(e.equals(get(i)))list.remove();
这时候不需要使用i--,其实现得iterator.remove方法已经帮我们实现了i--
3,foreach 增强for循环,与1差异不大
在jdk1.8中,便有便捷方式 简写了,其实现也是与上一致
list.removeif(o->{if(o.equals(E)) return true;});
这个问题便是让你知道动态数组的特性!
可能会再问,哪种方式遍历快?其实这个问题意义并不大,前面说过,数组很重要一点的index
说明不管那种遍历方法,其底层都是数组,都是index,都是get(i)也是问你,其数据结构的理解
有兴趣的同学可自行查看源码,都是get(i)
下面来说说为啥在高并发下,不建议使用ArrayList,只需从一个方法
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
举个例子:如果size=1,两个栈帧执行size++操作指令,我们期望的结果是3,但是有可能结果是2;
那么想要保证读改写共享变量的操作是原子的,
就必须保证一个栈帧读改写共享变量指令的时候,
另一个栈帧不能操作缓存了该共享变量内存地址的缓存。
从jvm这个角度来看的话,需要了解一个知识点,就是java锁机制;
下面来扯扯面试中许多问题会延伸过来的小知识:
在JDK 5之前Java语言是靠synchronized关键字保证同步;
锁机制存在以下问题:
1,在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
2,一个线程持有锁会导致其它所有需要此锁的线程挂起。
3,如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。在java中,CAS是CPU的指令,同时借助JNI(jvm模型的一部分,与c的对接)来完成Java的非阻塞算法。
Compare and Swap 比较并替换
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B.否则return;
(list)共享变量进行读改写(size++就是经典的读改写操作)操作,
那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,
操作完之后共享变量的值会和期望的不一致.
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作;(转载,自己总结没那么详细)
1.ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
举个例子: AmoticInteger的++操作;
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
List -->ArrayList LinkedList Vector
下面从实用中着重各个分析
ArrayList,Vector都是实现了List接口,其底层数据结构是数组,数组很重要一点的是下标(index)
他俩都是有序的,并且可以存null。
ArrayList,并不是线程安全的,现在的高并发项目中已然不使用了。
为啥?下面从代码层面来剖析它
// 序列版本号,说明他可以支持序列化,能通过序列化去传输。
private static final long serialVersionUID = 8683452581122892189L;
//下面这俩属性便是Arraylist的核心
private transient Object[] elementData;
private int size;
上面说底层数据结构是数组 ,elementData 便是实锤,size便是该数组大小
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
add方法便更是诠释了其数据结构,而且在这方法中也说明他是一个动态的数组
从许多的方法来看,其index也置为重要,例如:
初级面试经常会问:遍历List有几种方式,删除某个指定元素咋整?
1,索引访问,if(e.equals(get(i)))list.remove(); i--
注意得i--,前面说了,他是一个动态数组,如果不i--,那么遍历是会报越界异常
2,迭代器 iterator
while(list.hasNext()) E e = list.next();
if(e.equals(get(i)))list.remove();
这时候不需要使用i--,其实现得iterator.remove方法已经帮我们实现了i--
3,foreach 增强for循环,与1差异不大
在jdk1.8中,便有便捷方式 简写了,其实现也是与上一致
list.removeif(o->{if(o.equals(E)) return true;});
这个问题便是让你知道动态数组的特性!
可能会再问,哪种方式遍历快?其实这个问题意义并不大,前面说过,数组很重要一点的index
说明不管那种遍历方法,其底层都是数组,都是index,都是get(i)也是问你,其数据结构的理解
有兴趣的同学可自行查看源码,都是get(i)
下面来说说为啥在高并发下,不建议使用ArrayList,只需从一个方法
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
举个例子:如果size=1,两个栈帧执行size++操作指令,我们期望的结果是3,但是有可能结果是2;
那么想要保证读改写共享变量的操作是原子的,
就必须保证一个栈帧读改写共享变量指令的时候,
另一个栈帧不能操作缓存了该共享变量内存地址的缓存。
从jvm这个角度来看的话,需要了解一个知识点,就是java锁机制;
下面来扯扯面试中许多问题会延伸过来的小知识:
在JDK 5之前Java语言是靠synchronized关键字保证同步;
锁机制存在以下问题:
1,在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
2,一个线程持有锁会导致其它所有需要此锁的线程挂起。
3,如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。在java中,CAS是CPU的指令,同时借助JNI(jvm模型的一部分,与c的对接)来完成Java的非阻塞算法。
Compare and Swap 比较并替换
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B.否则return;
(list)共享变量进行读改写(size++就是经典的读改写操作)操作,
那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,
操作完之后共享变量的值会和期望的不一致.
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作;(转载,自己总结没那么详细)
1.ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
举个例子: AmoticInteger的++操作;
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
-----------------------------------------------------------------------回到主题分割线------------------------------
了解到CAS特性,及锁机制;那么我们很容易知道哪个集合是线程安全的;
LinkedList
LinkedList继承于AbstractSequentialList,并且实现了Dequeue接口。
LinkedList包含两个重要的成员:header 和 size。
header是双向链表的表头,它是双向链表节点所对应的类Entry的实例。Entry中包含成员变量: previous, next, element。其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。
size是双向链表中节点的个数。
单链表:由两部分组成 数据域(Data)和结点域(Node),原理的实现是通过Node结点区的头指针head实现的,每个结点都有一个指针,每个节点指针的指向都是指向自身结点的下一个结点,最后一个结点的head指向为null,这样一来就连成了上述所说绳子一样的链,对单链表的操作只能从一端开始,如果需要查找链表中的某一个结点,则需要从头开始进行遍历。
双链表:双链表和单链表相比,多了一个指向尾指针(tail),双链表的每个结点都有一个头指针head和尾指针tail,双链表相比单链表更容易操作,双链表结点的首结点的head指向为null,tail指向下一个节点的tail;尾结点的head指向前一个结点的head,tail 指向为null,是双向的关系;
在单链表中若需要查找某一个元素时,都必须从第一个元素开始进行查找,而双向链表除开头节点和最后一个节点外每个节点中储存有两个指针,这连个指针分别指向前一个节点的地址和后一个节点的地址,这样无论通过那个节点都能够寻找到其他的节点。
咱们也从这个add方法看:
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
无锁机制,也无CAS特性,所以只是单纯的链表,不支持并发操作;
Vector
Vector的数据结构和ArrayList差不多,包含了3个成员变量:elementData,elementCount,capacityIncrement。
1,elementData是Object[]的数组,初始大小为10,会不断的增长。
2,elementCount是元素的个数。
3,capacityIncrement是动态数组增长的系数。
遍历比ArrayList多了一种:
Enumeration enu=vector.elements();
while(enu.hasMoreElements()){E=enu.nextElement();}
从add方法来看这个集合
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
差异无大,多了上面所说的所机制,当在add方法增加synchronized 时,每次仅有一个独占其资源,其他线程需等待其释放;从jvm指令来看,最后synchronized exit 时,其他线程便开始竞争其资源,所以他是线程安全的.但仅仅只是做到了同步,并不支持并发;
总结:(建议多看源码)在一般传统项目中ArrayList便足够使用;使用哪一种,看具体业务场景,不同业务要用到不同数据结构;只有了解了,才能随便使用