java源码解析之CopyOnWriteArrayList

CopyOnWriteArrayList的并发实现

  • 锁机制的采用:final transient ReentrantLock lock = new ReentrantLock();
  • 元素的存储:private transient volatile Object[] array;,采用了volatile修饰,确保array的在多线程下的可见性
  • 并发下的设置元素,采用了java.util.concurrent包的ReentrantLock锁(创建时采用了默认的不公平锁),在设置新元素时,是获取了原数组的一个拷贝Object[] elements = getArray();,在拷贝的数组上进行元素的更改,在更改完成后,通过setArray方法更新旧的array数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(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 oldValue;
} finally {
lock.unlock();
}
}
  • 特点:CopyOnWriteArrayList对于数据的更改的反馈是延迟性的,从上面一个示例代码可以看出,由于是通过数组的copy操作,在拷贝的副本上进行相应的数据操作,然后在进行副本替代原数据的操作,从而实现ArrayList的并发操作;因此,当两个线程同时对CopyOnWriteArrayList进行操作时,假设A线程遍历CopyOnWriteArrayList,B线程在对CopyOnWriteArrayList进行数据的修改,A线程所遍历的还是旧数据,是无法立马感知线程B对CopyOnWriteArrayList的修改操作

final ReentrantLock lock = this.lock 为什么要多此一举?

在看CopyOnWriteArrayList时,对其中的一段代码很是不解

1
2
3
4
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
}

当时搞不清楚为什么要把lock这个全局变量再次复值给一个finalReentrantLock,通过一些博客的解析之后,终于明白了

解答问题

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MemoryTest {

private final ReentrantLock lock = new ReentrantLock();

public MemoryTest() {
}

public void test() {
lock.lock();
lock.unlock();
}

public void test2() {
final ReentrantLock l = this.lock;
l.lock();
l.unlock();
}

public static void main(String[] args) {
MemoryTest m = new MemoryTest();
m.test();
m.test2();
}
}

对上面的代码进行编译生成class文件后,我们对class文件进行查看

javap -verbose MemoryTest.class

通过查看常量池的内容,可以看到,lock这个成员变量是在常量池里的

常量池内容

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
Constant pool:
#1 = Methodref #11.#26 // java/lang/Object."<init>":()V
#2 = Class #27 // java/util/concurrent/locks/ReentrantLock
#3 = Methodref #2.#26 // java/util/concurrent/locks/ReentrantLock."<init>":()V
#4 = Fieldref #7.#28 // MemoryTest.lock:Ljava/util/concurrent/locks/ReentrantLock;
#5 = Methodref #2.#29 // java/util/concurrent/locks/ReentrantLock.lock:()V
#6 = Methodref #2.#30 // java/util/concurrent/locks/ReentrantLock.unlock:()V
#7 = Class #31 // MemoryTest
#8 = Methodref #7.#26 // MemoryTest."<init>":()V
#9 = Methodref #7.#32 // MemoryTest.test:()V
#10 = Methodref #7.#33 // MemoryTest.test2:()V
#11 = Class #34 // java/lang/Object
#12 = Utf8 lock
#13 = Utf8 Ljava/util/concurrent/locks/ReentrantLock;
#14 = Utf8 a
#15 = Utf8 I
#16 = Utf8 <init>
#17 = Utf8 ()V
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 test
#21 = Utf8 test2
#22 = Utf8 main
#23 = Utf8 ([Ljava/lang/String;)V
#24 = Utf8 SourceFile
#25 = Utf8 MemoryTest.java
#26 = NameAndType #16:#17 // "<init>":()V
#27 = Utf8 java/util/concurrent/locks/ReentrantLock
#28 = NameAndType #12:#13 // lock:Ljava/util/concurrent/locks/ReentrantLock;
#29 = NameAndType #12:#17 // lock:()V
#30 = NameAndType #35:#17 // unlock:()V
#31 = Utf8 MemoryTest
#32 = NameAndType #20:#17 // test:()V
#33 = NameAndType #21:#17 // test2:()V
#34 = Utf8 java/lang/Object
#35 = Utf8 unlock

通过查看编译后字节码后,我们可以发现,在test方法中,由于直接使用成员变量,因此有两次getfield操作,这是从常量池中获取lock这个成员变量,而java内存情况是,方法执行是是在虚拟机栈中的,而常量池是在方法区中的,需要从堆中拿到指针然后压入栈中,因此,由于test方法是直接使用成员变量,因此会有而外的开销;而test2方法中,由于将成员变量赋值给了方法内的局部变量,相比test方法,少了一次的指针从堆拿取然后入栈的操作

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
public void test();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #4 // Field lock:Ljava/util/concurrent/locks/ReentrantLock;
4: invokevirtual #5 // Method java/util/concurrent/locks/ReentrantLock.lock:()V
7: aload_0
8: getfield #4 // Field lock:Ljava/util/concurrent/locks/ReentrantLock;
11: invokevirtual #6 // Method java/util/concurrent/locks/ReentrantLock.unlock:()V
14: return
LineNumberTable:
line 13: 0
line 14: 7
line 15: 14

public void test2();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: aload_0
1: getfield #4 // Field lock:Ljava/util/concurrent/locks/ReentrantLock;
4: astore_1
5: aload_1
6: invokevirtual #5 // Method java/util/concurrent/locks/ReentrantLock.lock:()V
9: aload_1
10: invokevirtual #6 // Method java/util/concurrent/locks/ReentrantLock.unlock:()V
13: return
LineNumberTable:
line 18: 0
line 19: 5
line 20: 9
line 21: 13