什么是同步容器
同步容器通过synchronized关键字修饰容器,保证同一时间内只有一个线程在使用容器,从而使得容器线程安全。
什么是并发容器
允许多线程同时使用容器,并且保证线程安全。核心:锁,cas,cow,分段锁。
同步容器
vector:
stack: Stack实现的是先进后出的栈,入栈出栈都使用synchronized
hashTable:
实现Map接口,实现的功能和HashMap基本一致(HashTable 不可出现null,HashMap键值可以使用null);
hashTable在使用的时候也用synchronized修饰了方法
Arraylist、HashMap 不是线程安全的。
Collections提供了同步集合类
List list = Collections.synchronizedList(new ArrayList());
Set set = Collections.synchronizedSet(new HashSet());
并发容器
CopyOnWriteArrayList:写的时候复制容器,添加元素的时候,先复制一个新的容器。读写分离。
读: 从原来的容器读
写:把数据写入到新的容器。
写数据过程:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
缺点
1.内存占有问题。
2.数据一致性问题:只能保证数据的最终一致性,不能保证数据的实时一致性。
ConcurrentHashMap
HashMap:是线程不安全的,在并发情况下会产生死锁。
HashTable:线程安全,代价大,简单粗暴,get/put所有的操作都是synchronized修饰。相当于给整个HashTable加了一把大锁。
ConcurrentHashMap: 可以支持并发的读写,支持高并发的检索和更新。
在JDK1.7: 数组+链表+分段锁
在JDK1.8: 数组+链表+红黑树
不能使用null作为key或者value
取消segments,采用HashEntry保存数据
1.当一个位置有多个元素的时候,ConcurrentHashMap优先采用链表的形式存储
2.当链表的元素个数大于8个,并且数组的长度小于64时,进行扩容。
3.当链表的元素个数大于8个,并且数组长度大于64时,链表转为红黑树。
原理:
1.第一次添加元素:
初始长度:16
—>用hash算法算出元素存放在数组的哪个位置
—>如果出现放在同一个位置的时候,优先采用链表的形式存放
—>在同一个位置的个数达到8个以上的时候,如果数组的长度小于64的时候,扩容数组。
—>数组的长度大于等于64,会将该节点的链表转换为红黑树。
put:
- 如果没有初始化table先调用initTable()方法进行初始化过程
- 如果没有hash冲突就直接CAS插入。
- 如果数组在扩容操作就先进行扩容。
- 如果存在hash冲突,就加锁来保证线程安全(一种是链表形式,直接遍历到尾端插入,一种是红黑树就按照红黑树的结构插入)
- 最后一个如果链表的数量大于8,就要先转换成红黑树的结构
- 如果添加成功,就调用addCount()方法统计size,并且检查是否要扩容
代码:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
队列 BlockingQueue
单项队列:只能向队尾添加元素,从对头删除元素。
双向队列:头和尾都支持元素的入队和出队。
放入数据:
add(Object)
offer(Object): 如果队列可以容纳,则返回true,否则返回false,该方法不阻塞当前执行方法的线程。
put(e): 把object加入到BlockingQueue,如果队列没有空间,则调用此方法的线程被阻塞直到队列有空间在继续。
获取数据:
poll(time); 取走队列排在首位的元素,如果不能立即取走,则可以等设置的time参数的时间,取不到返回false;
poll(timeout):
take(object):取走队首的元素,如果队列为空,当前线程阻塞,直到队列中有新的元素加入。
实现类
LinkedBlockingQueue
final Object[] items;
ArrayBlockingQueue
DelayQueue: 元素只能当其指定的延迟时间到了,才能从队列中获取该元素,
priorityBlockingQueue: 基于优先级的阻塞队列。
SynchronousQueue: 无缓冲的等待队列。(只有一个元素)
ArrayBlockQueue
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
添加元素:
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();
}
}