共计 2647 个字符,预计需要花费 7 分钟才能阅读完成。
后面咱们曾经简略剖析过导致 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 内存区域以及线程平安问题是比拟底层比较复杂的问题,以上仅是集体的了解,决不能排除了解谬误。程序员应该把本人当知识分子看待,继续学习,后续如有新发现否定自己此时的意识,必将即时更正。