当前位置: 首页 > 后端技术 > Java

JAVA线程安全之voliate

时间:2023-04-01 22:23:27 Java

我们已经简单分析了JAVA线程安全问题的成因。其实主要有两个:多线程同时访问共享数据。多线程访问共享数据的过程中使用的计算方法不是原子的。相应地,解决线程安全问题有两种方案:避免共享数据。确保对共享数据的并发访问安全。避免数据共享是很自然的。我们可以想到第一个。如果能避免共享数据,每个线程都使用自己的数据,不访问共享数据,那么线程安全肯定是没有问题的。我们知道JAVA虚拟机在内存管理过程中将内存划分为不同的区域,其中类成员变量存放在堆内存中,方法变量存放在栈内存中。堆内存在不同线程之间共享数据,存在线程安全问题,而栈内存是线程独享的,不存在线程安全线问题。因此,在允许的情况下,如果不使用成员变量,而是使用方法变量和临时变量,就可以避免共享数据,从而保证数据的线程安全。比如下面的代码,在多线程并发的情况下,counter存在线程安全问题,而变量j是线程安全的,不存在线程安全问题。公共类账户{privateintcounter=0;publicvoiddoAddCounter(){for(intj=0;j<100;j++){counter++;}}publicintgetCounter(){返回计数器;}}保证共享数据的并发访问安全但是,实际上我们很少有机会用局部变量代替成员变量来规避线程安全,因为成员变量是有存在的理由和价值的。为了避免线程安全问题,减少成员变量的使用是因为噎废食。代码注定是丑陋的。JAVA为我们提供了不同的线程安全问题解决方案,我们可以根据不同的场景采用不同的解决方案。包括voliate、synchronized关键字,以及ThreadLocal类等等。本文首先分析voliate。voliatevoliate是一种轻量级的同步机制,可以保证:内存可见性,避免指令重排以上两个是JAVA虚拟机在处理voliate关键字时的基本原理,但是总的来说,以上两个解释让你深刻理解了线程安全问题没有帮助。要想完全理解voliate,还得做进一步的分析。什么是轻量级同步机制?首先将轻量级同步机制与synchronized进行对比。由于synchronized的实现依赖于操作系统的线程管理机制,因此需要更多的系统资源调度来实现。所以,我们一般把管理他叫做重量级实现。相比之下,voliate是在JAVA世界内部实现的,可以在JAVA虚拟机内部自行解决,所以我们称voliate为一种轻量级的同步机制。内存可见性理解voliate以保证“内存可见性”需要对JAVA内存模型JMM(JAVAMemeryModule)有一个简单的了解。请记住,我们以明确的目标理解JMM。现在我们明确的目标是理解voliate的“内存可见性”的具体含义,所以我们不去扩展non-offset目标,我们也不是想去理解整个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++的++操作就不是原子的。操作系统底层在执行++操作时,会先从内存(此时可以理解为工作内存)中读取计数器变量的值到CPU寄存器中,然后执行+1操作,然后从寄存器写回工作内存。这3个步骤中的任何一个都可能被中断。下面尝试用一个例子来解释一下voliate不能保证线程安全的问题:counter是一个voliate变量,线程ABC是并发的。假设线程A先完成counter++的操作。此时voliate保证修改写回主存,立即被线程BC获取,此时并没有出现线程安全问题,一切正常。此时假设线程BC是并发执行的,线程B的++操作被线程C的++操作打断,然后BC同时完成++操作。当他们将操作后的计数器值写回主存时,线程安全问题就出现了!JAVA内存模型、JAVA内存区域、线程安全问题是底层比较复杂的问题。以上仅为个人理解,不排除误解。程序员应该把自己当作知识分子,不断学习。如果以后有新的发现否定他们此时的理解,他们会立即纠正。