前面我们已经简单分析过导致JAVA线程安全问题的原因,其实最主要的就两条:

  1. 多线程同时访问共享数据。
  2. 多线程访问该共享数据的过程中使用的计算方法不具备原子性。

对应的,解决线程安全问题的方案总结起来也有两条:

  1. 避免共享数据。
  2. 确保对共享数据的并发访问安全性。

避免数据共享

非常自然的我们能够想到第一条,如果我们能够避免共享数据的话,每一条线程都使用各自的数据、不访问共享数据,那么一定就不会存在线程安全问题。

我们知道JAVA虚拟机在内存管理过程中将内存划分为不同区域,其中类成员变量存储在堆内存,方法变量存储在栈内存。堆内存在不同线程之间共享数据,有线程安全问题,而栈内存是线程独占的内存,不存在线程安全线问题。

所以,允许的情况下,不使用成员变量、而是用方法变量、临时变量的话,可以避免共享数据,从而确保数据的线程安全问题。

比如以下代码,多线程并发的情况下,counter有线程安全问题,而变量j是线程安全的、没有线程安全问题。

public class Account {
        private int counter=0;
        public void doAddCounter(){
            for(int j=0;j<100;j++){
                counter++;
            }
        }

        public int getCounter(){
            return counter;
    }
}

确保对共享数据的并发访问安全性

然而实际上我们很少有利用局部变量代替成员变量从而避免线程安全的机会,因为成员变量有他存在的理由和价值,为了避免线程安全问题而减少成员变量的使用,就是因噎废食,代码一定会丑陋不堪。

JAVA为我们提供了不同的线程安全问题解决方案,我们可以根据不同的场景采用不同的方案。包括voliate、synchronized关键字,以及ThreadLocal类,等等。

这篇文章首先分析一下voliate。

voliate

voliate是轻量级同步同步机制,可以确保:

  1. 内存可见性
  2. 避免指令重排

以上两条是JAVA虚拟机在处理voliate关键字的时候的基本原则,但是一般情况下,以上两条解释对你深入理解线程安全问题并没有什么鸟帮助,想要彻底理解voliate,你必须要进行进一步的剖析。

什么是轻量级同步机制

首先,轻量级同步机制是和synchronized相比较而言的,由于synchronized的实现依赖于操作系统的线程管理机制,需要更多的系统资源调度才能实现,所以我们一般管他叫做重量级实现。相比而言,voliate是在JAVA世界的内部实现,是JAVA虚拟机内部自己就能解决的,所以我们管voliate叫轻量级同步机制。

内存可见性

理解voliate确保“内存可见性”,需要对JAVA内存模型JMM(JAVA Memery Module)做一个简单的了解,记住,我们带着明确的目标去了解JMM,现在我们这个明确的目标是理解voliate的“内存可见性”的具体含义,所以我们不扩展不偏移目标,我们不是要了解整个JMM世界。

好了,我们带着这个明确的目标来了解一下JMM:JAVA内存模型约定,JAVA的内存分为主内存和工作内存,JAVA线程只能访问工作内存,各条线程都有自己的工作内存,而工作内存的数据均来源于主内存,JAVA线程从工作内存获取到数据、并对数据操作之后,必须将数据写回主内存才能使得操作生效。

多线程访问共享数据时,根据JMM的约定,共享数据存放在主内存,各线程访问时首先从主内存读取数据到自己的工作内存,然后在自己的工作内存区对数据进行操作(比如+1),操作完成后再从自己的工作内存写入到主内存(+1后的值)以使得线程对该变量的操作生效。

所以对于普通变量(指的是没有被voliate修饰的变量),假设有两条线程A和B并发执行,线程A和线程B同时将该变量从主内存读取到自己的工作内存,这时线程A和线程B获取到相同的初始数据。假设线程A先执行,该变量+1后被写回主内存。这个写回数据到主内存的动作线程B并不知道。

接下来线程B获得执行权,线程B对该变量+1后,再写回主内存。此时线程B其实就覆盖掉了线程A的操作,从而引发了线程安全问题。

如果变量加了voliate关键字,JMM会解决上述案例中线程A对该共享变量执行+1操作后的“线程B”并不知道的问题,voliate确保线程A对变量修改后,所有其他线程对该修改立即可见,也就是,线程B也知道了该变量的新值,从而可以在新值的基础上进行操作,也就避免了线程安全问题。

指令重排

这个问题比较简单,一般来讲,出于性能考虑,JVM并不是完全按照我们代码的顺序生成机器码的,他会判断在不影响程序逻辑的基础上调整我们代码的顺序,我们一般把这个顺序调整称为指令重排。

然而,指令重排虽然不会影响单线程应用的执行结果,但是在多线程并发环境下,指令重排有可能会导致线程安全问题。

voliate关键字会避免指令重排,因此,从指令重排的角度,可以避免线程安全问题。

voliate是否会彻底避免线程安全问题?

根据以上分析,我们猜测的答案应该是:voliate可以彻底避免线程安全问题。

然而,答案是:这个猜测是错误的。

这个答案很让人费解,但是这个答案是对的,你可以很容易的通过测试进行验证,但是解释起来却比较麻烦。

这又涉及到一个操作原子性的问题,原子性的操作一口气完成,不允许其他线程中断,而非原子性的操作却无法保证这一点,操作的过程中可能会被其他线程中断。

比如我们上面的例子,counter++的这个++操作,就不是原子性的,操作系统底层在执行++操作的时候首先会将counter变量的值从内存(此时你可以理解为工作内存)读入到CPU寄存器,然后再进行+1操作,之后再从寄存器写回到工作内存。这3个步骤的任何一步都有可能被中断。

我们尝试举例解释一下volicate无法确保线程安全性的问题:counter是voliate变量,线程ABC并发,假设线程A首先完成了counter++的操作,这个时候voliate确保该修改写回主内存后立即被线程BC获取到,这个时候线程安全问题没有发生,一切正常。此时假设线程BC并发执行,线程B的++操作被线程C的++操作中断,随后BC同时完成了++操作,当他们将操作后的counter值写回主内存时,线程安全问题发生!

JAVA内存模型、JAVA内存区域以及线程安全问题是比较底层比较复杂的问题,以上仅是个人的理解,决不能排除理解错误。程序员应该把自己当知识分子对待,持续学习,后续如有新发现否定本人此时的认识,必将即时更正。


45 声望17 粉丝