JMM(Java Memory Model)规定了所有的变量都存储在主内存(虚拟机内存的一部分)中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的主内存副本(副本并非复制整个对象,而仅复制线程中使用的的变量),线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,即 主内存 =>工作内存 =>线程。
作用:
屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
JMM图示
其中菱形的储存区是我自己读《深入理解JVM虚拟机》一书根据自己理解抽象出的一个区域,因为书中没有具体的介绍,网上找了一圈也没看到有提及的。
图中每个操作的步骤发生在哪儿个区域也是一一对应的。
内存交互细节
八种基本操作
关于主内存与工作内存之间具体的交互协议。即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8种操作来完成。(JVM在实现时必须保证下面提及的每一种操作都是原子的、不可再分)
操作 | 作用域 | 描述 |
---|---|---|
lock(锁定) | 主内存 | 把一个变量标识为一条线程独占的状态; |
unlock(解锁) | 把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定; | |
read(读取) | 把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用; | |
load(载入) | 工作 内存 |
把read操作从主内存中得到的变量值放入工作内存的变量副本中; |
use(使用) | 把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作; | |
assign(赋值) | 把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作; | |
store(存储) | 把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用; | |
write(写入) | 主内存 | 把store操作从工作内存中得到的变量的值放入主内存的变量中; |
- 细节补充:
- Java内存模型只要求 read与load之间 或 store与write之间 必须按顺序执行,但不要求是连续执行。也就是说 read与load之间或store与write之间 可插入其他指令 ,如对主内存中的变量a、b进行访问时,一种可能出现的顺序是read a -> read b -> load b -> load a;
- 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况出现;
- 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存;
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
- 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use、store操作之前,必须先执行load和assign操作;
- 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁;
- 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作以初始化变量的值;
- 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量;
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
volatile关键字
关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制。当一个变量被定义成volatile之后,它将具备两项特性:
- 可以保证此变量对所有线程的可见性;
- 由于内存屏障,实现禁止指令重排优化。
注意,volatile是不能保证原子性的。
两大特性
java并发程序都是基于多线程,操作系统为了充分利用CPU的资源,将CPU分成若干个时间片,线程会被操作系统调度进行任务切换,达到最大限度地利用CPU空闲时间。
禁止指令重排
- 指令重排序
从硬件架构上讲,指令重排序是指处理器采用了允许将多条指令不按程序规定的顺序分开发送给各个相应的电路单元进行处理。但并不是说指令任意重排,处理器必须能正确处理指令间的数据依赖关系保障程序能得出正确的执行结果。
- 指令重排执行流程
源代码—> 编译器优化的重排—> 指令并行的重排—> 内存系统的重排—-> 最终执行的指令 - volatile是如何实现禁止指令重排的呢?
通过在volatile变量相关的执行逻辑前后加上内存屏障来保证对应的指令不会出现重排。
- 指令间数据依赖关系 as-if-serial
- 场景一(单线程环境下):
指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3把地址B中的值减去3,这时指令1和指令2是有依赖的,它们之间的顺序不能重排—— (A+10)2 与 A2+10 显然不相等,但指令3可以重排到指令1、2之前或者中间,只要保证处理器执行后面依赖到A、B值的操作时能获取正确的A和B值即可。所以在同一个处理器中,重排序过的代码看起来依然是有序的。 在单线程环境下指令重排是可以保证数据的最终一致性的。
- 场景二(多线程环境下):
由于编译器优化重排,多线程环境下,使用的变量不能保证数据的一致性。
保证可见性
保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个被volatile修饰的变量A的值,新值对于其他线程来说是可以立即得知的。但仅仅是可见,并不会同步将其他线程中变量A的值修改到最新,只有当其他线程在使用该变量A的时候,会重新去主内存拿到新值的副本到工作内存中使用。简单来说,这里的“可见性”是立即让其他线程的工作内存中变量A的值失效,执行引擎使用变量A的时候需要去主内存中重新读取。
- 细节
线程和主内存之间有条总线,加入volatile关键字修饰的变量会开启缓存一致协议(MESI),监听线程中的共享变量。如果volatile修饰的变量被修改,会通过总线嗅探告知其他线程工作区,其他线程工作区会把这变量设置为无效,然后在主内存中重新读取。
而修改了变量A的线程之所以会实现可见性,也是因为在执行引擎中修改了变量A值后,会立即通过assign、store、write操作将该值回写到主内存中。 示例代码
/**
* volatile可见性测试
*/
class VolatileVisibilityDemo {
/**
* 不加volatile程序会死循环,加volatile可以保证可见性
*/
private static Integer i = 0;
// private volatile static Integer i = 0;
@SneakyThrows
public static void main(String[] args) {
new Thread(() -> {
//T1线程对主内存的变化
while (i == 0) {
/*注意:此处不能使用println方法打印到控制台,
因为println方法被synchronized修饰,会清空工作内存的值,重新获取主内存的值,从而立刻感知到了i值的变化。
System.out.println(Thread.currentThread().getName() +":"+ i);*/
}
}, "T1").start();
//保证线程T1先执行,主线程才改值
TimeUnit.SECONDS.sleep(1);
i = 1;
System.out.println(i);
}
}
不能保证原子性
原子性:
在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。原子性拒绝多线程交叉操作,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。例如 i=1是原子性操作,但是i++和 i +=1就不是原子性操作。
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized、java.util.concurrent中的锁或原子类)来保证原子性:
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值;
- 变量不需要与其他的状态变量共同参与不变约束。
无法保证原子性的代码示例:
/**
* volatile不保证原子性测试
*/
class VolatileAtomDemo {
private volatile static int num = 0;
// private static AtomicInteger num = new AtomicInteger();
public static void main(String[] args) {
//理论上num结果应该为3w,但实际并不是
for (int i = 0; i < 30; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
//非原子操作(内部需要三步执行)
num++;
// num.addAndGet(1);
}
}, "T1").start();
}
/**
* 保证其他线程执行完毕后才打印结果
*/
//当前进程中存活的线程数超过2,说明还有线程没有执行完毕(一个Java进程默认有两个线程 main线程和gc线程)
while (Thread.activeCount() > 2) {
//主线程礼让,停止向下执行,其他未执行完毕的线程继续执行。否则不能保证主线程执行完毕时,其他线程也已经执行完毕
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + ":" + num);
}
}
- 说明:
i++其实是一个复合操作,包括三步骤:
- 读取i的值;
- 对i加1;
将i的值写回内存。
volatile是无法保证这三个操作是具有原子性的,我们可以通过AtomicInteger或者Synchronized来保证+1操作的原子性。
volatile vs 锁
在众多保障并发安全的工具中选用volatile的意义——它能让我们的代码比使用其他的同步工具更快吗?在某些情况下,volatile的同步机制的性能确实要优于锁(使用synchronized关键字或java.util.concurrent包里面的锁),但是由于虚拟机对锁实行的许多消除和优化,使得我们很难确切地说volatile就会比synchronized快上多少。如果让volatile自己与自己比较,那可以确定一个原则:v**olatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢上一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行**。不过即便如此,大多数场景下volatile的总开销仍然要比锁来得更低。我们在volatile与锁中选择的唯一判断依据仅仅是volatile的语义能否满足使用场景的需求。
volatile在JMM中的特殊规定
假定T表示一个线程,V和W分别表示两个volatile型变量,那么在进行read、load、use、assign、store和write操作时需要满足如下规则:
- 在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改。
只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load动作。即保证了read、load、use动作的原子性。
- 在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V所做的修改。
只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才能对变量V执行assign动作。即保证了 assign、store、write动作的原子性。
ThreadLocal
ThreadLocal 是 Java 里一个特殊的类,它提供了线程封闭的能力。 即 变量属于线程独有、相互隔离,所以即使这个对象不是线程安全的,也不会出现并发安全问题。
ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
- 作用:为创建代价高昂的对象获取线程安全的好方法。
比如你可以用 ThreadLocal 让 SimpleDateFormat 变成线程安全的,因为那 个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,但是作为成员变量又会存在线程安全问题,那么如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。
首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。
- vs 局部变量
其实局部变量同样具备线程封闭的能力,因为局部变量存储在虚拟机栈的栈桢中,虚拟机栈是线程隔离的,所以不会有线程安全问题。但是局部变量只能在栈帧(即方法)中共享,不能在整个栈(线程)中共享,它的作用域相对于Thread Local更小。
- vs Synchronized
ThreadLocal
- Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
- Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
简单使用
class ThreadLocalDemo {
private static final ThreadLocal<String> LOCAL_VAR = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
LOCAL_VAR.set("main");
Thread thread = new Thread(() -> {
System.out.println(LOCAL_VAR.get()); //此处获取的是子线程set的值,而不是main,由于子线程未set,所以为null
LOCAL_VAR.set("sub");
System.out.println(LOCAL_VAR.get());
});
thread.start();
//让子线程先执行完毕,再继续执行主线程
thread.join();
System.out.println(LOCAL_VAR.get()); //此处获取的是主线程set的值,所以为main,而不是sub
LOCAL_VAR.remove();
System.out.println(LOCAL_VAR.get());
}
}
=========console=========
null
sub
main
null
运行结果说明了ThreadLocal只能获取本线程设置的值,也就是线程封闭。基本上,ThreadLocal对外提供的方法只有三个get()、set(T)、remove()。
适用场景
- 适用于每个线程需要自己独立的实例;
- 该实例需要在多个方法中被使用,
即 变量在线程间隔离而在方法或类间共享的场景。
- 场景一:存储用户Session
- 场景二:数据库连接,处理事务
维护JDBC的java.sql.Connection对象,因为每个线程都需要保持特定的Connection对象。
- 场景三:数据跨层传递(controller、service、dao)
Web开发时,有些固定信息(如 该请求的用户信息)需要从controller传到service传到dao,甚至传到util类。这时便可以使用ThreadLocal来优雅的解决。
- 场景四:Spring中Bean的线程安全
一般情况下,只有无状态的Bean才可以在多线程环境下共享。Spring中,绝大部分Bean都声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。
- 场景五:保证线程不安全的工具类的线程安全,比如Random、SimpleDateFormat等
底层原理
源码分析
ThreadLocal内部定义了一个静态ThreadLocalMap类,ThreadLocalMap内部又定义了一个Entry类,这里只看一些主要的属性和方法:
public class ThreadLocal<T> {
/**
* 存储变量
*/
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//实际存储的数据结构类型
ThreadLocalMap map = getMap(t);
//如果存在map就直接set,没有则创建map并set
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
/**
* 获取变量
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
/**
* 删除变量
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* 获取ThreadLocalMap容器
*/
ThreadLocalMap getMap(Thread t) {
//从这里可以看出ThreadLocalMap对象是被Thread类持有的
return t.threadLocals;
}
/**
* 创建ThreadLocalMap容器
*/
void createMap(Thread t, T firstValue) {
//实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* 内部类ThreadLocalMap
*/
static class ThreadLocalMap {
// 注意这里维护的是Entry数组
private Entry[] table;
/**
* 内部类Entity,实际存储数据的地方
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
// Entry的key是ThreadLocal对象,不是当前线程ID或者名称
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
注意:
- ThreadLocalMap虽然是在ThreadLocal类中定义的,但是实际上被Thread持有;
ThreadLocalMap被Thread持有是为了实现每个线程都有自己独立的ThreadLocalMap对象,以此为基础,做到线程隔离。
- Entry的key是(弱引用)ThreadLocal对象,而不是当前线程ID或者线程名称;
假设Entry中key为当前线程ID或者名称的话,那么程序中定义多个ThreadLocal对象时,Entry数组中的所有Entry的key都一样(或者说只能存一个value),显然是不对的。
- ThreadLocalMap中持有的是Entry数组,而不是Entry对象。
对于2、3点都是为了保证可定义多个ThreadLocal对象。
为何也会产生内存泄漏?
从ThreadLocal内存结构可知,Entry数组对象通过ThreadLocalMap最终被Thread持有,并且是强引用。也就是说Entry数组对象的生命周期和当前线程一样。即使key对象ThreadLocal对象被回收了,Entry数组的value对象也不一定被回收,这样就有可能发生内存泄漏。ThreadLocal在设计的时候就提供了一些补救措施:
- Entry的key是弱引用的ThreadLocal对象,很容易被回收,导致key为null(但是value不为null)。所以在调用get()、set(T)、remove()等方法的时候,会自动清理key为null的Entity。
- remove()方法就是用来清理无用对象,防止内存泄漏的。所以每次用完ThreadLocal后需要手动remove()。