集合
ArrayList
它继承于 AbstractList,实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口。
ArrayList 实现了RandomAccess 接口, RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。 实现了Cloneable 接口,即覆盖了函数 clone(),能被克隆。 实现java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。和 Vector 不同,ArrayList 中的操作不是线程安全的!所以,建议在单线程中才使用 ArrayList,而在多线程中可以选择 Vector 或者 CopyOnWriteArrayList。
ArrayList是一个动态扩容的数组,使用指定的容量或者默认的10容量进行初始化。在插入时,如果容量超出了capacity,则执行**grow()方法进行扩容(1.5倍);如果扩容后还是不够,则扩容到minCapacity,也就是存放新数据所需的容量;最大扩容到Integer.MAX_VALUE。扩容后需要将原有的数据通过Arrays.copyOf(data, newCapacity)**进行拷贝。
与扩容相关ArrayList的add方法底层其实都是System.arraycopy()来实现的,该方法是由C/C++来编写的 native方法,效率较高。
由于支持随机访问,ArrayList的查找速度很快。而关于增删操作,如果是在数组的末端进行增删(类似于堆栈),push和pop不涉及数据移动操作,速度也是很快的。不适合作为队列,因为一定会涉及到整个数组的移动;但是可以用于实现环形队列,通过两个指针来记录读写的位置。
论遍历ArrayList要比LinkedList快得多,ArrayList遍历最大的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销。
LinkedList
LinkedList底层是双向链表,实现了Deque接口,因此,我们可以操作LinkedList像操作队列和栈一样。
链表要检索任意一个值时,需要进行遍历(根据下标情况,选择从头开始或者从尾开始遍历),不支持随机访问,因而查找较慢。但是再进行增删操作时,可以只修改目标节点前后的指针,就能完成修改,而不需要对其余的数据进行移动,因而增删操作较快。
HashMap
HashMap继承了AbstractMap,实现了Map接口。无序,允许为null,非同步。
初始容量16,最大容量为2的31次方,默认装载因子0.75f。转换链表为红黑树的阈值为8(且table_size > 64),红黑树转回链表的阈值为6。
当桶中的链表要转为红黑树时,散列表的最小Size为64。每次扩容为原本容量的两倍,最大为Integer.MAX_VALUE。数组+链表--->散列表。
loadFactor加载因子如果太大则容易碰撞,查找效率低;如果太小则存放的数据会很分散,利用率低;且影响到扩容的频率。
Hash碰撞的解决方案(拉链法):
- java8之前是头插法:原链表为 A ;插入 B 之后为 B -> A;在java8之后使用尾插法,解决了多线程环境下扩容死循环的问题
- 扩容之前为( A -> B -> C)
- 第一步( A );( B -> C ) 。第二步( B -> A) ;( C )。
- 但此时 A 中仍然保存有指向 B 的指针,从而 A 和 B 组成了一个“环形链表”,发生了死循环问题
- 重写equals保证当发生hash碰撞时,能够在链表中正确的存储(不发生重复);同时重写hashCode保证相同的对象hash一定相同
- 链表中元素的个数超出阈值时,链表O(N)重构为红黑树O(logN),以保证在数据量大的情况下能有较好的查询速度
线程不安全问题
- 头插法(jdk1.7)导致的扩容后死循环问题
- jdk1.7中扩容时可能发生数据丢失(略)
- 数据覆盖问题:
- 当put操作插入时,如果没有发生hash碰撞,会直接在table中插入元素
- 两个线程插入两个hash值相同的元素时,都判定没有发生hash碰撞
- A 线程插入后,轮到B线程的时间片,也直接插入,从而覆盖了A的记录,而不是插入链表,导致A的数据丢失
多线程的场景如何处理:
- 使用Collections.synchronizedMap(Map)创建线程安全的map集合;
- Hashtable:Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null(null键放在table的第一个位置)
- ConcurrentHashMap
TreeMap
实现了NavigableMap接口(继承自SortedMap接口),TreeMap是有序的。TreeMap底层是红黑树,它的方法的时间复杂度都不会太高:log(n)。可以使用Comparator或者Comparable来比较key是否相等与排序的问题;如果传入的comparator变量为null,则按照自然顺序排序。非同步。
key不能为null,为null会抛出NullPointException。
Put
- 如果红黑树为null,则新建红黑树
- comparator规则比较,找到合适的位置插入到红黑树中;如果comparator为null,则使用key作为比较器进行比较(需实现Comparable接口)
- 创建新节点,找到其父节点的位置,插入并调整红黑树
getEntry
根据comparator或者key自身进行查找,找到对应的位置。之后据此方法进行get()、remove()、set()
Iterator
TreeMap遍历是使用EntryIterator这个内部类的,其继承自PrivateEntryIterator,实现了Iterator接口
Vector 和 SynchronizedList
SynchronizedList 和 Vector 可能会出现的问题:虽然**size()和get()以及remove()都是原子性的**,但是其内部的**getLast()和deleteLast()是可以交替进行的**,即会导致线程不安全。同时,在遍历Vector的时候,有别的线程修改了Vector的长度,那还是会有问题!
Java推荐使用for-each(迭代器)来遍历我们的集合,好处就是简洁、数组索引的边界值只计算一次。同时要保证线程安全,必须在循环前加锁。
Collections.synchronizedMap
在Collections的内部类,SynchronizedMap的内部维护了一个普通对象Map,还有排斥锁mutex。
SynchronizedMap有两个构造器,如果传入了mutex参数,则将传入的参数赋值给锁;否则将this赋值给锁,即调用SynchronizedMap对象。
之后在操作Map时,所有方法都会上锁synchronized(mutex) { ... }
JUC
ConcurrentHashMap
存储结构和HashMap相似,也是数组+链表,是线程安全的。检索操作不加锁,即Get方法非阻塞,Key 和 Value 值都不允许为 null。
1.7中的存储结构
由Segment(分段)数组Segment<K,V> extends ReentrantLock和HashEntry组成,采用了分段锁机制,即理论上最多支持Segment数量(即容量,默认为16)的线程并发访问。
Put
先定位到Segment,再进行put操作:先尝试获取锁,如果有竞争则通过scanAndLockForPut() 自旋获取锁,如果重试次数达到了MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。
Get
将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。get 方法是非常高效的,因为整个过程都不需要加锁。
1.8中的存储结构
抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)。
Put
- 对key进行散列,获取hash值
- 当表为null时,初始化表
- 如果hash值定位到的Node为空,则可以直接写入不需要加锁,利用 CAS 尝试写入,失败则自旋保证成功。
- 如果当前位置的
hashcode == MOVED == -1,则需要进行扩容,帮助当前线程扩容。 - 散列冲突,利用 synchronized 锁写入数据。:
- 如果数量大于
TREEIFY_THRESHOLD则要转换为红黑树。
- 如果数量大于
Get
get方法是不用加锁的,是非阻塞的。Node节点是重写的,设置了volatile关键字修饰,致使它每次获取的都是最新设置的值。
volatile V val;
volatile Node<K,V> next;
根据key计算hash值,查找指定位置之后,根据当前位置是头结点、链表、红黑树用不同的方式进行查找。
CopyOnWriteArrayList
CopyOnWriteArrayList底层就是数组,加锁就交由ReentrantLock来完成。
/** 可重入锁对象 */
final transient ReentrantLock lock = new ReentrantLock();
/** CopyOnWriteArrayList底层由数组实现,volatile修饰 */
private transient volatile Object[] array;
Add、Set
在添加或修改的时候就上锁lock.lock(),并复制一个新数组,操作在新数组上完成,将array指向到新数组中,最后解锁lock.unlock()。写加锁,读不加锁。
Iterator
CopyOnWriteArrayList在使用迭代器遍历的时候,操作的都是原数组。
- 内存占用:如果CopyOnWriteArrayList经常要增删改里面的数据,经常要执行
add()、set()、remove()的话,都需要复制一个数组出来,那是比较耗费内存的。 - 数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
CopyOnWriteSet
CopyOnWriteArraySet的原理就是CopyOnWriteArrayList。
private final CopyOnWriteArrayList<E> al;
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
并发相关
CAS算法
CAS(Compare And Swap)是乐观锁的一种实现方式,有3个操作数:
- 内存值V
- 旧的预期值A
- 要修改的新值B
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。先比较是否相等,如果相等则替换。
当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值(A和内存值V相同时,将内存值V修改为B),而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试**(否则什么都不做)**
使用版本号、时间戳来防止ABA问题。
Volatile
volatile经典总结:volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性。
- 保证该变量对所有线程的可见性
-
- 在多线程的环境下:当这个变量修改时,所有的线程都会知道该变量被修改了,也就是所谓的“可见性”
- 不保证原子性
-
- 修改变量(赋值)实质上是在JVM中分了好几步,而在这几步内(从装载变量到修改),它是不安全的。只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
- 禁止进行指令重排序。(实现有序性)
COW(CopyOnWrite)
如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作时可以共享同一份资源。