特别说明:封面图片来源 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;
|
留个有趣的问题:线程安全的中的原子性和数据库中的原子性是一个意思吗?线程安全中的原子性是怎么导致的呢?