在并发编程中,可能最害怕听到一个词就是线程不安全。因为它意味着程序运行的时候,可能出现数据的读取或写入不准确等情况发生。

但是各位老铁,你们有想过线程不安全产生的根本原因是什么?

其实就是多线程下的变量共享,我们举一个平时工作的中的例子先来说明一下

上述图1,可能对于每个工程师来说都不陌生,就是我们工作中常见的一个环节,我们都会对代码在git上代码进行拉取、提交,进行需求或功能的开发,但是我们经常会遇到一些突如其来的问题,如下图所示:

上述图2中,几乎每个开发工程师都遇到过,也很郁闷,那就是代码冲突。提交代码的时候,莫名其妙发现不能提交,这是因为git上的代码已经被人修改了,需要先合并代码。

从上述图3中,不难看出,出现代码冲突的原因,是每个工程师本地都有一份代码的副本,在提交代码的时候,代码可能已经被其他工程师给修改了。其实核心问题是多个工程师共享了一份代码,每个人本地都有自己代码副本,其他人修改git上的代码,我们没有感知,就导致了代码错乱。其实对于java来说也是一样的,如果一个变量对于多个线程是共享的,就会出现线程不安全的情况发生,我们来看下图示例

上述图4中,其实就是多个线程对同一个数据进行读取和修改的流程,每个线程自己都有本地的数据副本,其他线程对数据的修改,自己感知不到,就会导致数据被各种覆盖,最后导致数据错乱。其实核心问题还是数据的共享,这个时候可能有同学会说,如果变量不共享是不是就不会出现线程不安全的问题呢?先别急,这就进入我们今天的主题,如何保证线程安全?

现在我们知道如果变量共享会存在线程不安全的问题,反之,如果变量不共享,则会不会出现线程不安全问题呢?如果变量不共享,则是线程安全的,那有请问那些变量是不共享的呢?

Java中有哪些变量是不共享的?

先来看看java虚拟机的内存模型,因为java运行时候的内存模型决定了哪些变量是共享的,哪些变量是非共享的:

上述图5,就是java虚拟机运行时的数据区,其中分为线程共享区和线程隔离区,线程共享区所有的变量对于java虚拟机的所有线程都是共享的,反之,线程隔离区是java虚拟机每个线程独有的且是隔离的,图5这个java虚拟运行是数据区,在面试中被问到的几率还是很大的。下面我们用一张图表示一下线程和虚拟机栈空间的关系。

上述图6,在java虚拟机栈空间,每个线程都有自己栈空间,且相互独立的,每次可以用不同的参数调用相同的方法,且线程之间相互不影响,每次执行方法的时候,变量都是存储在自己的栈空间里,没有了共享,则就不会出现线程安全问题。图6中,画出了每个线程的栈空间中都含有栈帧,那同学们,如果我们知道了栈帧能存储哪些变量,是不是我们就知道了哪些变量是线程安全的?那我们接下来就一起看看栈帧到底有哪些东西?

这里,我们先来说一下栈帧是什么:是用于虚拟机执行时方法调用和方法执行时的数据结构,每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程。最顶部的栈帧称为当前栈帧,每一个栈帧包含的内容有局部变量表、操作数栈、动态链接、方法返回地址等信息。

从图7中,我们能清楚的看到方法1->方法2->方法3整个执行过程。当方法返回的时候,方法对应的栈帧,也会在栈空间被弹出,同时,执行的方法和对应的栈帧是同生共死的关系。

那栈帧里存储变量在哪里?

答案就是在局部变量表里。

局部变量表,就是存储一些局部变量的地方,我们可以从图8中看到,局部变量表存放了方法参数和方法内部定义的一些局部变量等信息,但是局部变量分为基本数据类型和对象引用类型,我们来看一下代码示例

从图9中,我们可以看到该方法中的局部变量有基本类型和对象引用类型,这个时候同学们可能有疑惑了,之前不是说存储在栈空间的变量都是线程独有的吗?你这个new Object()代码是创建一个对象,是在java虚拟机的堆空间里,而堆空间是整个虚拟机所有线程共享的区域,这到底是怎么回事呀?我们继续往下看。

我们把代码还原到图10中,int c = a + b,基本类型的局部变量是存在栈空间,而new Object()代码创建的的对象实例的确是存在虚拟机的堆空间里,但是Object object只是引用(reference)堆空间里的对象,重要的事情说三遍,是引用,是引用,是引用,而new Object()这个对象只有被当前线程的自己所引用,我们再来看下面这张图

从图11中,我们能到看到,每个线程创建的对象,只有自己能引用,而其他线程是引用不到的,这样就不存在了变量共享的问题,同时也不会出现数据被其他修改,导致数据错乱的情况了。局部变量为什么是线程安全的呢?因为变量不共享,就不存在线程安全问题。

既然变量不共享就可以避免线程安全问题,那是不是可以把需要访问的变量作为每个线程的私有变量,每个线程访问各自的变量,相互间不共享,这样就不存在线程安全问题了吗?我们接下来看下面的代码

图12中,就是按照同学们的想法,把需要访问的变量设置到自己的私有变量里,如果多个线程都需要使用Object object对象,为了避免线程安全问题,我们不希望把Object object变量共享,我们可以让每个线程都有一个Object object对象,如下图所示:

如上图13,对于一个Object 对象,我们可以在每个线程都去创建一个Object 对象,相当于每个线程都有一个Object 对象副本,这样就没有共享了,这个做法思路是正确的,但是通用性不是很好,如果想获取Object对象来使用,总不能每次都去获取当前线程,然后去获取线程的私有变量吧。这时我们可以引入一个代理对象,把获取每个线程所持有的Object object对象的细节隐藏掉。

到目前为止,我们知道线程不安全的核心问题在于,变量的共享。如果避免变量的共享,则能解决线程安全问题。

与此我们也学习了局部变量为什么是线程安全的,同时我们还有一个好的想法来解决变量共享问题,就是对每个线程都有且仅有一个Object object对象,不存在多线程变量的共享问题,如图14中所表现出来的,用一个代理对象把获取每个线程的Object object对象细节屏蔽调。

来源:https://zhuanlan.zhihu.com/p/422859022

作者:程序员夏天