后面咱们曾经简略剖析过导致JAVA线程平安问题的起因,其实最次要的就两条:
- 多线程同时访问共享数据。
- 多线程拜访该共享数据的过程中应用的计算方法不具备原子性。
对应的,解决线程平安问题的计划总结起来也有两条:
- 防止共享数据。
- 确保对共享数据的并发拜访安全性。
防止数据共享
十分天然的咱们可能想到第一条,如果咱们可能防止共享数据的话,每一条线程都应用各自的数据、不访问共享数据,那么肯定就不会存在线程平安问题。
咱们晓得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是轻量级同步同步机制,能够确保:
- 内存可见性
- 防止指令重排
什么是轻量级同步机制
以上两条是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操作后的“线程A”并不知道的问题,voliate确保线程B对变量批改后,所有其余线程对该批改立刻可见,也就是,线程A也晓得了该变量的新值,从而能够在新值的根底上进行操作,也就防止了线程平安问题。
指令重排
这个问题比较简单,一般来讲,出于性能思考,JVM并不是齐全依照咱们代码的程序生成机器码的,他会判断在不影响程序逻辑的根底上调整咱们代码的程序,咱们个别把这个程序调整称为指令重排。
然而,指令重排尽管不会影响单线程利用的执行后果,然而在多线程并发环境下,指令重排有可能会导致线程平安问题。
voliate关键字会防止指令重排,因而,从指令重排的角度,能够防止线程平安问题。
voliate是否会彻底防止线程平安问题?
依据以上剖析,咱们猜想的答案应该是:voliate能够彻底防止线程平安问题。
然而,答案是:这个猜想是谬误的。
这个答案很让人费解,然而这个答案是对的,你能够和容易的通过测试进行验证,然而解释起来却比拟麻烦。
这有波及到一个操作原子性的问题,原子性的操作一口气实现,不容许其余线程中断,而非原子性的操作却无奈保障这一点,操作的过程中可能会被其余线程中断。
比方咱们下面的例子,count++的这个++操作,就不是原子性的,操作系统底层在执行++操作的时候首先会将count变量的值从内存(此时你能够了解为工作内存)读入到CPU寄存器,而后再进行+1操作,之后再从寄存器写回到工作内存。这3个步骤的任何一步都有可能被中断。
咱们尝试举例解释一下volicate无奈确保线程安全性的问题:counter是voliate变量,线程ABC并发,假如线程A首先实现了counter++的操作,这个时候voliate确保该批改写回主内存是立刻被线程BC获取到,这个时候线程平安问题没有产生,一切正常。此时假如线程BC并发执行,线程B的++操作被线程C的++操作中断,随后BC同时实现了++操作,当他们将操作后的counter值写回主内存时,线程平安问题产生!
JAVA内存模型、JAVA内存区域以及线程平安问题是比拟底层比较复杂的问题,以上仅是集体的了解,决不能排除了解谬误。程序员应该把本人当知识分子看待,继续学习,后续如有新发现否定自己此时的意识,必将即时更正。
发表回复