Java并发编程1 —— 线程安全问题是如何产生的

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/qq_25246305/article/details/82316665

前提

在研究线程安全问题之前,有必要简单回顾一下jvm内存模型。

如下图所示,jvm内存模型可分成两大部分,即主存区和jvm内存。

主存区又叫做共享内存,顾名思义,是各个线程运行时所共享的内存区域,用来存放类加载时产生的对象实例,以及共享变量、静态变量、常量等。jvm内存为线程私有,即每个线程独享该内存,用来存放局部变量、方法信息等。

当线程需要访问共享变量时,首先需要从共享内存中读取该变量,并在私有内存中生成该变量的副本,再对这个副本进行操作,最后把修改后的值上传到共享内存中,覆盖原值。

举个栗子

有一家没有钱的银行,当前银行账户余额为0。A同学有100元需要存入银行,B同学急需取出50元,他们几乎同时在不同的ATM上进行各自的操作。A同学率先向ATM里放入100元,并且银行账户余额随之变成了100元。但是就在后台更新余额之前,B同学发起了取钱的操作,他读取到的当前余额仍然是0元,随后取出50元,账户余额变成了-50元。于是,账户余额最终结果是-50元。

也就是说,这个过程中各个事件按照下面的顺序发生:

初始余额为0 -> A存100元 -> B取50元 -> 更新余额为100元 -> 更新余额为-50元 -> 最终余额为-50元

那么问题来了

在上面的栗子里发生了什么?显然-50的结果是错误的。问题在于,当B同学取钱时,他读到的当前余额是0元,而不是我们期望的100元。换句话说,A存钱后的实时余额没有被B感知到,B在一个错误的余额上进行操作,所产生的结果自然也是错误的。

“银行账户余额”相当于“共享变量”,我们所有人操作的都是这一个余额。而A、B两同学相当于两个线程,他们各自独立地进行不同的操作,但是都对共享变量的值进行了修改。

线程安全问题,通俗的讲就是多个线程在处理共享变量时产生了冲突,导致某些线程使用的错误的值,进而使其最终结果也是错误的。

JVM三大特性

怎样能使这种操作不产生冲突?我们需要保证下面的三大特性。

原子性

显然在上面的栗子中,A同学“存钱”和“更新余额”这两个操作应当是顺序执行,且不被拆分的,然而在这两步中被插入了B同学“取钱”的操作。这两步操作涉及到对共享变量的读和写,操作前后的值发生了变化,所以“存钱-更新余额”应当是一个“原子操作”,具备“原子性”。

可见性

A同学存钱并且更新余额后,B同学再去取钱,这就满足了原子性操作。此时,B看到的余额应当是100元,而不再是0元。“可见性”就是说,当某个线程修改了共享变量的值后,其他线程应当能够立刻感知到这个值的变化,使用新值做接下来的操作。

有序性

这里涉及到后面的一个知识点——指令重排序。指令重排序要保证线程内部的语义是串行的,所以在某个线程内部来看,并没有什么太大的差异,依然是有顺序的;而在某个线程内观察另一个线程,由于重排序、共享变量同步的延时等诸多因素,看到的操作是无序的。

猜你喜欢

转载自blog.csdn.net/qq_25246305/article/details/82316665