最近写代码的时候傲然F3点进去看了一下源代码,看了之后有几个问题,然后找资料解决了,记录一下。
CopyOnWriteArrayList在写的时候复制整个数组,这样操作其实比较耗时,所以这个List适合写少读多的并发场景,此时有很好的性能。 读操作(get(),indexOf(),isEmpty(),contains())不加任何锁,而写操作(set(),add(),remove())通过Arrays.copyOf()操作拷贝当前底层数据结构(array),在其上面做完增删改等操作,再将新的数组置为底层数据结构,同时为了避免并发增删改, CopyOnWriteList在这些写操作上通过一个ReetranLock进行并发控制。
数据结构:Object数组(含有get和set方法)
关于读和写:
链表中单个元素的get操作,直接返回数组中对于的数据。
1
2
3
|
public
E get(
int
index) {
return
(E)(getArray()[index]);
}
|
链表中元素的设置set操作,先copy一份,然后添加元素,之后再调用setArray来重新赋值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public
E set(
int
index, E element) {
//加锁操作
final
ReentrantLock lock =
this
.lock;
lock.lock();
try
{
Object[] elements = getArray();
//获取原先数据
Object oldValue = elements[index];
//比较新旧数据是否相同
if
(oldValue != element) {
int
len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
}
else
{
// Not quite a no-op; ensures volatile write semantics这里一般看不懂
setArray(elements);
}
return
(E)oldValue;
}
finally
{
lock.unlock();
}
}
|
1、在set中有个疑问,就是为啥在新老对象相同的时候,还调用了一下setArray函数呢?
http://cs.oswego.edu/pipermail/concurrency-interest/2010-February/006886.html
从注释来看,是确保volatile的写的语义。至于为啥,看了邮件列表中,应该是为了确保happends-before特性。
http://blog.csdn.net/fg2006/article/details/6397142
2、看了一下,貌似这个方法调用的比较多System.arraycopy,Arrays.copyof 里面也是调用的这个方法,这个方法是干啥的?
这个在System中是一个native方法,负责数组拷贝,参数简单明了。
1
2
3
4
5
6
7
8
9
|
/* @param src the source array.
* @param srcPos starting position in the source array.
* @param dest the destination array.
* @param destPos starting position in the destination data.
* @param length the number of array elements to be copied.
*/
public
static
native
void
arraycopy(Object src,
int
srcPos,
Object dest,
int
destPos,
int
length);
|
写了个程序对比了一下使用System和自己循环进行数据拷贝的效率问题,还是原生的给力啊。
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
|
import
java.util.Arrays;
import
java.util.List;
import
java.util.concurrent.CopyOnWriteArrayList;
public
class
ListTest {
public
static
void
main(String[] args) {
int
size =
1000
;
//每次copy运行的次数
int
runTime =
100000
;
String[] src = genString(size);
String[] destSysCopy =
new
String[size];
String[] destSelfloop =
new
String[size];
/**通过System.arrayCopy来复制*/
long
s1 = System.currentTimeMillis();
for
(
int
i=
0
;i<runTime;i++){
System.arraycopy(src,
0
, destSysCopy,
0
, src.length);
}
System.out.println(
"arrayCopy:"
+(System.currentTimeMillis()-s1)+
",size="
+destSysCopy.length);
/**通过循环来完成复制*/
long
s2 = System.currentTimeMillis();
for
(
int
i=
0
;i<runTime;i++){
for
(
int
j=
0
;j<src.length;j++){
destSelfloop[j] = src[j];
}
}
System.out.println(
"selfLoop:"
+(System.currentTimeMillis()-s2)+
",size="
+destSelfloop.length);
}
/**初始化算是数组*/
private
static
String[] genString(
int
size){
String[] a =
new
String[size];
for
(
int
i=
0
;i<size;i++){
a[i] =
"iamzhongyong+"
+i;
}
return
a;
}
}
|
1
2
|
arrayCopy:
78
,size=
1000
selfLoop:
453
,size=
1000
|
3、关于迭代器
CopyOnWriteList所实现的迭代器其数据也是底层数组镜像,所以在CopyOnWriteList进行interator,同时并发增删改CopyOnWriteList里的数据实不会抛“ConcurrentModificationException”,当然在迭代器上做remove,add,set也是无效的(抛UnsupportedOperationExcetion),因为迭代器上的数据只是当前List的数据数组的一个拷贝而已。
1
2
3
|
public
Iterator<E> iterator() {
return
new
COWIterator<E>(getArray(),
0
);
}
|
1
2
3
4
5
6
7
8
9
10
|
public
void
remove() {
throw
new
UnsupportedOperationException();
}
public
void
set(E e) {
throw
new
UnsupportedOperationException();
}
public
void
add(E e) {
throw
new
UnsupportedOperationException();
}
|
4、为啥数据对象加了volatile,不加有啥问题?
private volatile transient Object[] array;
Volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。(http://www.infoq.com/cn/articles/ftf-java-volatile )
如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。
Volatile变量修饰符如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度。
锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。
互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。
可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。
Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。
Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。
至此能够清楚了,array的值在写的时候,通过synchronized来保证原子性,但是此时还有别的线程可能用到了array这个对象,例如在读的时候,这时候通过volatile能够保证可见性。
5、CopytOnWriteArrayList中写方法使用RentrantLock来实现同步,为啥不用synchronized呢?
这个可能是历史原因,在jdk6之前,synchronized在多线程情况下吞吐量下降和明显,而Lock的话会好很多,所以并发包中采用了Lock的机制,后来JDK6对于synchronized锁做了优化,两者持平,所以正在synchronized能够满足需求的情况下,优先考虑使用这个,毕竟非常方便吗。
重入锁:指的是同一个线程多次试图获取它所占有的锁,请求会成功。当释放锁的时候,直到重入次数清零,锁才释放完毕。
搞个例子对比一下现在的情况,例子参照:http://www.blogjava.net/killme2008/archive/2007/09/14/145195.html
Counter接口:
1
2
3
4
|
public
interface
Counter {
public
long
getValue();
public
void
increment();
}
|
三种情况下的实现:
1
2
3
4
5
6
7
8
9
10
11
|
public
class
NoLockCounter
implements
Counter {
private
long
count =
0
;
@Override
public
long
getValue() {
return
count;
}
@Override
public
void
increment() {
count++;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
|
public
class
SynchronizeCounter
implements
Counter {
private
long
count =
0
;
@Override
public
long
getValue() {
return
count;
}
@Override
public
synchronized
void
increment() {
count++;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public
class
ReentranLockCounter
implements
Counter {
private
volatile
long
count =
0
;
private
Lock lock =
new
ReentrantLock();
@Override
public
long
getValue() {
return
count;
}
@Override
public
void
increment() {
lock.lock();
try
{
count++;
}
finally
{
//切记在finally中释放锁,synchronize的话JVM会自动释放锁
lock.unlock();
}
}
}
|
单线程情况下的测试:
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
class
LockTest {
public
static
void
main(String[] args) {
//单线程情况下基本没啥插件,synchronize略强一点,无锁最好,这个不用说呵呵
int
runTime =
100000000
;
Counter c1 =
new
SynchronizeCounter();
Counter c2 =
new
ReentranLockCounter();
Counter c3 =
new
NoLockCounter();
// 单线程情况下测试
long
s1 = System.currentTimeMillis();
for
(
int
i=
0
;i<runTime;i++){
c1.increment();
}
System.out.println(
"synchronize锁,C1:"
+c1.getValue()+
",用时:"
+(System.currentTimeMillis()-s1));
long
s2 = System.currentTimeMillis();
for
(
int
i=
0
;i<runTime;i++){
c2.increment();
}
System.out.println(
"ReentranLock锁,C2:"
+c2.getValue()+
",用时:"
+(System.currentTimeMillis()-s2));
long
s3 = System.currentTimeMillis();
for
(
int
i=
0
;i<runTime;i++){
c3.increment();
}
System.out.println(
"无锁,C3:"
+c3.getValue()+
",用时:"
+(System.currentTimeMillis()-s3));
}
}
|
多线程情况测试,原生锁和重入锁差别不大,无锁有现成安全问题:
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
37
38
|
import
java.util.concurrent.CountDownLatch;
public
class
BenchmarkTest {
private
Counter counter;
public
static
void
main(String[] args)
throws
Exception{
Counter c1 =
new
SynchronizeCounter();
Counter c2 =
new
ReentranLockCounter();
Counter c3 =
new
NoLockCounter();
//这里和参照例子不同,我用来CountDownLatch来做现成同步
CountDownLatch latch1 =
new
CountDownLatch(
1000
);
CountDownLatch latch2 =
new
CountDownLatch(
1000
);
CountDownLatch latch3 =
new
CountDownLatch(
1000
);
//100线程,每个线程加100,增把数据增加到10000
long
s1 = System.currentTimeMillis();
for
(
int
i=
0
;i<
1000
;i++){
new
TestThread(c1,latch1).start();
}
latch1.await();
System.out.println(
"SynchronizeCounter的结果是:"
+c1.getValue()+
",用时:"
+(System.currentTimeMillis()-s1));
long
s2 = System.currentTimeMillis();
for
(
int
i=
0
;i<
1000
;i++){
new
TestThread(c2,latch2).start();
}
latch2.await();
System.out.println(
"ReentranLockCounter结果是:"
+c2.getValue()+
",用时:"
+(System.currentTimeMillis()-s2));
long
s3 = System.currentTimeMillis();
for
(
int
i=
0
;i<
1000
;i++){
new
TestThread(c3,latch3).start();
}
latch3.await();
System.out.println(
"无锁的结果是:"
+c3.getValue()+
",用时:"
+(System.currentTimeMillis()-s3));
}
}
|
1
2
3
|
SynchronizeCounter的结果是:
100000
,用时:
125
ReentranLockCounter的结果是:
100000
,用时:
125
无锁的结果是:
99999
,用时:
109
|