线程安全 – 找出不安全的数据
什么才是线程安全?
《Java Concurrency in Practice》 有一个比较恰当的界说 :“当多个线程访问一个方针时,假设不必考虑这些线程在运转时环境下的调度和替换履行,也不需求进行额外的同步或许在调用办法进行任何其他的协调操作,调用这个方针的行为都能够获得正确的成果,那这个方针是线程安全的。”
经典 Counter 计数比方🌰
/**
* MultiErrorDemo 多线程环境下的常见的计数过错
*
* @author suremotoo
* @date 2022/11/07 12:24
*/
public class MultiErrorDemoCounter implements Runnable {
static int index = 0;
static MultiErrorDemoCounter errorDemo = new MultiErrorDemoCounter();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
index++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(errorDemo);
Thread t2 = new Thread(errorDemo);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(index);
}
}
没错,上述代码,咱们启动了 2 个线程:t1、t2 ,别离对 index 进行 10 万次的计数,也便是别离履行 index++ 。
成果,index 最终咱们 预期值为:200000,为实践运转成果,往往都是 小于 200000 ;
这便是典型的线程不安全问题操作,由于 两个线程履行同一个 index 方针的时分,总有可能是针对同一个数值核算,这样重复核算才导致真是数字往往小于预期!
小处理
当然,咱们给 index++ 运用一个 synchronized 同步锁即可处理,run 办法调整后代码如下:
public void run() { for (int i = 0; i < 10000; i++) { // 需求获得 errorDemo 方针的锁才干进行 index++, 然后保证精确核算 synchronized(errorDemo) { index++; } } }
当然,更多具体进程能够参考文档 synchronized 剖析文章(我还没出呢😁)
测验找出 Counter 比方中重复核算的数字!
鉴于上面的问题,大部分想法都是去处理这个问题,已然针对某些值进行了重复核算,那么能不能测验着能不能找出到底是哪些值呢?本着研究学习的心态,去试一试!💪💪
第一步:记载
已然是重复核算,那么咱们就每次核算的都记载一下,判别一下是不是现已核算过了,核算过了就打印出来。
public class FindErrorNumsCounter implements Runnable {
static FindErrorNumsCounter instance = new FindErrorNumsCounter();
int index = 0;
/**
* 真实运转的次数,该值正好和 index 理论上核算的值是共同
*/
static AtomicInteger realCount = new AtomicInteger();
/**
* 过错的次数
*/
static AtomicInteger errorCount = new AtomicInteger();
/**
* 记载符号核算的数字,容量比理论核算的数值大一些,以便都能装进去
*/
static boolean[] marked = new boolean[1000000];
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
index++;
realCount.incrementAndGet();
// 判别 index 是否现已核算过
if (marked[index]) {
System.out.println("犯错了: " + index);
errorCount.incrementAndGet();
}
marked[index] = true;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("真实核算的次数: " + realCount.get());
System.out.println("过错核算的次数: " + errorCount.get());
System.out.println("实践成果: " + instance.index);
System.out.println("----------------");
}
}
为了便利,咱们先界说了 3 个变量:
boolean[] marked:用于记载符号现已核算过的数值; AtomicInteger realCount:用于记载理论预期的正确值; AtomicInteger errorCount: 记载重复核算的次数;
要点就剖析一下 run()
办法
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
index++;
// 核算次数加 1,核算真实核算的次数,也便是理论上 index 的值
realCount.incrementAndGet();
// 判别 index 是否现已核算过,假设现已核算过,阐明重复核算,则打印出来该值
if (marked[index]) {
System.out.println("犯错了: " + index);
// 一起重复核算的次数加 1
errorCount.incrementAndGet();
}
// 没有重复核算则添加到数组中,符号现已核算过
marked[index] = true;
}
}
运转一下:
真实核算的次数: 200000
过错核算的次数: 255
实践成果: 199604
会发现数字很离谱,理论上 实践成果 + 过错核算的次数 = 真实核算的次数 才对!
这儿咱们犯了一个过错,便是咱们核算重复核算的逻辑,也是线程不安全的!便是这儿:
// 判别 index 是否现已核算过,假设现已核算过,阐明重复核算,则打印出来该值
if (marked[index]) {
System.out.println("犯错了: " + index);
// 一起重复核算的次数加 1
errorCount.incrementAndGet();
}
// 没有重复核算则添加到数组中,符号现已核算过
marked[index] = true;
过错剖析
过错剖析
前提条件: 假设两个线程发生了重复核算,
index 从 0 开端
,都履行完index++
后 index 的值都为 1;
- 第 1 个线程判别
if (marked[index])
不符合,那么要符号该 index 现已核算过,也便是要履行marked[index] = true;
- 成果第 1 线程还没履行
marked[index] = true;
,偏偏 CPU 调度切换履行第 2 个线程;- 第 2 个线程判别
if (marked[index])
也不符合,然后第 2 个线程就履行了marked[index] = true;
- 第 2 个线程履行完成后,CPU 调度又切回第 1 个线程去履行
marked[index] = true;
那么最终两个线程对同一个 index 进行了符号!就跟 index++ 重复核算相同,这样便是两个线程冲突,却没有核算到犯错的数字。
示例图
能够配合该动图,理解上面的话(动图稍大,耐心等候)
find-counter-error-marked-unsafe
没处理问题反而还新造出了新问题🤦🤦
第二步:记载调整 – 添加 synchronized
上面记载重复次数的代码不安全,那么咱们用 synchronized 来同步这段代码试试呀!😏😏
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
index++;
realCount.incrementAndGet();
// 运用 synchronized 保护
synchronized (instance) {
if (marked[index]) {
System.out.println("犯错了: " + index);
errorCount.incrementAndGet();
}
marked[index] = true;
}
}
}
这样咱们再看看~
... ...
犯错了: 183544
犯错了: 190630
真实核算的次数: 200000
过错核算的次数: 1781
实践成果: 199999
多运转几回,实践成果 199999 都现已迫临 真实核算的次数 200000 了,但是这个 过错核算的次数 竟然高的离谱!应该是 1 的呀!😦😦
过错剖析
过错剖析 前提条件: 假设两个线程没有发生冲突,正常核算,
index 从 0 开端
,第 1 个线程履行完index++
后 index 的值为 1;提示 2 : 这个又要提到一个概念,便是 synchronized 拥有一个特性:线程可见。
- 第 1 个线程正常履行 index++ ,index 值变为 1,紧接着进入 synchronized 中,第 2 个线程是无法进入 synchronized 代码块的。
- 第 1 个线程此刻即即将履行
if (marked[index])
代码,却又没履行的时分, CPU 调度又回去让第 2 个线程持续履行;- 这时分第 2 个线程又履行 index++ ,index 变为 2,履行完后 CPU 调度又回去让第 1 个线程履行,这时分第 1 个线程要履行:
if (marked[index])
代码counter-error-img-1
- 由于 synchronized 的线程可见性, 1 个线程能够看到之前的线程干了什么事情,这样第 1 个线程原本要
if (marked[index])
判其他是 marked[1] ,由于第 2 个线程的成果导致变成了 marked[2]- 如此的话,第 1 个线程将 index 2 就被符号为 true,然后退出 synchronzied 代码块。
- 轮到第 2 个线程持续履行的时分,第 2 个线程履行
if (marked[index])
判其他也是 marked[2] ,由于第 1 个线程现已符号过了,所以会满意if (marked[index])
的条件, 然后打印出犯错了
。可实践上两个线程并没有冲突,1 个线程将 0+1=1,另 1 个线程将 1+1=2。
这样下来,原本正确的核算,却打印出了 1 次
犯错了
,就会导致上述 过错核算的次数 核算过多的问题了。
示例图
能够配合该动图,理解上面的话(动图稍大,耐心等候)
find-counter-error-marked-synchronized
第三步:记载调整 – 坚持每两个线程一组
上面剖析添加 synchronized 还不可,由于 CPU 调度,可能会让前面的线程一、线程二中的某一个线程步骤加快,进入下一次 index++ 的核算,那么咱们就控制一下,每次 index++ 前保证是 2 个线程一起来!
这时分咱们引入一个新的工具类:CyclicBarrier,先不必理解它到底是什么,只需求知道它到底有什么作用即可。
CyclicBarrier 其实就个栅门,假设你有个草场,养了一窝的阿拉斯加,它们也总是兴致勃勃,为了不让他们乱跑,你为了个栅门把他们圈起来~,哪天你想放出来遛遛它们,打开栅门,那叫一个: 斯如涌泉 🤪,一下子全冲出来了~
CyclicBarrier 其实就跟这个差不多,咱们能够设置一个条件,比方有 2 个线程都等候安排妥当后,然后才允许放行!咱们看代码:
/**
* FindErrorNumsCounter
*
* @author suremotoo
* @date 2022/11/07 19:57
*/
public class FindErrorNumsCounter implements Runnable {
static FindErrorNumsCounter instance = new FindErrorNumsCounter();
int index = 0;
/**
* 真实运转的次数,该值正好和 index 理论上核算的值是共同
*/
static AtomicInteger realCount = new AtomicInteger();
/**
* 过错的次数
*/
static AtomicInteger errorCount = new AtomicInteger();
/**
* 记载符号核算的数字,容量比理论核算的数值大一些
*/
static boolean[] marked = new boolean[1000000];
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
try {
cyclicBarrier1.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
index++;
realCount.incrementAndGet();
synchronized (instance) {
if (marked[index]) {
System.out.println("犯错了: " + index);
errorCount.incrementAndGet();
}
marked[index] = true;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("真实核算的次数: " + realCount.get());
System.out.println("过错核算的次数: " + errorCount.get());
System.out.println("实践成果: " + instance.index);
System.out.println("----------------");
}
}
要点看这儿:
counter-error-img-cyclicBarrier1
咱们界说了一个变量 cyclicBarrier1 并且设置了栅门开启的线程数量是 2 个,这儿 cyclicBarrier1.await();
便是 2 个线程安排妥当了才会履行下一行!
这下咱们再运转,看看成果:
真实核算的次数: 200000
过错核算的次数: 4
实践成果: 200000
🎵眼睛瞪得像铜铃🔔 ~~~~
我… … 怎样仍是有问题❓❓❓❓❓
过错剖析
过错剖析经过
cyclicBarrier1.await();
代码后,咱们有 2 个线程过来。前提条件(很重要): 咱们 假设 index 为 0, 第 2 个线程进来就一向没有履行,就卡在 index++; ,留意:是没履行 index++;
- 而第 1 个线程履行 index++ , i 变成 1,并且进入 synchronized 代码块,
if (marked[index])
marks[1] 也不满意条件(不记载过错),当即将履行marked[index]=true
的时分却被 CPU 调度走了,让第 2 个线程持续履行了~- 第 2 个线程履行 index++; index 是从 1 开端核算的,由于第 1 个线程现已 index++ 了!
- 这时分 第 2 个线程 index++ 完 index 就变成了 2 ,刚履行完又被 CPU 调度回去,持续让第 1 个线程履行,而这时分第 1 个线程持续履行:
marked[index]=true
,可此刻 index 现已变成 2 了,原本是要履行 marked[1]=true,成果变成了 marked[2]=true!- 最终第 1 个线程履行完后退出 synchronized,第 2 个线程回来持续履行 进入 synchronized。
- 这样第 2 个线程原本要
if (marked[index])
判其他是 marked[1] ,成果也变成了 marked[2] !如此的话,第 2 个线程
if (marked[index])
判其他也是 marked[2] ,这样 2 就现已重复了,就会打印出来 “犯错了:” ,可实践并没有重复呢!和之前剖析导致 过错核算的次数 核算过多的问题相同。
其实你会发现,这和 第二步 的场景是完全相同的嘛!😁
第四步:记载调整 – 坚持每两个线程一组 – 晋级!
第三步,咱们添加了 1 个 CyclicBarrier 栅门来处理 第二步两轮核算 index++ 的问题,发现仍是不可,依然存在一个在 synchronized 代码块里,由于线程切换履行 index++ 导致 index 值变的问题!
已然有可能线程半路才 index++,这样,那么我再加 1 个 CyclicBarrier 栅门,放在 index++ 后边 synchronized 前面,这样就避免了其中 1 个线程在 synchronized 里履行代码的时分突然让其他线程 index++ 。
没问题,上代码:
/**
* FindErrorNumsCounter
*
* @author suremotoo
* @date 2022/11/07 19:57
*/
public class FindErrorNumsCounter implements Runnable {
static FindErrorNumsCounter instance = new FindErrorNumsCounter();
int index = 0;
/**
* 真实运转的次数,该值正好和 index 理论上核算的值是共同
*/
static AtomicInteger realCount = new AtomicInteger();
/**
* 过错的次数
*/
static AtomicInteger errorCount = new AtomicInteger();
/**
* 记载符号核算的数字,容量比理论核算的数值大一些
*/
static boolean[] marked = new boolean[1000000];
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
try {
cyclicBarrier2.reset();
cyclicBarrier1.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
index++;
try {
cyclicBarrier1.reset();
cyclicBarrier2.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
realCount.incrementAndGet();
synchronized (instance) {
if (marked[index]) {
System.out.println("犯错了: " + index);
errorCount.incrementAndGet();
}
marked[index] = true;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("真实核算的次数: " + realCount.get());
System.out.println("过错核算的次数: " + errorCount.get());
System.out.println("实践成果: " + instance.index);
System.out.println("----------------");
}
}
咱们在 index++ 前后都添加了 CyclicBarrier 栅门!这样能够保证让两个线程都会履行 index++ 。
ok,咱们运转一下代码。
犯错了: 199920
犯错了: 199922
犯错了: 199924
犯错了: 199926
犯错了: 199928
犯错了: 199930
犯错了: 199932
犯错了: 199934
犯错了: 199936
犯错了: 199938
犯错了: 199940
犯错了: 199942
犯错了: 199944
犯错了: 199946
犯错了: 199948
犯错了: 199950
犯错了: 199952
犯错了: 199954
犯错了: 199956
犯错了: 199958
犯错了: 199960
犯错了: 199962
犯错了: 199964
犯错了: 199966
犯错了: 199968
犯错了: 199970
犯错了: 199972
犯错了: 199974
犯错了: 199976
犯错了: 199978
犯错了: 199980
犯错了: 199982
犯错了: 199984
犯错了: 199986
犯错了: 199988
犯错了: 199990
犯错了: 199992
犯错了: 199994
犯错了: 199996
犯错了: 199998
真实核算的次数: 200000
过错核算的次数: 100000
实践成果: 199998
美丽马!怎样还越来越离谱了❓❓❓❓❓❓并且犯错的还都是偶数。
counter-error-img-cyclicBarrier2
留意:
留意: 由于在 index++ 前后都加入了栅门,所以 2 个线程都会履行 index++ 的。
过错剖析
前提条件:index 为 0:
- 假设现在 2 个线程是正常按照逻辑履行, 第 1 个线程
index++
index 变为 1;- 由于第 1 个线程的成果,第 2 个线程
index++
index 就变为 2;- 然后铺开栅门!两个都去履行 synchronized 代码,要进行锁竞赛,由于 synchronized 的线程可见特性, 1 个线程能够看到之前的线程干了什么事情, 所以不管哪个线程抢到锁去履行,它们用的 index 值都是 2。
- 第 1 个线程
if (marked[index])
判别 marked[2] 不满意条件,则履行 marked[2]=true;- 第 2 个线程
if (marked[index])
判别 marked[2] 满意条件,则打印犯错了:
这样的状况对吗?必定不对啊,咱们的方针、主旨是要找出重复核算的,现在的 index 有重复核算吗?并没有! 1 个线程将 index 从 0 变为 1,另 1 个线程从 1 变为 2,没问题呀,但咱们现在这种代码就会多打印出来
犯错了:
。
🤨🤨你会发现,这仍是和之前 第三步、第四步 类似呀,都是正常的逻辑状况下,由于其中 1 个线程将 index 改变后,导致另 1 个线程运用改变后的 index ,然后导致重复打印的问题!
第五步:记载调整 – 调整重复核算的判别处理!
基于上面 第四步,咱们剖析了正常状况多核算了,现在想办法要去掉,那么,一起咱们也要会想一下过错的状况,应该是什么样的。
提示
提示:
由于 index 从 0 开端,index++ 对于 0 是不会漏算的,咱们就把设置
marked[0]=true;
正常的状况,0→1→2:
线程 | index 值 | marked 符号成果 | 补白 |
---|---|---|---|
线程 1 | 1 | false | 并没有履行 marked[1] = true; 符号, 由于 线程 2 的将 index 改变为 2 了,所以 线程 1 实践履行的是 marked[2] = true; 所以 index 为 1 在 marked 里默许的值为 false |
线程 2 | 2 | true | 实践履行的是 marked[2] = true;
|
或许
线程 | index 值 | marked 符号成果 | 补白 |
---|---|---|---|
线程 2 | 1 | false | 并没有履行 marked[1] = true; 符号, 由于 线程 1 的将 index 改变为 2 了,所以 线程 2 实践履行的是 marked[2] = true; 所以 index 为 1 在 marked 里默许的值为 false |
线程 1 | 2 | true | 实践履行的是 marked[2] = true;
|
⚠️过错的状况,0→1→1:
线程 | index 值 | marked 符号成果 | 补白 |
---|---|---|---|
第 1 个线程 | 1 | true | 实践履行的是 marked[1] = true;
|
第 2 个线程 | 1 | true | 实践履行的是 marked[1] = true;
|
从上面的表格,咱们能够看到,由于 2 个线程去履行 index++ , 所以 正常的状况
,总会像 0→1→2 这样的规则,并且 0→1→2 中间的那个 1 是会略过 sychronized 的代码处理的,2 是正确的,但咱们却多打印出来来了,不应该打印它,所以 marked [0] 总是 true,marked [1] 是 false,marked [2] 是 true; 以此类推,后边都是 true、false 替换的成果。
过错的状况
总会像 0→1→1 这样的规则,所以 marked [0] 总是 true,marked [1] 也是 true,这样呢,我就能够得出一个规则,只要呈现了 marked [index] 和 marked [index-1] 都为 true 的状况,index 才是真实的重复核算,这种状况下才是需求将信息打印出来!
这样咱们就知道调整哪里的代码了,便是 synchronized 代码块中 if (marked[index])
这句代码,咱们调整为 if (marked[index] && marked[index - 1])
run() 完整代码如下:
@Override
public void run() {
// 0 的时分永久不会重复核算,手动符号为 true
marked[0]=true;
for (int i = 0; i < 100000; i++) {
try {
cyclicBarrier2.reset();
cyclicBarrier1.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
index++;
try {
cyclicBarrier1.reset();
cyclicBarrier2.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
realCount.incrementAndGet();
synchronized (instance) {
// 判别条件,假设上一个和当时这个都为 true,则阐明 index 漏算
if (marked[index] && marked[index - 1]) {
System.out.println("犯错了: " + index);
errorCount.incrementAndGet();
}
marked[index] = true;
}
}
}
这样咱们再次运转
真实核算的次数: 200000
过错核算的次数: 0
实践成果: 200000
ok,没问题了!emm,运转好屡次,只是一向没有失利的,这是由于咱们调整屡次代码后,线程磕碰的概率变小了,不要紧!咱们微调一下再测验就能够!
第六步:调整 main 办法,运转代码打印!
只要呈现过错,errorCount
必定能核算到值,咱们就来个循环,直到遇到漏算的过错状况。
完整代码:
/**
* FindErrorNumsCounter 找出并发状况下哪些计数被漏算
*
* @author suremotoo
* @date 2022/11/07 15:57
*/
public class FindErrorNumsCounter implements Runnable {
static FindErrorNumsCounter instance = new FindErrorNumsCounter();
int index = 0;
/**
* 真实运转的次数,该值正好和 index 理论上核算的值是共同
*/
static AtomicInteger realCount = new AtomicInteger();
/**
* 过错的次数
*/
static AtomicInteger errorCount = new AtomicInteger();
/**
* 记载符号核算的数字,容量比理论核算的数值大一些
*/
static boolean[] marked = new boolean[1000000];
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
@Override
public void run() {
marked[0] = true;
for (int i = 0; i < 100000; i++) {
try {
cyclicBarrier2.reset();
cyclicBarrier1.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
index++;
try {
cyclicBarrier1.reset();
cyclicBarrier2.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
realCount.incrementAndGet();
synchronized (instance) {
if (marked[index] && marked[index - 1]) {
System.out.println("犯错了: " + index);
errorCount.incrementAndGet();
}
marked[index] = true;
}
}
}
public static void main(String[] args) throws InterruptedException {
while (errorCount.get() == 0) {
realCount.set(0);
errorCount.set(0);
instance.index = 0;
marked = new boolean[1000000];
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("真实核算的次数: " + realCount.get());
System.out.println("过错核算的次数: " + errorCount.get());
System.out.println("实践成果: " + instance.index);
System.out.println("----------------");
}
}
}
成果:
真实核算的次数: 200000
过错核算的次数: 0
实践成果: 200000
----------------
真实核算的次数: 200000
过错核算的次数: 0
实践成果: 200000
----------------
犯错了: 77953
真实核算的次数: 200000
过错核算的次数: 1
实践成果: 199999
----------------
77953 重复核算了,过错 1 次,实践成果 199999,真实的核算为:200000,这次核算对咯~
能够屡次运转验证,发现没问题~🎉🎉