虚假唤醒

Resource有两个方法一个给number自增另一个自减,四个线程,分别扮演生产者和消费者的角色。当生产者将number加一后便进入等待状态并唤醒消费者,消费者消费完后进入等待状态并唤醒生产者。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class Resource {
private int number = 0;

public synchronized void increment() throws InterruptedException{
if (number != 0){
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
this.notifyAll();
}

public synchronized void decrement() throws InterruptedException{
if (number == 0){
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
this.notifyAll();
}
}

/**
* 有四个线程,可以操作初始值为0的变量
* 实现一个线程对该变量加1,一个线程对该变量减1
* 交替实现,重复10轮,变量初始值为0
*/
public class ThreadWaitNotify {
public static void main(String[] args) throws Exception{
Resource resource = new Resource();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
resource.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();

new Thread(() -> {
for (int i = 1; i <= 10 ; i++) {
try {
resource.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();

new Thread(() -> {
for (int i = 1; i <= 10 ; i++) {
try {
resource.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();

new Thread(() -> {
for (int i = 1; i <= 10 ; i++) {
try {
resource.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}

一个理想的输出应当是下面这种形式

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
39
40
A	1
B 0
A 1
B 0
A 1
B 0
A 1
B 0
A 1
B 0
A 1
B 0
A 1
B 0
A 1
B 0
C 1
B 0
A 1
D 0
C 1
B 0
A 1
D 0
C 1
D 0
C 1
D 0
C 1
D 0
C 1
D 0
C 1
D 0
C 1
D 0
C 1
D 0
C 1
D 0

但实际的输出是这样:

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
39
A	1
B 0
A 1
B 0
C 1
B 0
A 1
B 0
C 1
D 0
B -1
B -2
B -3
B -4
B -5
B -6
A -5
D -6
D -7
D -8
D -9
D -10
D -11
D -12
D -13
D -14
C -13
A -12
C -11
A -10
C -9
A -8
C -7
A -6
C -5
A -4
C -3
A -2
C -1

这是因为在多线程环境下,由于CPU的时间片可能会在程序执行的任意时刻用完,假设线程B在执行时number==1 那么现在应该跳过if块,将number自减、然后唤醒其他线程,但此时间片用完,操作系统转而去执行线程D,由于之前还未将number自减所以number现在仍等于1所以线程D将顺利的执行完此时number变为0。然后时间片轮到线程B它从上次停下的地方继续执行,number将变为-1,至此number的值将永远不会变成0,也就是在B的时间片里它永远都不会调用wait()方法,线程B将一直执行下去,直到for循环结束。这种现象称为虚假唤醒,即由于if所判断的对象被其他线程改变,而进入了不该进入的if块,或跳过的不该跳过的if块,从而唤醒错误的线程的现象。所以在线程唤醒的判断上一定使用的是while而不是if因为if只会判断一次,而while是会做重复判断的也就是线程切回来的时候while会再检查一遍,从而避免的进入错误代码块的问题。

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
class Resource{
public synchronized void add() throws Exception{
System.out.println("add");
}
public synchronized void remove() throws Exception{
System.out.println("remove");
}
}

public class Lock {
public static void main(String[] args) throws InterruptedException{
Resource resource = new Resource();
new Thread(() -> {
try {
Resource.add();
} catch (Exception e){
e.printStackTrace();
}
},"A").start();
Thread.sleep(200);

new Thread(() -> {
try {
Resource.remove();
} catch (Exception e){
e.printStackTrace();
}
},"B").start();
}
}

Resource类有两个同步方法,但synchronized锁的不是某个方法,它锁的是整个对象中所有同步方法。所以A,B线程绝对不会是同时持有Recourse类,一定是A先执行,然后B再执行。也就是说某一时刻内,只能有唯一一个线程去访问synchronized方法。

CopyOnWriteArrayList原理

CopyOnWriteArrayList源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
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();
}
}

CopyOnWrite容器即写时复制的容器。往容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行复制,复制到一个新的容器Object[] newElements,然后往新容器里添加元素,加完后再将原来的容器的引用指向新的容器。这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器利用的是读写分离的思想,读和写用的是不同的容器。