内存模型
1. 可见性
1.1 问题引发
- 一个线程对主存中数据写操作,对其他线程的读操作不可见
package com.erick.multithread.d4;
import java.util.concurrent.TimeUnit;
public class Demo01 {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
/*中间不要打印任何东西,避免出现错误*/
}
}, "t1").start();
TimeUnit.SECONDS.sleep(3);
/*3秒后主线程修改了flag,但是上面线程没有如期停下来*/
flag = false;
}
}
1.2 原理分析
# 初始状态
- 初始状态,静态变量run被加载到主存中
- t线程从主存中读取到了run
- t线程要频繁从主存中读取数据,
JIT编译器会将run的值缓存到自己的工作内存中的高速缓存中,减少对主存的访问,提高性能
# 3秒后
- 主存中的数据被修改了,但是t线程还是读取自己工作内存的数据,并不会去主存中去拿
1.3 解决方案
volatile
- 修饰成员变量或者静态成员变量
- 线程获取该变量时,不会从自己的工作内存中去读取,每次都是去主存中读取
- 牺牲了性能,保证了一个线程改变主存中某个值时,对于其他线程不可见的问题
package com.erick.multithread.d4;
import java.util.concurrent.TimeUnit;
public class Demo01 {
private static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
}
}, "t1").start();
TimeUnit.SECONDS.sleep(3);
flag = false;
}
}
synchronized
- 解决不可见性:重量级锁
- java内存模式中,synchronized规定,线程在加锁时
1. 先清空工作内存
2. 在主存中拷贝最新变量到工作内存中
3. 执行代码
4. 将更改后的共享变量的值刷新到主存中
5. 释放互斥锁
package com.erick.multithread.d4;
import java.util.concurrent.TimeUnit;
public class Demo02 {
private static boolean flag = true;
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (true){
synchronized (lock){
if (!flag) {
break;
}
}
}
}).start();
TimeUnit.SECONDS.sleep(3);
flag = false;
}
}
1.4 犹豫模式–Balking
- 一个线程发现另一个线程已经做好了某个事,那么该线程就无需再做
- 类比单例模式
重复做
package com.erick.multithread.d4;
import lombok.Getter;
import java.util.concurrent.TimeUnit;
public class Demo03 {
public static void main(String[] args) throws InterruptedException {
Balking balking = new Balking();
for (int i = 0; i < 5; i++) {
new Thread(() -> balking.operation()).start();
}
TimeUnit.SECONDS.sleep(3);
System.out.println(balking.getJobDoneTimes());
}
}
class Balking {
private boolean isJobDone;
@Getter
private int jobDoneTimes;
public void operation() {
if (isJobDone) {
return;
}
/*大概为5*/
jobDoneTimes++;
try {
System.out.println(Thread.currentThread().getName() + " execute job");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
isJobDone = true;
}
}
synchronized
package com.erick.multithread.d4;
import lombok.Getter;
import java.util.concurrent.TimeUnit;
public class Demo04 {
public static void main(String[] args) throws InterruptedException {
BalkingJob balkingJob = new BalkingJob();
for (int i = 0; i < 5; i++) {
new Thread(() -> balkingJob.operation()).start();
}
TimeUnit.SECONDS.sleep(2);
System.out.println(balkingJob.getJobDoneTimes());
}
}
class BalkingJob {
private boolean isJobDone;
@Getter
private int jobDoneTimes;
public synchronized void operation() {
if (isJobDone){
return;
}
jobDoneTimes++;
System.out.println(Thread.currentThread().getName() + " execute job");
isJobDone = true;
}
}
2. 有序性
2.1 计组思想
- 每条指令划分为 取指令 --- 指令译码 --- 执行指令 --- 内存访问 --- 数据回写
- 计组思想 并不能提高单个指令的执行时间,但是变相的提高了吞吐量
2.2 指令重排
- 一条代码,可能被字节码执行分为若干个指令
- JVM在不影响性能的情况下,会对代码的执行顺序进行重排
正常重排
- 即使重排序,不会对结果产生影响
private int i;
private int j;
i=0;
j=2;
异常重排
- 不可重排,重排后就会发生错误
- 这种JVM层面的指令重排,就会引发莫名其妙的错误
private int i = 0;
private int j = 2;
i = 10;
j = i-1;
禁止重排
- volatile修饰成员变量或者静态成员变量,该变量上面的所有指令都不会重排
private int i = 0;
private volatile int j = 1;
// 只需要在j上加,这样代码中j的上面的所有代码,就不会被放到j后实现
i = 10;
j = i - 1;
Volatile 原理
- Memory Barrier(Memory Fence): 底层实现是内存屏障
- 写屏障: 在写操作后,加上写屏障
- 读屏障: 在读操作前,加上读屏障
1. 可见性
1.1 写屏障
- voliate修饰的变量,会在写操作后,加上写屏障( 代码执行处)
- 在该屏障之前的所有代码的改动,同步到主存中去
package com.dreamer.multithread.day02;
public class Demo03 {
private static int number = 0;
private static volatile int age = 0;
public static void main(String[] args) {
new Thread(() -> {
number++;
age=10;
// JVM 会加上写屏障
}).start();
}
}
1.2 读屏障
- volatile修饰的变量,会在读取到该变量的前面,加上读屏障
- 读屏障后面的数据,都会在主存中获取
package com.dreamer.multithread.day02;
public class Demo03 {
private static int number = 0;
private static volatile int age = 0;
public static void main(String[] args) {
new Thread(() -> {
int ageResult = age;
int numberResult = number;
}).start();
}
}
2. 有序性-指令重排
- 只能保证 本线程内 不会发生指令重排
2.1 写屏障
- 确保指令重排时,写屏障之前的代码,不会重排到写屏障后面
private int number = 0;
private volatile boolean flag = true;
public void method(){
number ++;
flag = false; // 写屏障,不会将写屏障前面的代码,重排到写屏障后面
}
2.2 读屏障
- 确保指令重排时,读屏障后的代码,不会重排到读屏障之前
3. 指令交错
- volatile不能保证不同线程在执行的时候的指令交错引发的问题(不能保证线程安全)
- 只是保证各个线程在执行的时候都从主存中去加载
4. synchronized/volatile
synchronized | volatile | |
---|---|---|
锁级别 | 重量级锁 | 轻量级锁 |
可见性 | 可解决 | 可解决 |
原子性(多线程指令交错) | 可解决 | 不保证 |
指令重排 | 可以重排,但不会出错 | 可以禁止重排 |
场景 | 一个线程修改,其他线程读 | 多线程并发修改 |
Happen-Before原则
- 对共享变量的读写操作,代码的可见性和有序性的一套总结
1. synchronized
- 线程解锁之前对变量的写,对于接下来的用加锁的其他线程的读,改变是可见的
- 线程解锁前,会将写操作的变量,从工作内存会同步到主内存中
package com.dreamer.multithread.day02;
public class Demo04 {
private static int x = 0;
private static Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock) {
x = 10;
}
}).start();
new Thread(() -> {
synchronized (lock) {
System.out.println(x);
}
}).start();
}
}
2. volatile
- 变量用volatile修饰,一个线程对其的写操作,对于其他线程来说是可见的
- 写屏障结合读屏障,保证是从主存中读取变量
package com.dreamer.multithread.day02;
public class Demo05 {
private static volatile int x = 0;
public static void main(String[] args) {
new Thread(() -> x = 10).start();
new Thread(() -> System.out.println(x)).start();
}
}
3. 先写先得
- 线程start前对变量的写操作,对该线程开始后的读操作是可见的
package com.dreamer.multithread.day02;
public class Demo06 {
private static int x = 0;
public static void main(String[] args) {
x = 10;
new Thread(() -> System.out.println(x)).start();
}
}
4. 通知准则
- 线程结束前对变量的写操作,对其他线程得知它结束后的读操作可见性
- 如调用join方法
package com.dreamer.multithread.day02;
public class Demo07 {
private static int x = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> x = 10);
thread.start();
thread.join();
System.out.println(x);
}
}
5. 打断规则
- 线程t1 打断t2前,t1对变量的写,对于其他线程得知得知t2被打断后,对变量的读可见
package com.dreamer.multithread.day06;
public class Demo07 {
private static int x = 0;
public static void main(String[] args) {
Thread t2 = new Thread("t2") {
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
}
};
Thread t1 = new Thread("t1") {
@Override
public void run() {
x = 5;
t2.interrupt();
}
};
t1.start();
t2.start();
while (t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);
}
}
6. 默认值
- 对变量默认值的写,0,false,null,其他线程对该变量可见
7. 传递性
- 其实就是volatile的读屏障和写屏障问题
package com.dreamer.multithread.day02;
public class Demo08 {
private static int x = 0;
private static volatile int y = 0;
public static void main(String[] args) {
new Thread(() -> {
x = 10;
y = 20;
// 写屏障,会将上面的操作全部赋值到主存中去
}).start();
new Thread(() -> {
System.out.println(x);
System.out.println(y);
}).start();
}
}
文章评论