同步容器

本节内容不仅丰富而且十分有趣实用~

概述

同步容器大致分为两类:

  1. List发展来的VectorStack;由HashMap发展来的HashTable(其中K,V均不能为null)
  2. 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
@Slf4j
@ThreadSafe
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
11
public 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;
}

源码分析:

  1. get方法出现该异常一定是remove方法造成的。
  2. 数组越界情况为:index < 0index >= size()。但既然是remove方法,那应该只能是index小于0或index不存在的情况了。
  3. 同步容器不一定就能保证线程并发安全。

例子情况分析:(常见的多线程间执行顺序的差异导致)
在其中的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
36
public 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);
}
}

运行结果:

  1. main函数执行test1(vector)时,抛出java.util.ConcurrentModificationException;
  2. main函数执行test2(vector)时,抛出java.util.ConcurrentModificationException;
  3. 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
26
public 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();
}

源码分析:
由于迭代器iteratorforeach循环中的remove操作使得modCount != expectedModCount,即修改后的count与期望的count不一致,定是并发过程中Vector被修改;但for循环每次循环都会重新计算i,此时Vector已被更新……(好吧,我承认,我其实这里还是不太懂,for循环这里只是我的猜想。)
解决方案:
在讯循环中不要进行修改操作:

  1. 先查,若有需要进行修改的对象,则做上标记
  2. 循环之后进行修改

当使用迭代器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
23
public 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
11
static class SynchronizedRandomAccessList<E>
extends SynchronizedList<E>
implements RandomAccess {

SynchronizedRandomAccessList(List<E> list) {
super(list);
}
.
.
.
}

可知SynchronizedRandomAccessList继承SynchronizedList,并实现了RandomAccess接口。
那这接口是什么鬼?

1
2
public interface RandomAccess {
}

没了,他就只是个接口……

读者:求你了,看一下注释吧,老哥~
我:emmmm,行~~

1
2
3
4
5
6
7
/**
  * 标记接口被<tt> List </ tt>用来指示
  *它们支持快速(通常是恒定时间)随机访问。
  *此接口的首要目的是允许通用算法更改它们,
*当被应用于随机或顺序访问多个列表时,
  *以提供良好性能
*/

读者:泥垢了!!~~
我:emmmm~~~

synchronizedSet

只是把上面的例子的实例换成Set罢了、、

synchronizedMap

只是把上面的例子的实例换Map罢了、、

读者:这仨除了名字还有啥区别?尼莫不四郎肥劳资时间?(四窜口音~)
我:好吧,我真的不想骗大家了,这都被大家发现了~ (花泽香菜兵库北的笑~23333)

SupriseMF wechat
欢迎关注微信订阅号【星球码】,分享学习编程奇淫巧技~
喜欢就支持我呀(*^∇^*)~

热评文章