面向小白的线程安全问题讲解

178 阅读2分钟

这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战

本篇文章带领大家一起思考多线程的问题,问这个程序世界什么最好玩啊?答曰:多线程,怎么好玩了呢?其bug诡异的出现,又诡异的消失,深不可测又不知其所踪,这种扑朔迷离的现象总给其围上一缕神秘的面纱,促使着我们挠头探索。

嘛探索,不都是为了Rmb嘛。

并发编程的背景

其实,看多了历史,你会发现:历史总是有相似之处,任何历史事件的发生都离不开那个时候的历史背景,就连并发编程也不例外。

并发编程的出现主要是因为Cpu不够快了,由于Cpu处理速度有限加之做不完的任务,使得Cpu不堪重负,正要撂挑子走人的时候,人们想出了一个绝好的办法,就是:一个不够,那就再加一个。

那如果你是四核Cpu,就会有cpu0.cpu1,cpu2,cpu3。cpu从此不再孤独,因为前进的路上它有了兄弟,但线程犯难了,这cpu分家,也没把线程安排明白,怎么回事呢?

早期这个Cpu计算的实在是太快,内存慢它一步,人们就给cpu加了个缓存,有缓存读缓存这样不就提高性能了嘛,单核cpu的时候,所有线程都从它那过,这没问题。

image.png

现在是多核时代,每个cpu都有自己的缓存,现在大家不走一条道,还让数据是一样的,你说这不让线程犯难嘛?

我们可以搞张图看看效果:

image.png

如果线程1能看见线程2修改的数据,我们叫可见性

如果还没有说明白,我们这里可以举个例子:

每个加10K的方法

  private static long count = 0;
  private void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count += 1;
//      System.out.println(count);
    }
  }

我们开启两个线程,然后执行加1ok方法,不出意外,我们会得到20k:

final Test test = new Test();
// 创建两个线程,执行 add() 操作
Thread th1 = new Thread(()->{
  test.add10K();
});
Thread th2 = new Thread(()->{
  test.add10K();
});
// 启动两个线程
th1.start();
th2.start();
// 等待两个线程执行结束
th1.join();
th2.join();

但实际上,我们的结果:

image.png

不可思议对吧,而且每次都不一样,而且不会达到我们预期的结果,为什么?

如果我们把这个值加到100000000,得到的结果也不是我们要的:

image.png

原因就是有一些时候,这两线程一起执行,一起写,拿相同的数去计算下一个值,而不是拿上一个线程计算好的值,这就是线程安全问题。