本节内容不仅丰富而且十分有趣实用~
概述
同步容器大致分为两类:
- 由
List
发展来的Vector
、Stack
;由HashMap
发展来的HashTable
(其中K,V均不能为null)Collections
工具类提供的静态工厂方法 –> 均为synchronizedXXXX(List/Set/Map)
的模样。
Vector
看个例子:(好吧,这个测试的框架都快看恶心了ORZ)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 4j
public class VectorExample1 {
public static int clientTotal = 5000;
public static int threadTotal = 200;
private static List<Integer> list = new Vector<>();
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count = i;
executorService.execute(() -> {
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("size:{}", list.size());
}
private static void update(int i) {list.add(i);}
}
运行结果:1
14:10:04.044 [main] INFO com.mmall.concurrency.syncContainer.VectorExample1 - size:5000
看一下Vector
的源码:1
2
3
4
5
6
7
8
9
10
11public synchronized void insertElementAt(E obj, int index) {
modCount++;
if (index > elementCount) {
throw new ArrayIndexOutOfBoundsException(index
+ " > " + elementCount);
}
ensureCapacityHelper(elementCount + 1);
System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
elementData[index] = obj;
elementCount++;
}
其他方法同理,基本都使用synchronized进行标识。
但是!!
同步容器不一定就是线程安全的!
再看例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33@NotThreadSafe
public class VectorExample2 {
private static Vector<Integer> vector = new Vector<>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread thread1 = new Thread() {
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
};
Thread thread2 = new Thread() {
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.get(i);
}
}
};
thread1.start();
thread2.start();
}
}
}
运行结果:
既然Vector的remove和get方法都抛出ArrayIndexOutOfBoundsException异常,那看一下源码吧:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30/**
* Returns the element at the specified position in this Vector.
*
* @param index index of the element to return
* @return object at the specified index
* @throws ArrayIndexOutOfBoundsException if the index is out of range
* ({@code index < 0 || index >= size()})
* @since 1.2
*/
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
public synchronized E remove(int index) {
modCount++;
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
E oldValue = elementData(index);
int numMoved = elementCount - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--elementCount] = null; // Let gc do its work
return oldValue;
}
源码分析:
get
方法出现该异常一定是remove
方法造成的。- 数组越界情况为:
index < 0
或index >= size()
。但既然是remove方法,那应该只能是index小于0或index不存在的情况了。 - 同步容器不一定就能保证线程并发安全。
例子情况分析:(常见的多线程间执行顺序的差异导致)
在其中的for循环中,当一个线程调用get方法时(其中其下标设为i),另一个线程恰好在前一时刻调用了remove方法(恰好其下标也是i),此时下标为i的数据已经不存在,便抛出ArrayIndexOutOfBoundsException异常。
再来看一个Vector的测试例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36public class VectorExample3 {
private static void test1(Vector<Integer> v1) { // foreach
for(Integer i : v1) {
if (i.equals(3)) {
v1.remove(i);
}
}
}
// java.util.ConcurrentModificationException
private static void test2(Vector<Integer> v1) { // iterator
Iterator<Integer> iterator = v1.iterator();
while (iterator.hasNext()) {
Integer i = iterator.next();
if (i.equals(3)) {
v1.remove(i);
}
}
}
// success
private static void test3(Vector<Integer> v1) { // for
for (int i = 0; i < v1.size(); i++) {
if (v1.get(i).equals(3)) {
v1.remove(i);
}
}
}
public static void main(String[] args) {
Vector<Integer> vector = new Vector<>();
vector.add(1);
vector.add(2);
vector.add(3);
test1(vector);
}
}
运行结果:
- main函数执行test1(vector)时,抛出java.util.ConcurrentModificationException;
- main函数执行test2(vector)时,抛出java.util.ConcurrentModificationException;
- main函数执行test3(vector)时,程序正常结束。
结果分析:
使用迭代器iterator或foreach循环(加强版for循环)会抛出并发修改异常;但一般for语句正常结束。
废话不多说,看源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public E next() {
synchronized (Vector.this) {
checkForComodification();
int i = cursor;
if (i >= elementCount)
throw new NoSuchElementException();
cursor = i + 1;
return elementData(lastRet = i);
}
}
public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
synchronized (Vector.this) {
checkForComodification();
Vector.this.remove(lastRet);
expectedModCount = modCount;
}
cursor = lastRet;
lastRet = -1;
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
源码分析:
由于迭代器iterator
或foreach
循环中的remove
操作使得modCount != expectedModCount
,即修改后的count
与期望的count
不一致,定是并发过程中Vector
被修改;但for循环
每次循环都会重新计算i,此时Vector
已被更新……(好吧,我承认,我其实这里还是不太懂,for
循环这里只是我的猜想。)
解决方案:
在讯循环中不要进行修改操作:
- 先查,若有需要进行修改的对象,则做上标记
- 循环之后进行修改
当使用迭代器iterator迭代时,使用synchronized或Lock做同步措施(也可以使用并发容器copyOnWriteArrayList等代替ArrayList或Vector)
Stack
继承了Vector,其两者用法基本一致。只不过它是一个LIFO的数据结构。
HashTable
好吧,其实用的还是那套框架,换一下实例声明的名字就行了。
运行结果:1
15:05:24.433 [main] INFO com.mmall.concurrency.syncContainer.HashTableExample - size:5000
既然是线程安全的,再看一下源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
好吧,还是synchronized标识修饰方法。故HashTable是一个线程安全的同步容器。
Collections工具类方法
synchronizedList
例子:(还是原来的配方,还是熟悉的测试框架…不过实例声明换成Collections的方法)1
private static List<Integer> list = Collections.synchronizedList(Lists.newArrayList());
好吧,非常不幸的告诉你,它的运行结果还是:1
15:20:49.641 [main] INFO com.mmall.concurrency.syncContainer.CollectionsExample1 - size:5000
不管怎样,来都来了,那看一下源码:(没错,注释已经“入党”,已经自动汉化了)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31/ **
*返回由指定的支持的同步(线程安全)列表
*清单。为了保证串行访问,至关重要的是
* <strong>所有</ strong>对支持列表的访问权限已完成
*通过返回的列表。<p>
*
*用户必须手动地同步返回的内容
*迭代时列出:
* <pre>
* List list = Collections.synchronizedList(new ArrayList());
* ......
* synchronized(list){
* Iterator i = list.iterator(); //必须在同步块中
* while(i.hasNext())
* foo(i.next());
*}
* </ pre>
*不遵循此建议可能会导致非确定性行为。
*
* <p>如果指定的列表是,则返回的列表将是可序列化的
*可序列化。
*
* @param <T>列表中对象的类
* @param列出要在同步列表中“包装”的列表。
* @return指定列表的同步视图。
* /
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
注意到,这竟然还有SynchronizedRandomAccessList和SynchronizedList之分?1
2
3
4
5
6
7
8
9
10
11static class SynchronizedRandomAccessList<E>
extends SynchronizedList<E>
implements RandomAccess {
SynchronizedRandomAccessList(List<E> list) {
super(list);
}
.
.
.
}
可知SynchronizedRandomAccessList继承SynchronizedList,并实现了RandomAccess接口。
那这接口是什么鬼?1
2public interface RandomAccess {
}
没了,他就只是个接口……
读者:求你了,看一下注释吧,老哥~
我:emmmm,行~~
1
2
3
4
5
6
7 /**
* 标记接口被<tt> List </ tt>用来指示
*它们支持快速(通常是恒定时间)随机访问。
*此接口的首要目的是允许通用算法更改它们,
*当被应用于随机或顺序访问多个列表时,
*以提供良好性能
*/
读者:泥垢了!!~~
我:emmmm~~~
synchronizedSet
只是把上面的例子的实例换成Set罢了、、
synchronizedMap
只是把上面的例子的实例换Map罢了、、
读者:这仨除了名字还有啥区别?尼莫不四郎肥劳资时间?(四窜口音~)
我:好吧,我真的不想骗大家了,这都被大家发现了~ (花泽香菜兵库北的笑~23333)