多线程执行1+2+3+...+n

特别说明:封面图片来源 https://www.zcool.com.cn/work/ZNDgwMzg3MzI=.html PS:未授权…若作者看到后不愿意授权可联系下架

如果不涉及操作共享资源,那么每个线程操作执行的结果都是一样,一旦涉及多线程操作就可能带来线程安全问题,线程安全问题的根本原因无非有三点,可见性、原子性、有序性。

一个线程执行 1+2+3+…+n 很容易实现,每个方法都有他自己的栈帧,JVM层面就保证了线程私有,如果多个线程执行这么一个方法,如何保证结果依然是对的呢?

多线程操作,保证线程安全,无非是通过加锁的方式去保证,这里使用轻量级锁 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
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
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
* 多线程执行 1+2+3+..+n = ((n+1))/2 * n
*
* @author 0x4096.peng@gmail.com
* @date 2020/12/26
*/
public class MultiThreadAdd {

private final int max = 100;
private final Lock lock = new ReentrantLock();
private int res;
private int count = 1;

public void add() {
while (count <= max) {
try {
/* 让线程睡一会 有足够的时间来抢 */
try {
Thread.sleep(500);
} catch (InterruptedException e) {
/**/
}

/* 上锁 多个线程只有一个进来获取到锁 其他线程会进入休眠状态(源码解释) */
lock.lock();

/* 线程释放锁后 之前进入的线程获取到 count 值被修改 此时可能大于 max 再次 check */
if (count > max) {
return;
}

System.err.println(Thread.currentThread().getName() + ", res=" + (res += count) + ", count=" + count++);
} finally {
/* 释放锁 */
lock.unlock();
}
}
}

public static void main(String[] args) throws InterruptedException {
MultiThreadAdd multiThreadAdd = new MultiThreadAdd();

/* 被执行的次数 */
int threadCount = 15;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 1; i <= threadCount; i++) {
new Thread(() -> {
multiThreadAdd.add();
countDownLatch.countDown();
}, "Thread-" + i).start();
}

countDownLatch.await();
System.err.println("\n" + multiThreadAdd.res);
}

}

上面的代码开启了 15 个线程去执行,每个线程都有执行的机会,操作下来结果是 5050 。比较有意思的代码是这块:

1
2
3
4
5
6
7
/* 上锁 多个线程只有一个进来获取到锁 其他线程会进入休眠状态(源码解释) */
lock.lock();

/* 线程释放锁后 之前进入的线程获取到 count 值被修改 此时可能大于 max 再次 check */
if (count > max) {
return;
}

一开始写的代码没有加 count > max 这个判断,运行了几次执行结果都是对的,当我多跑了几次出现了 bug ,结果大于 5050 ,觉得很奇怪。去看 Lock 的源码才发现,一旦线程没有拿到锁实际上会进入休眠,而不是像 synchronized 重量级锁一样让线程阻塞,相比 synchronized ,Lock 的特点很明显,1、不阻塞线程 2、可超时,相比不那么容易发生死锁 。所以当线程被唤醒时,这时的 count 的值被 +1 了,这里需要 double check 一下。

如果你认真看代码会发现, count 没有用 volatile 修饰,但是这里依然保证了可见性,具体是怎么实现的呢?我们知道 Java 中的可见性是通过 Happens-Before 规则保证的,除了 volatile 可以保证可见性外,synchronize 和 final 也可以保证可见性。那么 Lock 是如何保证可见性的呢?其实它是利用了 volatile 相关的 Happens-Before 规则。Java SDK 里面的 ReentrantLock,内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值。

1
2
3
java.util.concurrent.locks.AbstractQueuedSynchronizer

private volatile int state;

留个有趣的问题:线程安全的中的原子性和数据库中的原子性是一个意思吗?线程安全中的原子性是怎么导致的呢?