以下基本的知识概要在博文开头的链接都有详细的讲述,如需详细可点开阅览。
ArrayList
数组结构实现,查询快,增删慢
JDK1.2版本。运行效率快,线程不安全
LinkedList
链表实现,查询慢、增删快
Vector
数组结构实现,查询快,增删慢
JDK1.0版本。运行效率慢,线程安全
HashSet
此类实现Set接口,由哈希表(实际为HashMap
实例)支持。 对set的迭代次序不作任何保证; 特别是,它不能保证顺序在一段时间内保持不变。 这个类允许null元素。(HashSet
基于HashCode
来实现元素的不可重复)
SortedSort
Set进一步提供其元素的总排序 。 元素使用他们的自然顺序,或通常在创建有序Set
时提供的Comparator
进行排序。 改Set的迭代器将以递增的元素顺序遍历集合。 提供了几个额外的操作来利用订购。 (此接口是该组类似物SortedMap
)。
LinkedHashSet
哈希表和链表实现的Set
接口,具有可预测的迭代次序。 这种实现不同于HashSet
,它维持于所有条目的运行双向链表。 即LinkedHashSet
可以为我们保留插入顺序。
TreeSet
实现升降序,如果需要利用TreeSet进行排序,必须让比较对象实现Comparable
接口,并重写compareTo()
方法,在该方法定义排序条件(按什么排序)、排序方式(升序还是降序)。
HashMap
基于哈希表的实现的Map
接口。 此实现提供了所有可选的映射操作,并允许null
的值和null
键。( 除了它是不同步的,并允许null之外,HashMap
类大致相当于Hashtable
)。这个类不能保证映射的顺序; 特别是,它不能保证该顺序恒久不变。
JDK1.2版本,线程不安全,运行效率快;允许使用null
作为Key
或者是value
。
Hashtable
该类实现了一个哈希表,它将键映射到值。 任何非null
对象都可以用作键值或值。
为了从散列表成功存储和检索对象,用作键的对象必须实现hashCode
方法和equals
方法。
JDK1.0版本,线程安全,运行效率慢;不允许使用null
作为Key
或者是value
。
TreeMap
该方法可以讲Map中的键值对按照键值的进行自然排序;
在之前的文章中有提到过Collection工具类的几个简单的方法:
方法 | 描述 |
---|---|
public static void reverse(List> list) |
反转集合中元素的顺序 |
public static void shuffle(List> list) |
随机重置集合元素中的顺序 |
public static void sort(List> list) |
升序排序(元素必须实现Comparable接口) |
当然,它也提供了多个可以获得线程安全集合的方法:
方法 | 描述 |
---|---|
static |
返回由指定集合支持的同步(线程安全)集合。 |
static |
返回由指定列表支持的同步(线程安全)列表。 |
static |
返回由指定地图支持的同步(线程安全)映射。 |
static |
返回由指定集合支持的同步(线程安全)集。 |
static |
返回由指定的排序映射支持的同步(线程安全)排序映射。 |
static |
返回由指定的排序集支持的同步(线程安全)排序集。 |
①:点进去之后,会把我们的list
传进去,然后根据随机访问类型来进行一个3元运算符的判断,判断的结果就是new
一个新的对象,以new SynchronizedList<>(list))
为例;
②: SynchronizedList<>(list))
构造方法中将用户传进来的list
交给了父类,且把list
付给了自己的一个属性final List
③: SynchronizedList<>(list))
的父类将传进来了list
在不为空的条件下,将它存到了final Collection
,并且会拿到一个内部类对象mutex
即相当于一个锁对象;
④:当使用对集合操作的方法时,这里以add
为例,通过内部类对象使用内部类方法add
,该方法必须拿到所标记才能访问,进而使用该方调用普通的add
方法(⑤);
总体来看,通过对原来的集合进行了简单的包装,即加了一个锁对象mutex
来保证线程的安全性;(mutex
是一个实例变量,每个集合只有一个该实例变量)
如当多个对集合操作的方法需要执行时,需要拿到锁标记才可以操作集合,将原来可能并发执行的线程不安全的集合,升级为串行执行的线程安全的集合;
当我们使用的某个类,不能满足当前需求的时候,不需要对原来的类进行操作,而是再加一个类 ,增强其功能,这就是proxy
,代理;包括现在流行的Spring
框架核心就两块:工厂+代理;
synchronizedList
互斥锁实现,因此性能上没有提升;所以实质上,该方法和使用Vector以及HashTable区别不大;了解读写锁实质上是对该类设计模式的了解,因为该类设计模式上和读写锁有区别也有相似之处放在一起理解会更加印象深刻;提供一个之前关于读写锁相关的链接:
对于读写操作来说,写锁是互斥的,不能并发执行,而读锁不是互斥的可以并发执行,因此对于互斥锁来说,不存在读写之别,因为互斥,所以都不能并发,相比于此,读写锁能够大大提高效率。
首先接触一个新的类,我们不应该去尝试搞懂对他的定义,你需要通过阅读源码理解它实质上做了什么事情,进而你会知道它名字的含义和由来,这是最重要的;
add
为例)array
,付给一个新的对象数组elements
,然后基于该数组进行具体的修改,即将elments
的值赋给newElements
,并将newElements
容量加一,然后把传进来的e
(即这里的字符“A”)存进newElements
,等一切就绪,然后直接将新的数组替换掉该类的实例变量array
;array
进行任何的操作,而仅仅是新建一个对象数组,在新建的对象数组操作完毕之后再进行对array
的替换; public E get(int index) {
return get(getArray(), index);
}
显然写操作并没有锁对象,而是直接存储,支持并发执行;
显然是不互斥的,这得益于写操作中那个巧妙的设计:即在进行写操作的时候不是对类的实例变量啊array
进行操作,而是在新建的数组进行写操作,当写入完毕后直接替换;显然当多个读操作和写操作并发执行的时候,读的过程中不会看到写操作对数字操作的的过程,因此线程是安全的,读写是不互斥的!
这里可以类比App
的更新,比如QQ
在进行版本更新的时候,影响用户的使用吗?显然不会,程序猿会将新版本测试完毕之后,将新版本的代码替换掉旧版本,显然用户在使用的时候,功能上使用并不会受到影响,因此类比这里读写也不是互斥的;
因此,该实现类不仅线程安全,而且在读操作比较多的情境之下,会使程序效率翻倍!!!
使用方式和ArrayList是一模一样的,这也体现了一种设计模式:接口引用指向实现类对象,更容易更换实现,是一种很好的解耦办法
如果在程序开发过程中,我们需要一个线程安全的List
,尽量不使用也不建议使用Vector
,在早期的JDK版本当中我们会使用Vector
,但是因为效率比较低(Vector
所有的方法都加了重入锁,互斥,不支持并发),因此在提供了高效的线程安全的CopyOnWriteArrayList
集合类之后(CopyOnWriteArrayList
读写、读读均支持并发),已经渐渐淘汰了对Vector
的使用;
ArrayList
,加强版读写分离(读写不互斥);copy
一个容器副本、在添加新元素,最后替换引用。ArrayList
无异;
CopyOnWriteArraySet
底层实现是CopyOnWriteArrayList
,但是List的特点是有序有下标,元素不可重复,关键点:怎么实现去重的呢?
private final CopyOnWriteArrayList<E> al; //底层实现 ArrayList
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
add
方法为例,看一下Set的去重原理 /**
* Appends the element, if not present.
*
* @param e element to be added to this list, if absent
* @return {@code true} if the element was added
*/
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}
/**
* A version of addIfAbsent using the strong hint that given
* recent snapshot does not contain e.
*/
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
关键的代码如下,其余和CopyOnWriteArrayList
相似
HashMap
:HashMap
是线程不安全的,在并发环境下,可能会形成环状链表(扩容时可能造成),导致get
操作时,cpu
空转,所以,在并发环境中使用HashMap是非常危险的。
横向看Map
数组状,存储了16个主要元素,接着每一个元素下有挂靠了以链表结构存储的其他元素。
推荐阅读:HashMap实现原理及源码分析
但是怎么保证线程的安全性呢?毫无疑问讲就是通过加锁,因此引入线程安全的HashTable;
HashTable和HashMap区别:
HashTable使用的是synchronized互斥锁,在多线程访问时候,因为一个临界资源 对象只有一把锁,因此当某个线程那到锁标记操作该对象时,那其他线程只能阻塞,不支持并发执行,相当于将所有的操作串行化,在高并发场景中性能就会非常差。
HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,互不影响,就不会导致阻塞状态了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想。
Segment
),使用分段锁思想设计;HashMap
对整体加锁,而是分别为每个Segment
加锁;Segment
时,才需要互斥;Segment
,并行数量为16HashMap
一样待更…