【JAVA并发】一、并发问题产生的根源

一个问题的产生,往往是各种原因导致的。如果不了解问题的源头,我们就没办法给出合理的解决方案,再说源头都不了解,还谈什么解决问题啊。今天,我们来浅析一下并发问题产生的原因。

一、CPU缓存导致的可见性问题

先说下题外话:缓存这个东西就是个双刃剑,合理运用缓存很重要!
为了平衡CPU、内存之间的速度差,CPU的硬件设计师引用了CPU缓存这个东西。其实倒退个几十年,大部分服务器还是单CPU的情况下,这么设计问题也不大。但是在现在起步都是双核的情况下,问题就来了,我先来手工画一幅多CPU跟内存的交互模型图(个人理解,纯手工制作,大家将就看吧)
在这里插入图片描述
CPU在读数据的时候,会先去CPU缓存里面找,如果没有,再去内存里面取。可是CPU之间的缓存却是相对隔离的,这就导致了各个缓存之间缓存的数据并不是可见的。因此,并发问题的第一大坑就来了。
顺带提一嘴这个问题的解决办法:就是禁用缓存,让每次都直接从内存里面去存取数据,不走缓存,不就可以解决这个问题了吗,java的具体解决方案留着后面再说。

二、线程切换导致的原子性问题

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
  }
}

看上面的代码是一个单例模式的懒汉式实现方式,如果现在有两个线程同时调用getInstanc()方法,就有可能会发生线程安全问题。为什么会产生这样的问题呢,是因为在我们正常的概念中,一个方法应该执行完毕才会发生线程切换,可是实际上却并不是这样的。甚至对于高级语言来说,一行代码的执行,实际上会解析成多条指令,CPU只会保证单条指令的原子性。一句话来说线程随时可能切换
当线程A调用getInstance的时候,由于instance == null,会进行instance = new Singleton();的初始化,但是这个时候发生了线程切换,线程B也调用了 getInstance(),也判断出instance == null,这个时候线程A拿到的对象实例就跟线程B拿到的对象实例就不是同一个实例了。

例子2
针对原子性的问题,我再举一个例子,案例参考于来源是《Java并发编程实战》

public class Test{

	private long count;
	
	public void getCount(){
		return count;
	}
	
	public void test(){
	  	++count;
	}
}

先提出问题,这个Test类是线程安全的吗?
其实不是,++count看上去是一行代码,其实他也是一个“读取-修改-写入”的操作序列。由于线程随时可能切换,所以他可能在读取的时候发生切换,导致线程不安全的情况发生。

三、编译优化导致的有序性问题

接着第二点的代码,咱们给他优化一下成如下代码。

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

这样的代码看着就无懈可击了,但是智者千虑终有一失,这里我就摘抄一下别人的文章的片段了。

以下文章节选自JAVA并发编程实战:

假设有两个线程 A、B 同时调用 getInstance() 方法,他们会同时发现 instance == null ,于是同时对 Singleton.class 加锁,此时 JVM 保证只有一个线程能够加锁成功(假设是线程 A),另外一个线程则会处于等待状态(假设是线程 B);线程 A 会创建一个 Singleton 实例,之后释放锁,锁释放后,线程 B 被唤醒,线程 B 再次尝试加锁,此时是可以加锁成功的,加锁成功后,线程 B 检查 instance == null 时会发现,已经创建过 Singleton 实例了,所以线程 B 不会再创建一个 Singleton 实例。

这看上去一切都很完美,无懈可击,但实际上这个 getInstance() 方法并不完美。问题出在哪里呢?出在 new 操作上,我们以为的 new 操作应该是:

分配一块内存 M;
在内存 M 上初始化 Singleton 对象;
然后 M 的地址赋值给 instance 变量。
但是实际上优化后的执行路径却是这样的:

分配一块内存 M;
将 M 的地址赋值给 instance 变量;
最后在内存 M 上初始化 Singleton 对象。
优化后会导致什么问题呢?我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。
在这里插入图片描述

第三点总结一下,编译器的优化工作,可能会导致我们的代码前后顺序紊乱,虽然他保证了不会影响程序最终结果。但是偶尔还是可能导致意向不到的BUG产生。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是一个有理想的程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值