文章目录
- java内存模型
- 可见性
- 解决方法
- 原子性
- 有序性
- 流水线技术
- 模式之Balking(犹豫)
java内存模型
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面 :
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
可见性
按照如下代码,一般我们都会想,run改成false,那么线程t1也就运行结束了,但是并没有运行结束,而是无线运行下去了。
这就算因为可见性导致的。
@Slf4j(topic = "c.Test5")
public class st5 {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
}
});
t.start();
sleep(1);
run = false; // 线程t不会如预想的停下来
}
}
在一开始t1线程是在主内存中获取 run 值。
随着 while 次数一直获取次数增多,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率。
但是,会带来一个问题,就是主存和内存中的值不一致,当主存内容修改,t1 线程任然从内存中获取run值,就会读到旧的内容,从而一直停不下来。
解决方法
加volatile, 它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的最新值,线程操作 volatile 变量都是直接操作主存。
volatile static boolean run = true;
当然我么之前用synchronized上锁,让共享变量在锁代码块中操作,也是可以保证可见性的,不然我么之前的的代码不就都不对了么。
不过synchronized毕竟比较重还要创建monitor,而volatile是更加轻量级的。
原子性
volatile只能保证读取到最新值,而不能解决原子性问题。
比如线程t1读取到了值x = 0,要进行++操作时,正好切换了,切换到了线程t2了,而t2也读取x = 0, 并对于x进行了–操作返回给内存进行更新,然后又切换回线程t2,t2该进行++操作,并且赋值x更新到内存。
原本一加一减应该变回0,但经过这种线程切换的情况,x最后为1,显然不符合我们的预期,因为破坏了读写的原子性。
所以在保证原子性时,还是利用synchronized和ReentrantLock编写代码时对共享变量读写保证其原子性。
有序性
指令重排
JVM 会在不影响正确性的前提下,可以调整语句的执行顺序。
比如i和j同时++,因为互不影响,谁先执行都一样,所以jvm可能的执行顺序就是i++,j++也可以是j++,i++。
流水线技术
流水线技术是一种并行计算的方式,它类似于生产流水线,将一个复杂的任务分解为一系列的子任务,并且这些子任务可以并行地执行,每个子任务的输出作为下一个子任务的输入,从而实现整体任务的并行处理。
在计算机科学中,流水线技术通常用于优化处理速度,特别是对于那些可以被分解为多个相互独立阶段的任务。通过流水线技术,可以将一个任务分解为多个阶段,每个阶段由专门的处理单元来执行,这样就可以同时处理多个阶段,从而提高整体处理速度。
流水线技术常见于处理器的设计中,例如现代CPU中的指令流水线。在指令流水线中,CPU将指令执行分解为多个阶段,比如取指阶段、解码阶段、执行阶段等等,每个阶段由专门的硬件单元来执行,这样就可以实现多条指令的并行执行,从而提高CPU的吞吐量。
简单来说啊,在不改变结果的情况下,把一个任务,拆成多个,重排序,一起执行。
// 可以重排
int a = 10; // 指令1
int b = 20; // 指令2
System.out.println( a + b );
// 不能重排
int a = 10; // 指令1
int b = a - 5; // 指令2
指令重排出现的问题
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
r.r1最后可能为0,但是概率非常非常非常小,我们自己测不出来的。
原因线程2执行方法指令重排,先执行了ready = true,这时切换线程1,执行了r.r1 = num + num,然后线程2才执行了num = 2。那么最后r.r1就等于0.
解决方法:
还是加上volatile即可,可以防止指令重排序。
volatile会保证 顺序性 和 可见性。
模式之Balking(犹豫)
犹豫模式,一个线程在执行某个操作之前会检查某个条件,如果条件不满足,则放弃执行,直接返回。这个模式的核心思想就是避免重复执行某个操作,当发现已经有其他线程或本线程已经执行了相同的操作时,就直接结束当前线程的执行。
Balking 模式通常适用于一些需要进行资源共享或状态同步的场景。当多个线程需要对共享资源进行访问或修改时,可以使用 Balking 模式来确保资源的正确使用,避免出现竞态条件或资源浪费。
举个简单的例子,假设有一个线程池,多个线程需要从线程池中获取任务执行。在获取任务之前,每个线程会检查线程池中是否还有可用的任务,如果有,则取出任务执行,如果没有,则放弃执行,直接返回。这样就可以避免多个线程同时尝试获取任务导致的竞态条件,同时也可以避免线程在没有可执行任务时进行无效的等待,提高了线程池的效率。
public class MonitorService {
// 用来表示是否已经有线程已经在执行启动了
private volatile boolean starting;
public void start() {
log.info("尝试启动监控线程...");
synchronized (this) {
if (starting) {
return;
}
starting = true;
}
// 真正启动监控线程...
}
}
这里volatile是可以不加的,synchronized已经保证了可见性,不过,通常建议还是添加 volatile 关键字来明确地表达你的意图,即使这样做可能会带来一些微小的性能开销。