目录
3.1.1、 ArrayList类、LinkedList类、Vector类
3.2.1、HashSet类、TreeSet类、LinkedHashSet类
3.4、Collections工具的一个功能:线程安全地(synchronized)使用非线程安全的集合
4、Concurrent包下的阻塞队列(都是线程安全的,因为使用了Lock)
4.5.2、 看看DelayQueue的源码(这个源码不难,耐心点看吧)
1、集合框架
下面这个图基本包括了java中核心的集合结构,个人觉得还可以。
2、Iterable 接口
这个接口是集合系列的根接口,都要实现它,因为 Iterable 接口里面有个iterator()方法,用于遍历集合里的每一个元素,所以记着:凡是集合类,都可以通过iterator来遍历。
3、 Collection接口
下面是Collection接口的源码,定义了集合最基本的使用规范。不仅是java,其它的开源代码也是这样层层继承和实现,我们可能会比较疑惑,为什么要搞得这么复杂,把代码写在一起不好吗,干嘛弄出这么多的接口文件。层层分开的目的是让框架解耦,这样可以更加灵活地扩展和使用,你把代码都写到一堆,一旦需要更改或者扩展,会让你改得头疼。
public interface Collection<E> extends Iterable<E> {
// 返回集合的元素个数
int size();
// 集合是否为空
boolean isEmpty();
// 集合是否包含该元素
boolean contains(Object o);
// 返回迭代器,用于遍历集合
Iterator<E> iterator();
// 返回一个包含集合所有元素的数组
Object[] toArray();
// 同上,只是能够运行指定类型
<T> T[] toArray(T[] a);
// 添加一个元素,成功返回true,失败返回false
boolean add(E e);
// 删除一个元素,成功返回true,失败返回false
boolean remove(Object o);
// 集合是否包含该集合c(c是否是集合的子集)
boolean containsAll(Collection<?> c);
// 添加一个集合,成功返回true,失败返回false
boolean addAll(Collection<? extends E> c);
// 删除一个集合,成功返回true,失败返回false
boolean removeAll(Collection<?> c);
// 删除满足指定条件的元素
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
// 仅保留包含在指定集合c中的元素
boolean retainAll(Collection<?> c);
// 删除所有元素
void clear();
// 判等
boolean equals(Object o);
// 返回哈希码
int hashCode();
// 返回集合的Spliterator
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
// 返回以此集合作为源的Stream
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
// 返回可能并行的以此集合作为源的Stream
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
}
上面是Collection接口的源码,重点说一下:removeIf()。
removeIf(Predicate<? super E> filter):删除满足指定条件的元素,那怎么去设置条件呢?下面举个例子,删除集合中大于24的元素,其实就是对集合的每个元素进行遍历,然后判断每个元素是否满足条件,如果满足,就删除。很多地方建议removeIf()里面用lambda表达式,确实,如果条件比较简单的话,用lambda表达式更简洁,但是对于复杂的条件,还是用java方式吧。
ArrayList<Integer> collection = new ArrayList<>();
collection.add(22);
collection.add(40);
collection.removeIf(new Predicate<Integer>() {
@Override
public boolean test(Integer integer) {
return integer > 24;
}
});
3.1、 List接口
下面是List接口的源码,除去了从Collection接口继承的方法,只简单介绍了扩展的方法,熟悉这些方法的目的是让我们知道List集合框架下的结构可以有哪些属性和操作。
public interface List<E> extends Collection<E> {
// 在指定位置处添加集合
boolean addAll(int index, Collection<? extends E> c);
// 将所有元素替换,具体的替换操作代码就写在参数位置即可
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
// 给list排序,前提是元素能够比较(实现了Comparator接口)
@SuppressWarnings({"unchecked", "rawtypes"})
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
// 返回index处的元素
E get(int index);
// 设置index处的元素为element
E set(int index, E element);
// 在index处添加元素element
void add(int index, E element);
// 删除index处的元素
E remove(int index);
// 返回指定元素的位置
int indexOf(Object o);
// 返回指定元素在list中最后一个的位置
int lastIndexOf(Object o);
// 迭代器
ListIterator<E> listIterator();
// 迭代器,从index处开始迭代
ListIterator<E> listIterator(int index);
// 截取list的一段出来
List<E> subList(int fromIndex, int toIndex);
}
3.1.1、 ArrayList类、LinkedList类、Vector类
ArrayList类:线程不安全。底层是一个数组,new ArrayList(); 创建时默认为空数组(长度为0),当真正要用的时候,再扩展容量为10,其实也可以理解为ArrayList 的默认容量为10。之后的扩容机制:扩容后的大小= 原始大小*1.5。。
优点:能够随机访问,相对于数组而言,能够扩容。
缺点:容易造成空间浪费,扩展容量的话,也是创建一个新的更大容量的数组,让后将旧数组的数据复制到新数组,麻烦;另外,不方便插入、删除操作。
LinkedList类:线程不安全。底层是一个双向链表。
优点:插入,删除操作非常方便。
缺点:遍历,查询不方便。
Vector类:线程安全。和ArrayList差不多,区别在于:Vector是线程安全的,因为用了synchronized关键字;Vector的扩容机制是扩展1倍,而ArrayList是扩展0.5倍。
3.2、Set接口
Set接口的源码没有什么可说的,因为都是继承自Collection接口的。
3.2.1、HashSet类、TreeSet类、LinkedHashSet类
HashSet类:线程不安全。底层是HashMap的,元素就是HashMap的key,value固定是PRESENT。此处暂不做详细介绍,看下面的HashMap即可。
TreeSet类:线程不安全。底层是TreeMap实现的。
LinkedHashSet类:线程不安全。既有hash功能,以时间复杂度O(1)读取,又有Linked 链表性质,保证元素的先后顺序,底层原理和HashSet差不多,只不过每个元素还有链表指针,维护着先后顺序。
都是用Map来实现的,那Set系列存在的意义呢?其实Set系列都用的少,替我们封装了一层,让我们用起来更方便;另外,Set里的元素必须不重复,这也是Set的优点。
3.3、Map接口
3.3.1、HashMap类
下面的图画得很好理解,所以引用一下,请原作者别喷。
线程不安全。HashMap是由数组+链表实现的,添加一个元素A时,首先要确定插入的位置:先计算其hashCode值,再根据hashCode计算出位置,如果该位置已经有元素了,那就比较两个元素是否是同一个,如果是,则替换,如果不是,即出现哈希冲突,将两个元素形成链表,然后表头插入插入数组的该位置处,新来的元素A放入表头,就形成了链表。当链表的长度超过8时,就自动将链表转为红黑树结构。
默认情况下,数组的初始长度为16。默认的负载因子是0.75。当元素的个数超过 数组长度 * 负载因子 时,就扩容,数组长度翻倍。
负载因子:比如一个哈希数组长度为16时,里面存的元素越多,那么下一次存入元素时发生哈希冲突的可能性就越大,如果数组都满了,再存入一个元素,那哈希冲突概率就是100%。一旦冲突了,就会用链表的方法去解决,你要知道哈希的查询是O(1),但是链表的查询是要从表头挨个挨个地去找的,所以链表的大量存在会影响HashMap的性能。因此,不能等到数组都存满了再扩容,所以设置了一个负载因子,负载因子表明一个数组最多存储的元素比例,比如,因子 = 0.75时,长度为16的HashMap最多存入12个元素,然后就得扩容为32。
扩容:扩容比较麻烦,大小从16变为了32,之前元素的哈希位置需要重新计算;另外还得重新创建数组,将元素复制过去。因此,如果能提前预料到HashMap的最大容量是最好的,就不会出现扩容了。
负载因子的选择需要选好,大了,哈希冲突多,链表多,影响性能;小了,浪费空间。
如果要自己设置hashMap 的大小,最好设置为2的幂,为了让哈希计算出来的索引位置均匀地分布在数组里。如果不是2的幂,那么在二进制取模运算时,有些index结果的出现几率会更大,而有些index可能永远不会出现。
顺便说一下HashMap的哈希算法,hashCode方法是将对象的地址进行处理,高16位与低16位进行异或,再将异或的结果与(数组的长度 - 1)做 & 运算,得出的结果就是对象应该插入在HashMap 数组中的位置。
常用方法:
// 添加键值对
public V put(K key, V value);
// 添加很多键值对
public void putAll(Map<? extends K, ? extends V> m);
// 获取key对于的值
public V get(Object key);
// 是否包含该key
public boolean containsKey(Object key);
// 是否包含该value
public boolean containsValue(Object value);
// 删除key对应的键值对
public V remove(Object key);
// 获取所有value的集合
public Collection<V> values();
// 判断是否为空
public boolean isEmpty();
// 返回一个由所有的key组成的一个Set
public Set<K> keySet();
// 返回一个由所有键值对组成的一个Set
public Set<Map.Entry<K,V>> entrySet();
// 清空
public void clear();
// 获取大小
public int size();
3.3.2、 HashTable类
线程安全。虽然HashTable已经被弃用了,还是介绍一下吧,HashTable的结构与HashMap的一样,两者的主要区别在于:
1、HashTable是线程安全的(sychronized);
2、HashTable不允许key,value为null;
3、HashTable的默认大小为11,扩容时,变为原来大小的2倍+1。
HashTable被弃用了,肯定是有新的类替代它,下文会介绍ConcurrentHashMap。
3.3.3、TreeMap类
线程不安全。TreeMap类的底层结构是一颗红黑树,即自平衡二叉排序树,因此,查询操作的时间复杂度为O(log n)。如果说仅仅查询,添加,删除操作,那TreeMap的性能是不如HashMap的,但是TreeMap是能够自动排序的,这也是TreeMap存在的意义。
每添加/删除一个元素时,TreeMap都会自动调整自己的结构以再次让自己成为平衡二叉排序树,当我们要使用TreeMap的自动排序特性时,需要将所有元素/所有key导出为一个集合,然后这个集合里的内容是有序的(默认按照key升序),当然我们可以指定排序的规则,使得获得的集合按照我们自定义的规则来排序,因此,就需要让key实现Comparable接口。
常用方法:
1、public TreeMap():默认的构造方法,按key自然排序。
2、public TreeMap(Map<? extends K, ? extends V> m):将Map里的键值对直接构造成红黑树,key按照自然排序。
3、public TreeMap(Comparator<? super K> comparator):指定比较器,key按照比较器排序。
4、public V get(Object key):根据key获取value。
5、public V put(K key, V value):添加key-value。
6、Map.Entry<K,V> ceilingEntry(K key):返回大于等于此key 的元素。
7、K ceilingKey(K key):返回大于等于此key的key。
8、void clear():清空。
9、Object clone():返回此 TreeMap实例的浅拷贝。
10、Comparator<? super K> comparator():返回比较器,如果是自然排序,即返回null。
11、boolean containsKey(Object key):是否包含指定key。
12、boolean containsValue(Object value):是否包含指定value。
13、NavigableSet<K> descendingKeySet():以反序的方式返回所有的key
14、NavigableMap<K,V> descendingMap():以反序的方式返回所有键值对。
15、Set<Map.Entry<K,V>> entrySet():返回包含所有键值对的集合。
16、Map.Entry<K,V> firstEntry():返回最小键相关联的键值对 。
17、K firstKey():返回第一个(最低)键。
18、Map.Entry<K,V> floorEntry(K key):返回小于等于此key的键值对。
19、K floorKey(K key):返回小于等于此key 的key。
20、Set<K> keySet():返回包含所有key的集合。
21、Map.Entry<K,V> lastEntry():返回最大key对应的键值对。
22、K lastKey():返回最大key。
23、void putAll(Map<? extends K,? extends V> map):将map添加进TreeMap中。
24、V remove(Object key):删除key。
25、int size():返回键值对的总数量。
26、Collection<V> values():返回包含所有值的集合。
3.3.4、 WeakHashMap类
这是一个和HashMap类似的哈希结构类,常用的操作方法就是map那套,因为平时也没有用过,所以此处暂不作详细介绍,只是这个类是一个弱引用的哈希映射,在垃圾回收时,不管当前内存是否够用,都会回收掉弱引用指向的内存空间的。
3.4、Collections工具的一个功能:线程安全地(synchronized)使用非线程安全的集合
最近发现一个有趣的功能,Collections工具类里面有几个方法,可以使得能够以线程安全的方式去使用非线程安全的集合对象,用到了java 设计模式——装饰者模式,相当于在非线程安全的集合对象上封装一层,用synchronized 修饰的方法去包含非线程安全的集合对象的方法,这样不就可以安全访问了吗。
public static <T> List<T> synchronizedList(List<T> list):参数是一个非线程安全的列表类,返回一个List 对象,用过这个List 对象来操作列表类。
public static <T> Collection<T> synchronizedCollection(Collection<T> c):参数是一个非线程安全的集合类,返回一个Collection 对象,用过这个Collection 对象来操作集合类。
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m):参数是一个非线程安全的映射类,返回一个Map对象,用过这个Map对象来操作映射类。
public static <T> Set<T> synchronizedSet(Set<T> s):参数是一个非线程安全的Set类对象或者Set子类对象,返回一个Set对象,用过这个Set对象来操作。
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m):类似
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s):类似。
优点:面对一个已经存在的数据集合(非线程安全的),除非自己手动写代码,创建一个线程安全的集合对象,将数据复制到集合对象,这样非常麻烦,因此,上面的方法很方便的帮我们做到了。
缺点:返回的都是非常靠顶部的类对象,比如List,Set,我们就只能调用基本的方法,会失去一些功能方法。
4、Concurrent包下的阻塞队列(都是线程安全的,因为使用了Lock)
何为阻塞队列?当一个阻塞队列为空时,其它线程获取元素时,会阻塞,直到队列不为空。当一个阻塞队列满了,其它线程插入元素,会阻塞,直到队列有空位。
Queue接口继承了Collection接口,所以有时候会看到,一个队列类里面,既可以使用Collection里面的某些方法,又可以使用Queue接口里面的方法,而效果是一样的,比如add()方法和put()方法。个人觉得,还是按照队列这套接口去使用吧。
先看看Queue接口定义的操作规范:
public interface Queue<E> extends Collection<E> {
// 往队列尾部添加元素,如果队列已满,抛出异常
boolean add(E e);
// 往队列尾部添加元素,如果队列已满,返回false即可,不会抛出异常
boolean offer(E e);
// 删除队列头,如果队列为空,抛出异常
E remove();
// 弹出队列第一个元素,如果队列为空,返回null
E poll();
// 获取队列第一个元素,但是元素仍然在队列中,如果队列为空,抛出异常
E element();
// 获取队列第一个元素,但是元素仍然在队列中,如果队列为空,返回null
E peek();
}
4.1、ArrayBlockingQueue 类
个人觉得命名是一项非常厉害的本事,光从命名就可以看出一个人的技术水平。“Array”表示底层是数组,“Blocking”表示阻塞,“Queue”表示队列,因此,ArrayBlockingQueue就是底层是一个有界数组,拥有阻塞特性的队列。
构造方法:
1、public ArrayBlockingQueue(int capacity);指定队列大小,默认为非公平方式。
2、public ArrayBlockingQueue(int capacity, boolean fair):指定队列大小,指定是公平方式还是非公平方式。
3、public ArrayBlockingQueue(int capacity, boolean fair, Collection <? extends E> c):将c集合作为队列的初始值。
常用方法,实现Queue接口中的基本方法就不说了:
1、public void put(E e):添加元素至队尾。
2、public boolean offer(E e, long timeout, TimeUnit unit):在指定的时间里还不能添加元素,那就返回false。
3、public E take():删除队列头,如果没有的话,阻塞。
4、public E poll(long timeout, TimeUnit unit):在指定时间内还不能获得队列头,就返回null。
5、public int size():返回队列大小。
6、public int remainingCapacity():队列剩余大小。
7、public void clear():清空队列。
8、public boolean retainAll(Collection <?> c):保留队列中集合c包含的元素。
9、public boolean removeAll(Collection <?> c):删除队列中集合c包含的元素。
10、public boolean removeIf(Predicate <? super E> filter):删除队列中满足指定条件的元素。
11、public void forEach(Consumer <? super E> action):对每个元素进行操作,参数就是操作。
12、public int drainTo(Collection <? super E> c):将队列中的元素提取出来放入集合c,此时队列就空了。
13、public int drainTo(Collection <? super E> c, int maxElements):尽量提取maxElements个元素放入集合c,此时队列就空了。
4.2、LinkedBlockingQueue 类
线程安全。LinkedBlockingQueue的底层是基于单链表实现的,有两个指针 head 和 last ,分别指向队列头和队列尾,因此可以很方便地在队列尾添加元素,在队列头提取元素。LinkedBlockingQueue 默认是一个无界队列,最大长度可达到Integer.MAX_VLAUE,所以如果队列会有大量元素的话,会造成内存不足,因此,最好还是设置一个最大长度。
另外,LinkedBlockingQueue使用了两种锁 putLock 和 takeLock,因此,添加 和 删除 操作是不互斥的,略微地提高了并发量(相对于ArrayBlockingQueue来说)。
LinkedBlockingQueue类的api几乎和ArrayBlockingQueue一模一样,所以此处就不作介绍了。
4.3、SynchronousQueue 类
关于它的原理,这篇文章讲得比较可以:https://www.cnblogs.com/dwlsxj/p/synchronousqueue-unfair-pattern.html
https://www.cnblogs.com/dwlsxj/p/Thread.html
线程安全。SynchronousQueue队列是不存储数据的,当有生产者线程A准备向队列里添加数据时,如果此时没有线程取数据,那么线程A就要进入等待队列,将自己封装成一个QNode(队列节点,公平模式下)或者SNode(栈节点,非公平模式下),节点里面包含了数据 item,先自旋等待,如果自旋一会儿还没有消费者线程出现,就要阻塞,直到有消费线程出现,消费者也是要封装为节点,加入等待队列,去查看后一个节点是不是生产者节点,如果是,则进行匹配,匹配成功,则弹出这2个节点,并返回生成者的数据;如果有消费者线程B要获取队列里的数据,但是没有生产者线程添加数据,那么线程B就要阻塞,将自己封装成节点, item 为 null,表明自己是要取数据的,直到有生产者出现,还是要入队列进行匹配一番,如果匹配成功,然后B获取到了,如果B处于阻塞状态,要先把B唤醒,再让B获取数据。可以看出,生成者线程和消费者线程的数据是直接交互的,相互唤醒。
但是SynchronousQueue什么都不存吗?不是的,它会存储线程,如果是公平模式的话,就用一个队列来存储阻塞线程,如果是非公平模式的话,就用一个栈来存储阻塞队列。
那么问题来了,如果有2个线程向队列添加元素,一起阻塞了,此时来了一个消费线程,那到底是取哪个线程的数据呢?
因此,SynchronousQueue队列是有公平模式和非公平模式之分。
public SynchronousQueue();默认是非公平模式。
public SynchronousQueue(boolean fair); 可以设置模式(设置为公平模式)。
我们再讲讲在公平模式和非公平模式下,那些阻塞线程被唤醒的顺序。对于公平模式,SynchronousQueue用一个队列来存储阻塞线程,FIFO先进先出,即先到的,待会儿先被唤醒。对于非公平模式,用一个栈来存储阻塞线程,先进后出,即先到的,反而后被唤醒。
用图来给大家说明一下原理(假设是公平模式下):
1、如果此时仅有一个生产者线程P1,由于没有消费者线程,所以将P1添加到阻塞队列中。
2、此时又有一个生产者线程P2,由于仍然没有消费者线程,所以将P2添加到阻塞队列中。
3、此时出现了一个消费者线程C1,就应该匹配到阻塞队列中的队列头线程P1,线程P1将数据传递给线程C1,并将P1从阻塞队列中删除。
至此,基本原理明白了吧,如果是只有消费者线程,而没有生成者线程的话,那阻塞队列就应该存储消费者线程了。
举个例子来详细说明(两个条件数据的线程ThreadPut_One,ThreadPut_Two,一个读取数据的线程ThreadTake):
public class ThreadPut_One extends Thread {
private SynchronousQueue<String> synchronousQueue;
public ThreadPut_One(SynchronousQueue<String> synchronousQueue){
this.synchronousQueue = synchronousQueue;
}
@Override
public void run() {
super.run();
try {
this.synchronousQueue.put("DataOne");
System.out.println("DataOne 已经插入完毕");
} catch (InterruptedException e) {
System.out.println("线程ID(" + Thread.currentThread().getId() + "),线程名(" + Thread.currentThread().getName() + ") 抛出异常。");
System.out.println("异常原因:插入数据线程在阻塞时被中断了。");
e.printStackTrace();
}
}
}
public class ThreadPut_Two extends Thread {
private SynchronousQueue<String> synchronousQueue;
public ThreadPut_Two(SynchronousQueue<String> synchronousQueue){
this.synchronousQueue = synchronousQueue;
}
@Override
public void run() {
super.run();
try {
this.synchronousQueue.put("DataTwo");
System.out.println("DataTwo 已经插入完毕");
} catch (InterruptedException e) {
System.out.println("线程ID(" + Thread.currentThread().getId() + "),线程名(" + Thread.currentThread().getName() + ") 抛出异常。");
System.out.println("异常原因:插入数据线程在阻塞时被中断了。");
e.printStackTrace();
}
}
}
public class ThreadTake extends Thread{
private SynchronousQueue<String> synchronousQueue;
public ThreadTake(SynchronousQueue<String> synchronousQueue){
this.synchronousQueue = synchronousQueue;
}
@Override
public void run() {
super.run();
String entity = getEntity();
System.out.println("ThreadTake拿到数据了:" + entity);
}
/**
* 从同步队列synchronousQueue中获取数据,如果此时有往队列插入数据的线程,那么返回该将要插入的数据,否则阻塞
* @return 如果不阻塞,返回取到的数据
*/
public String getEntity(){
try {
return this.synchronousQueue.take();
} catch (InterruptedException e) {
System.out.println("线程ID(" + Thread.currentThread().getId() + "),线程名(" + Thread.currentThread().getName() + ") 抛出异常。");
System.out.println("异常原因:取数据线程在阻塞时被中断了。");
e.printStackTrace();
return null;
}
}
}
测试代码:
public class SynchronousQueueTest {
public static SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>(false);
public static void main(String[] args) throws InterruptedException {
ThreadPut_One threadPut_One = new ThreadPut_One(SynchronousQueueTest.synchronousQueue);
ThreadPut_Two threadPut_two = new
ThreadPut_Two(SynchronousQueueTest.synchronousQueue);
threadPut_One.setPriority(3);
threadPut_two.setPriority(8);
threadPut_One.start();
Thread.sleep(2000);
threadPut_two.start();
Thread.sleep(1000);
ThreadTake threadTake = new ThreadTake(SynchronousQueueTest.synchronousQueue);
threadTake.start();
}
}
运行结果:
分析:首先说一下,代码的运行结果肯定是符合SynchronousQueue的特性的,即便是我自己给线程设置了高低优先级,仍然没有改变SynchronousQueue的特性。有些网友在测试的时候,明明是公平模式,先到先执行,但是测试结果却不对,需要将这两个线程的启动间隔一定的时间,确保第一个启动的线程能够在第二个线程启动之前被操作系统运行,因为如果没有间隔时间的话,两个线程start()之后,就处于就绪状态,等待操作系统调度,但是先调度谁不好说。
4.4、PriorityBlockingQueue 类
线程安全。这是一个自动维护有序性的阻塞队列,一看到Priority就知道是按照优先级的,这里的优先级可不是指线程的优先级,而是说存储在队列的元素必须是可以比较的(必须实现comparable接口,并重写compareTo方法),经过比较后,可以确定谁大谁小,然后按照顺序排好序。
PriorityBlockingQueue实际上是一个小顶堆(完全二叉树形式),用数组去实现的小顶堆,具体怎么用数组去实现的呢?如下,顺便还可以学习一下堆排序:
因为完全二叉树有此特性:对于一个节点的编号N(或者叫数组里的索引),它的父节点的编号 = (N - 1) / 2,它的左子节点的编号 = 2 x N + 1,它的右子节点的编号 = (N + 1) x 2。在编程语言里这种计算公式是正确的,比如 7 和 2 都是int类型, 7 / 2 = 3。
如果添加一个元素呢,如果自动维护小顶堆的特性呢?比如添加一个元素10,会将10添加到末尾,再进行一次堆排序即可。
如果从队列中取走顶元素(7),过程是怎么样的呢?将最后一个元素26直接放到顶端,然后进行一次堆排序即可,每次要换位置的时候,比如26 和 9 换位置,而不是和 13 换位置,因为总是和左右子节点中最小的换位置。
如果删除一个元素呢?同理,将最后一个元素替换掉删除元素,然后进行一次堆排序。
自己的感悟:为什么总是用最后元素呢,为什么添加元素时,总是添加到最后位置呢?因为数组最忌讳的就是大量的移动数据,所以利用堆的最后一个位置或者最后一个元素可以避免大量数据的移动,顶多在堆排序的时候,多次替换2个位置的值。
另外,PriorityBlockingQueue的数组默认大小为11,动态扩展(创建更长的数组,将旧数组数据复制到新数组),因此PriorityBlockingQueue默认一个无界队列。当然也可以设定数组的大小。
构造方法:
1、public PriorityBlockingQueue(); //默认数组初始大小为11.
2、public PriorityBlockingQueue(int initialCapacity); //设定数组大小。
3、public PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator); //指定数组大小,指定比较器
4、public PriorityBlockingQueue(Collection<? extends E> c); //指定队列里的初始化数据。
4.5、 DeleyQueue 类
DelayQueue的作用:是一个延迟队列,队列里的元素都必须设置一个过期时间(比如10秒后过期),然后根据当前系统时刻 + 过期时间 算出过期时刻,元素就按照过期时刻进行升序排序,因此,越早过期的元素越排在队列前面,当有线程来获取元素时,先看看队列头元素是否已经过期,如果没有,就返回null 或者 线程阻塞直到头元素过期再获取它。
DelayQueue本质上是在PriorityBlockingQueue上封装的,因为这样就可以按照过期时刻的大小来排序了,每次获取的元素都是最先过期的元素。
DelayQueue实现了BlockingQueue接口,说明具备阻塞接口的特性,同时队列中元素类型规定了必须实现Delayed接口。
4.5.1 讲讲队列中元素需要满足什么要求
首先看一下Delayed接口:
public interface Delayed extends Comparable<Delayed> {
//获得当前元素剩余时间,以unit为单位换算,并返回
long getDelay(TimeUnit unit);
}
我们的队列元素必须实现Delayed接口,并且要重点实现getDelay()方法 和 compareTo()方法,因为getDelay()方法被用于判断当前元素是否过期,compareTo()方法被用于排序。举个例子Message元素类:
delayNanoTime 存储的是过期时刻,一般人为设置过期时间都是按秒,所以构造方法中转换的时候是按秒转换的,如果你要按照其它的换算,也行。
public class Message implements Delayed {
private String content;
private long delayNanoTime;
public Message(String content, long delayTime){
this.content = content;
this.delayNanoTime = System.nanoTime() + TimeUnit.SECONDS.toNanos(delayTime);
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.delayNanoTime - System.nanoTime(), TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed o) {
long v = this.delayNanoTime - ((Message)o).delayNanoTime;
if(v < 0){
return -1;
}else if(v > 0){
return 1;
}else {
return 0;
}
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public long getDelayNanoTime() {
return delayNanoTime;
}
public void setDelayNanoTime(long delayNanoTime) {
this.delayNanoTime = delayNanoTime;
}
}
4.5.2、 看看DelayQueue的源码(这个源码不难,耐心点看吧)
在看看DelayQueue源码:
值得说一下的是,1、一般这种源码里面讲什么队列是无界的,实际上是有界的,只不过最大允许的容量是Integer的最大值,基本可以看作是无界的;2、正如本章标题所示,线程安全是由Lock实现的,线程的阻塞和唤醒也是Lock机制实现的;3、当判断一个元素是否过期时,使用的是getDelay()方法,即查看剩余时间是否大于0。
至于使用例子就不写了。
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue<E>();
private Thread leader = null;
private final Condition available = lock.newCondition();
//队列初始化,空
public DelayQueue() {}
//队列初始化,将集合中的元素添加到队列中作为初始化数据
public DelayQueue(Collection<? extends E> c) {
this.addAll(c);
}
//添加元素,其实也是调用的offer()方法
public boolean add(E e) {
return offer(e);
}
//添加元素,元素不能为null
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e);
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
//添加元素,其实还是用的offer()方法
public void put(E e) {
offer(e);
}
//添加元素,超时参数没有意义,因为添加元素从来不阻塞,所以无所谓超时。
public boolean offer(E e, long timeout, TimeUnit unit) {
return offer(e);
}
//获取队列头元素并删除(前提是过期时间到了),否则返回null
//如果判断元素是否过期呢?通过判断元素的剩余时间是否小于等于0
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return q.poll();
} finally {
lock.unlock();
}
}
//功能和poll一样,但是如果没有一个过期时间到了的头元素,就会等待
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();
if (first == null)
available.await();
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return q.poll();
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
//获取并删除队列头元素(前提是过期了),如果在指定时间内,还无法获得过期的队列头元素,返回null
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();
if (first == null) {
if (nanos <= 0)
return null;
else
nanos = available.awaitNanos(nanos);
} else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return q.poll();
if (nanos <= 0)
return null;
first = null; // don't retain ref while waiting
if (nanos < delay || leader != null)
nanos = available.awaitNanos(nanos);
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
long timeLeft = available.awaitNanos(delay);
nanos -= delay - timeLeft;
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
//返回队列头元素,如果队列为空,就返回bull
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.peek();
} finally {
lock.unlock();
}
}
//返回队列实际的大小
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.size();
} finally {
lock.unlock();
}
}
//返回队列头的元素(前提是过期了),否则返回null
private E peekExpired() {
// assert lock.isHeldByCurrentThread();
E first = q.peek();
return (first == null || first.getDelay(NANOSECONDS) > 0) ?
null : first;
}
public int drainTo(Collection<? super E> c) {
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
int n = 0;
for (E e; (e = peekExpired()) != null;) {
c.add(e); // In this order, in case add() throws.
q.poll();
++n;
}
return n;
} finally {
lock.unlock();
}
}
//这个方法上文将过,去看看吧
public int drainTo(Collection<? super E> c, int maxElements) {
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
if (maxElements <= 0)
return 0;
final ReentrantLock lock = this.lock;
lock.lock();
try {
int n = 0;
for (E e; n < maxElements && (e = peekExpired()) != null;) {
c.add(e); // In this order, in case add() throws.
q.poll();
++n;
}
return n;
} finally {
lock.unlock();
}
}
// 清空队列
public void clear() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.clear();
} finally {
lock.unlock();
}
}
//返回队列剩余容量,但是没什么意义,反正队列是无界的,就返回Integer最大值吧
public int remainingCapacity() {
return Integer.MAX_VALUE;
}
//将队列转换为数组
public Object[] toArray() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.toArray();
} finally {
lock.unlock();
}
}
//将队列转换为数组并存在数组a里
public <T> T[] toArray(T[] a) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.toArray(a);
} finally {
lock.unlock();
}
}
//删除一个元素,不管它是否过期,只要存在于队列中,就删除
public boolean remove(Object o) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.remove(o);
} finally {
lock.unlock();
}
}
//这个方法不用管,它是供Irt迭代器调用的
void removeEQ(Object o) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
for (Iterator<E> it = q.iterator(); it.hasNext(); ) {
if (o == it.next()) {
it.remove();
break;
}
}
} finally {
lock.unlock();
}
}
//返回此队列的迭代器
public Iterator<E> iterator() {
return new Itr(toArray());
}
/**
* 定义了一个针对DelayQueue队列的迭代器,是在q数组的副本上进行迭代的.
*/
private class Itr implements Iterator<E> {
final Object[] array; // Array of all elements
int cursor; // index of next element to return
int lastRet; // index of last element, or -1 if no such
Itr(Object[] array) {
lastRet = -1;
this.array = array;
}
public boolean hasNext() {
return cursor < array.length;
}
@SuppressWarnings("unchecked")
public E next() {
if (cursor >= array.length)
throw new NoSuchElementException();
lastRet = cursor;
return (E)array[cursor++];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
removeEQ(array[lastRet]);
lastRet = -1;
}
}
}
4.6、LinkedTransferQueue 类
这个队列有点牛掰,优势比较明显,相当于SynchronousQueue 和 LinkedBlockingQueue 的综合,队列既有实际的存储空间,又是线程安全(没有使用锁,因此更高效),建议学一学。
先看一个LinkedTransferQueue 类内部的一个静态内部类(队列的节点类Node):
static final class Node {
// 当前节点是否是数据节点,true代表是,false代表是请求获取数据节点
final boolean isData;
// 节点的内容,如果是数据节点,则不为null,如果是请求获取数据节点,为null
volatile Object item;
// 下一个节点引用
volatile Node next;
// 如果操作此节点的线程是阻塞的,那么waiter就记录该线程,否则为null
volatile Thread waiter;
/**
* 还有其他的方法和成员,此处省略吧,因为都是系统底层级别的,本人一时无法弄明白
*/
}
看完Node类之后,我们就明白了为什么很多资料里都说 LinkedTransferQueue 比 SynchronousQueue 多了存储数据的队列。这种说法非常肤浅,可能这样说的人也没有弄明白这两者的原理吧。真相是这样的:SynchronousQueue中也是有队列(公平模式下,如果是非公平模式,那就是栈了),只不过该队列只用于存储阻塞线程,然后让消费者线程和生产者线程去匹配,至于两者之间的数据传输,不会经过SynchronousQueue队列的,因此才说SynchronousQueue队列是没有数据缓冲的;而LinkedTransferQueue队列中的节点所包含的信息更加丰富,不仅可以包含线程,还可以包含数据。 而且LinkedTransferQueue队列的优点还远不止于此,稍后介绍。
再看看LinkedTransferQueue源码:
其实原理上的理解可以参照SynchronousQueue的原理图,只不过,LinkedTransferQueue队列节点包含了数据,而其操作队列的方法不仅仅是阻塞的,还可以是非阻塞的,限时的(NOW,ASYNC,SYNC,TIMED)。
NOW(立刻去获取数据或者传输数据,能做到就OK,做不到也要立即返回,不阻塞),针对tryTransfer()方法和poll()方法。
ASYNC(异步,不阻塞,因为是添加数据,自己是生产者线程,如果有消费者线程了,那就直接给它,不会阻塞,如果没有消费者线程,那就将自己作为数据节点添加到队列中,添加完就返回,也不阻塞),针对put、offer、add方法。
SYNC(同步,阻塞的,take方法是获取数据,如果当前没有生产者线程(数据节点),就将自己作为请求数据节点放入队列中阻塞等待吧;transfer方法是传递数据给消费者线程,如果此时有消费者线程,就给它,如果没有,就将自己作为数据节点放入队列中阻塞等待),针对take,transfer方法。
TIMED(限时的,如果在规定时间内没有完成操作,该阻塞就阻塞,该返回null就返回null)。
public class LinkedTransferQueue<E> extends AbstractQueue<E>
implements TransferQueue<E>, java.io.Serializable {
// 如果当前系统是多处理器的,那么MP是true,否则是false。这个有点高级哦~~
private static final boolean MP = Runtime.getRuntime().availableProcessors() > 1;
private static final int NOW = 0; // 立即执行,不阻塞,大不了就返回null
private static final int ASYNC = 1; // 异步执行,用于添加元素,因为添加元素不会阻塞
private static final int SYNC = 2; // 同步执行,该阻塞就阻塞
private static final int TIMED = 3; // 立即执行,如果在一定时间内还不行,就返回null
// 此方法是核心方法,其它操作方法几乎都是调用的此方法
private E xfer(E e, boolean haveData, int how, long nanos){...}
// 添加元素,不阻塞的,实际还是调用的xfer()方法
public void put(E e) {
xfer(e, true, ASYNC, 0);
}
// 添加元素,不阻塞,和put一样,只不过添加后,会返回一个true而已
// 这个超时时间一点用都没有,因为添加元素不阻塞,因此不会超时
public boolean offer(E e, long timeout, TimeUnit unit) {
xfer(e, true, ASYNC, 0);
return true;
}
// 和上面这个offer方法一模一样
public boolean offer(E e) {
xfer(e, true, ASYNC, 0);
return true;
}
// 添加元素,和offer方法一模一样
public boolean add(E e) {
xfer(e, true, ASYNC, 0);
return true;
}
// 尝试将数据立刻传输给一个消费者线程,如果消费者线程不存在,不会将该数据放入队列,
//并且立即返回false,如果存在,则立即将数据传给等待线程,并返回true
public boolean tryTransfer(E e) {
return xfer(e, true, NOW, 0) == null;
}
// 将数据传输给一个消费者线程,如果队列头就是一个消费者线程,那么就立即给它,否则,就将自己
// 排到队列尾,阻塞等待消费者线程出现
public void transfer(E e) throws InterruptedException {
if (xfer(e, true, SYNC, 0) != null) {
Thread.interrupted(); // failure possible only due to interrupt
throw new InterruptedException();
}
}
// 这个同理,只不过多了一个超时时间,如果在给定时间内,还没有完成数据传输,就返回false
public boolean tryTransfer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (xfer(e, true, TIMED, unit.toNanos(timeout)) == null)
return true;
if (!Thread.interrupted())
return false;
throw new InterruptedException();
}
// 从队列中获取一个数据,如果没有数据,那就将自己作为请求数据的节点添加到队列中,阻塞等待
public E take() throws InterruptedException {
E e = xfer(null, false, SYNC, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}
// 在指定时间内如果没有获取到数据,返回null即可
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E e = xfer(null, false, TIMED, unit.toNanos(timeout));
if (e != null || !Thread.interrupted())
return e;
throw new InterruptedException();
}
// 立即去获取数据,如果没有,立刻返回null
public E poll() {
return xfer(null, false, NOW, 0);
}
// 删除某个元素,得先找,只有确定在队列中,才能删除
public boolean remove(Object o) {
return findAndRemove(o);
}
// 是否包含该元素
public boolean contains(Object o) {
if (o == null) return false;
for (Node p = head; p != null; p = succ(p)) {
Object item = p.item;
if (p.isData) {
if (item != null && item != p && o.equals(item))
return true;
}
else if (item == null)
break;
}
return false;
}
// 返回队列的剩余容量,没什么意义,返回的是Integer的最大值
public int remainingCapacity() {
return Integer.MAX_VALUE;
}
// 将队列的每个元素都序列化,添加到输出流中
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
s.defaultWriteObject();
for (E e : this)
s.writeObject(e);
// Use trailing null as sentinel
s.writeObject(null);
}
// 从对象的序列化流中读取对象,然后把对象添加到队列中,相当于反序列化
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
for (;;) {
@SuppressWarnings("unchecked")
E item = (E) s.readObject();
if (item == null)
break;
else
offer(item);
}
}
总结一下,LinkedTransferQueue的节点类型更加丰富,节点的内容也更加多(包含数据),队列的操作方法的方式更加丰富。你想想,一个队列中每个节点都包含了数据,是不是和LinkedBlockingQueue有点像呀,另外,消费数据操作和生成数据操作是相互匹配的,如果仅有消费者,没有生产者,那消费者就要阻塞了,和SynchronousQueue的原理基本类似,。另外,LinkedTransferQueue队列的操作方法更加丰富,分为NOW,同步,异步,限时,这样效率更加,比如我就总是用NOW方法或者异步方法,就可以减少阻塞了呗。
5、并发性集合
5.1、ConcurrentHashMap类
作用:和HashMap的作用基本类似,就是一个散列映射集合,只不过ConcurrentHashMap是线程安全的,并且支持多线程并发访问。
结构:HashMap是数组 + 链表(8节点以上是红黑树),而ConcurrentHashMap的结构也是数组 + 链表(8节点以上是红黑树),只不过数据的元素类型是Node,Node是它内部的一个内部类,在多线程中,不是以整个ConcurrentHashMap对象作为锁,而是以Node节点作为锁,只要是多个线程访问的Node不一样,就不会相互阻塞,因此,ConcurrentHashMap具有高并发访问的特性。
先看看内部类Node的源码,基本了解一下Node是什么样的:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //哈希值,用于确定自己所存储的位置
final K key; //key 键
volatile V val; //value 值
volatile Node<K,V> next; //下一个节点,用于在形成链表时用的
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString(){ return key + "=" + val; }
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
// 判等方法
public final boolean equals(Object o) {
Object k, v, u; Map.Entry<?,?> e;
return ((o instanceof Map.Entry) &&
(k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
(v = e.getValue()) != null &&
(k == key || k.equals(key)) &&
(v == (u = val) || v.equals(u)));
}
// 查找指定指定key值的Node节点
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
}
再看看ConcurrentHashMap的部分基本属性, 对于ConcurrentHashMap的工作原理,个人的理解是:创建的时候,可以设置最大容量,因此最多只能存储指定容量的键值对,而且不会扩容,比如设置最大容量为16,然而Node数组的长度不会是16,而是32,为什么实际的数组大小比最大容量大2倍呢?因为要避免哈希冲突呀;另外,可以使用默认构造方法,数组大小默认为16,负载因子为0.75,存入数据时,如果发生哈希冲突,就以链表法解决,当某个链表的长度超过8时,就转换为红黑树,当红黑树的节点小于6的时候,就转换为链表;当添加数据后,数据的数量大于 16 * 0.75 时,说明要扩容了,就会创建一个更大容量(2倍)的数据,将原数组的节点复制到新数组去,节点的位置也会发生改变(通过相应的计算)。
连续几次扩容,数组会很大,但是当map里的元素变得很少时,会不会缩小容量呢?会。
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
// 默认的数组大小为16
private static final int DEFAULT_CAPACITY = 16;
// 数组最大容量,几乎可以认为是无界的
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 默认的并发级别
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 默认的负载因子
private static final float LOAD_FACTOR = 0.75f;
// 链表长度超过多少就变为红黑树,默认为8
static final int TREEIFY_THRESHOLD = 8;
// 红黑树的节点数小于多少就变为链表形式,默认为6
static final int UNTREEIFY_THRESHOLD = 6;
// ConcurrentHashMap 默认能存储的元素的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
// Node数组,懒加载模式,仅在第一个元素存入时才创建空间
transient volatile Node<K,V>[] table;
// 下一个Node数组,在扩容的时候用
private transient volatile Node<K,V>[] nextTable;
}
常用方法:
1、public V put(K key, V value); //添加键值对
2、public void clear(); //清空所有内容
3、public int size(); //返回map中元素个数
4、public Collection<V> values(); //返回包含所有value的集合
5、public boolean contains(Object value); //是否包含某个键值对
6、 public boolean containsKey(Object key)l //是否包含某个key
7、public boolean containsValue(Object value); //是否包含某个value
8、public V get(Object key); //根据key获取value
9、public boolean replace(K key, V oldValue, V newValue); //对于某个key-value,用新value替换旧value
10、public V replace(K key, V value); //替换value,不用指定旧value。
11、public V remove(Object key); //删除
12、 public boolean isEmpty(); //是否为空
13、public void putAll(Map<? extends K, ? extends V> m); //添加一个键值对集合
5.2、ConcurrentSkipListMap 类
介绍:按照key有序的映射结合,支持并发。(跳表结构)
都有一个ConcurrentHashMap了,为什么还要搞一个这个呢?原因有2个:
1、有序的(按照key排序)。
2、同等数据量下,并发量越高,ConcurrentSkipListMap的性能体现比ConcurrentHashMap更好。
缺点:存取数据的时间复杂度为O(log N),而ConcurrentHashMap比这个要小很多,具体多少不确定,因为链表和红黑树会影响读写速度。
ConcurrentSkipListMap的结构原理是什么呢?
它是一种跳表结构实现的,什么是跳表结构呢,下面给个图说明一下,跳表结构如下图,是个链表网,分为多层,用空间去换时间,比如获取数据23,从level 2 的 5 —>17 , 17 垂直下来到最底层,再从 17 —> 23,这样的速度更快,因为,对于一个单链表的话,就像在最底层要找到 23,需要进行 6 此比较操作 和 5 次索引移动,但是如果使用跳表的话,从level 2开始,只需要进行 4 次比较操作 和 4 次索引移动。其实这个例子非常简短,体现不出太大的优势,如果链表很长,元素非常多,我们从高级别的level开始,一下就能精确地跳到链表的中间去,省去了很多时间。感觉有点像二分查找哦。
为什么要用跳表结构呢?对于有序数组,能够以 O(1) 的时间获取到某个位置的数据,但是插入/删除数据的时间复杂度比较大;对于链表,查找的时间复杂度大,但是方便插入和删除数据。两者各有优势,鱼和熊掌不可兼得,于是ConcurrentSkipListMap就做了一个平衡(用空间作为代价),使用跳表结构,能够快速定位到某个区间,然后一层一层地下去,越来越精准,直到找到为止。
源码分析:上面的图只是为了方便理解,下面的图才是ConcurrentSkipListMap 的真实结构,由3种节点组成:Node、Index 和 HeadIndex 。Node是基础节点,真正用于存储<key,value>的节点;Index是用于快速定位区间的索引节点,不存储键值对,只是有3个引用,分别指向Node节点,同一层level的右索引 Index,下一层 level 对应的Index;HeadIndex是每一层索引的头结点,包含一个Index,而且还有一个level字段。
Node类的结构。
static final class Node<K,V> { //这是典型的单链表节点
final K key;
volatile Object value;
volatile Node<K,V> next;
}
Index类的结构。
static class Index<K,V> {
final Node<K,V> node; //指向存储了<k, v>的节点Node
final Index<K,V> down; //指向了下一层对应的索引节点
volatile Index<K,V> right; //指向同一层的右边索引节点
}
HeadIndex 类的结构。
static final class HeadIndex<K,V> extends Index<K,V> {
final int level; //所处层级
HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {
super(node, down, right);
this.level = level;
}
}
至此,ConcurrentSkipListMap的结构原理算是理明白了,但是还有很多细节没有弄明白,比如为什么它是高并发的,在此做个标记,以后补充。
基本使用方法。
由于ConcurrentSkipListMap是key有序的,肯定需要比较器,所以源码里面有一个字段:
final Comparator<? super K> comparator; 代表比较器。
1、构造方法
public ConcurrentSkipListMap(); //默认构造方法,比较器为null,就会按照key进行自然排序,有些情况下排序结果未知。
public ConcurrentSkipListMap(Comparator<? super K> comparator); //指定比较器
public ConcurrentSkipListMap(Map<? extends K, ? extends V> m); //带有初始化数据,但是比较器为null
public ConcurrentSkipListMap(SortedMap<K, ? extends V> m); //带有初始化数据,比较器就是m的比较器
2、常用方法
Map.Entry<K,V> ceilingEntry(K key); // 返回与大于等于给定键的最小键关联的键-值映射关系;若没有,则返回 null
K ceilingKey(K key); // 返回大于等于给定键的最小键;若没有,则返回 null
void clear(); // 清空
ConcurrentSkipListMap<K,V> clone(); // 返回此 ConcurrentSkipListMap 实例的浅表副本
Comparator<? super K> comparator(); // 返回key的比较器
boolean containsKey(Object key); // 是否包含指定key
boolean containsValue(Object value); // 是否包含指定value
NavigableSet<K> descendingKeySet(); // 返回包含所有键的逆序集合
ConcurrentNavigableMap<K,V> descendingMap(); // 返回包含所有<key, value>的逆序映射
Set<Map.Entry<K,V>> entrySet(); // 返回包含所有<key, value>的逆序集合
boolean equals(Object o); //判断两个map是否相等
Map.Entry<K,V> firstEntry(); //返回排在第一个位置的map
K firstKey() ; //返回第一个key
Map.Entry<K,V> floorEntry(K key) // 返回与小于等于给定键的最大键关联的键-值映射关系;若没有,则返回 null
K floorKey(K key) //返回小于等于给定键的最大键;若不存在,则返回 null
V get(Object key) //返回指定key的value
boolean isEmpty() //判断是否为空
NavigableSet<K> keySet() //返回包含所有key的集合
Map.Entry<K,V> lastEntry() //返回最后一个<key, value>
K lastKey() //返回最后一个key
V put(K key, V value) //添加一个键值对
V remove(Object key) //删除指定key的键值对
boolean remove(Object key, Object value) //删除该<key, value>
V replace(K key, V value) //用value替换掉key对应的值
boolean replace(K key, V oldValue, V newValue) //用newValue替换掉<key, oldValue>中的oldValue
int size(); //返回<key, value>数量
Collection<V> values() //返回包含所有value的集合
5.3、ConcurrentSkipListSet 类
ConcurrentSkipListSet 类 是基于ConcurrentSkipListMap实现的,就像TreeSet 是 基于TreeMap实现,一样一样的,ConcurrentSkipListSet 里面的每一个元素都作为 ConcurrentSkipListMap 的 key。
因此,ConcurrentSkipListSet中的元素不能重复。