juc并发编程02——JMM模型
我们在这篇文章中将介绍JMM模型,也就是java内存模型。注意,本文所提到的JMM模型与JVM内存模型属于不同层次的内容。JVM内存模型讲的是物理内存空间的分配,而JMM则强调对于JVM内存模型的抽象。
1.java内存模型
在计算机中,为了解决主内存的速度跟不上处理器速度的问题,我们给每个处理器添加一级或多级高速缓存(如下图)。但是,每个处理器上缓存的数据如何保证一致性呢?
为了实现缓存数据的一致性,我们计算机中使用了缓存一致性协议。java中也有类似的机制来实现多线程的数据模型。
java的内存模型规定如下:
- 所有的变量均存在主内存中(这里及后文的变量指所有可能出现竞争的变量:包括成员变量、静态变量,不包括局部变量)
- 每个线程都有自己的工作内存,线程对变量的操作必须在工作内存中完成。不同线程之间的工作内存相互隔离。
那么这个内存模型在jvm中究竟是如何实现的呢?
- 主内存:对应堆中的实例对象
- 工作内存:对应虚拟机栈,虚拟机可能会对这部分内存进行优化,将其放入寄存器或者高速缓存中。
看如下实例。
public class Demo4 {
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(()-> {
for(int j = 0; j < 1000000; j++) {
i++;
}
System.out.println("thread1 run pass");
}).start();
new Thread(()-> {
for(int j = 0; j < 1000000; j++) {
i++;
}
System.out.println("thread2 run pass");
}).start();
Thread.sleep(1000);
System.out.println(i);
}
}
运行结果如下:
thread1 run pass
thread2 run pass
1729677
为什么呢?如果您有jvm
的基础,就应该知道,原来自增操作并不是原子操作。可能出现如下图的情况。后面我们将讲解如何解决这个问题。
2.重排序
在编译或者运行期间,有可能会对指令进行重排序。有以下可能:
- java编译器根据对java语义的理解进行重排序。
- 现代处理器可能会对机器指令自主进行重排序。
在单线程的环境下,指令重排序可以在不改变执行结果的前提下优化代码的执行效率。但是多线程下指令重排序可能会导致一些问题。
public class Demo5 {
private static int a = 0;
private static int b = 0;
public static void main(String[] args) {
new Thread(() -> {
if(b == 1) {
if(a == 0) {
System.out.println("A");
}
if(a == 1) {
System.out.println("B");
}
}
}
).start();
new Thread(
() -> {
a = 1;
b = 1;
}
).start();
}
按朴素的理解,上面代码的输出只有可能是"B"或者没有输出。但是上面a,b的赋值可能被重排序.
public class Demo5 {
private static int a = 0;
private static int b = 0;
public static void main(String[] args) {
new Thread(() -> {
if(b == 1) {
if(a == 0) {
System.out.println("A");
}
if(a == 1) {
System.out.println("B");
}
}
}
).start();
new Thread(
() -> {
b = 1;
a = 1;
}
).start();
}
因此可能出现结果为"A"的情况。
3.volitile关键字
之前我们说了,线程并不会直接操作主内存的变量,而是操作工作内存中拷贝的变量副本。理解了这一点后,思考下面代码是有限操作还是无限操作。
public class demo6 {
private static int a = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while(a == 0) {}
System.out.println( "thread finished");
}).start();
Thread.sleep(1000);
System.out.println("edit var a...");
a = 1;
}
}
答案是无限操作,为什么呢?当然因为新建线程中使用的不过是工作缓存中的副本咯。
现在我们把代码稍微修改下。您觉得是有限操作还是无限操作呢?
public class demo6 {
private static int a = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (a == 0) {
System.out.println( "a = " + a);
}
}).start();
Thread.sleep(1000);
System.out.println("edit var a...");
a = 1;
}
}
答案是有限操作,不信您可以自己试试看。为什么呢?不妨看看System.out.println()
方法的源码。
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
原来使用了synchronized
关键字呀,除了保证操作的原子性外,还可以保证可见性.线程加锁时将清空工作内存的变量值,线程解锁前,将会把变量的最新值更新到主内存中,因此,它就可以看到最新的a的值为1,退出程序了。
除了synchronized
,我们更多使用volitile
关键字来实现可见性。
使用volitile
修饰变量a即可。
public class demo6 {
private static volatile int a = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (a == 0) {
}
System.out.println("thread execute finished...");
}).start();
Thread.sleep(1000);
System.out.println("edit var a...");
a = 1;
}
}
volitile
修饰变量为什么可以保证可见性呢?与sychoronized
加锁会让工作空间变量失效不同,它是通过禁止指令重排序实现的。其底层原理是通过内存屏障实现,它可以保证其修饰的变量在进行读写操作时,之前的指令已经执行完,之后的指令还未执行,即操作volitile
修饰变量的语句相对位置不会发生改变。
4.happens-before原则
前面我们已经了解了指令重排序的优缺点,JVM提出了happens-before
(先行发生)原则,确保程序员只要按照原则编程,就能保证并发编程的正确性。
- 同一个线程内,前面操作
happens-before
后面的操作。即使可能出现指令重排,但是前面语句对于变量的修改一定对后续操作可见。 - 对于一个锁的解锁操作,
happens-before
对于这把锁后续的加锁操作。即前一个线程解锁后,后面线程都能看到该锁对于变量修改的结果,实际上,我们前面的举例就说了,synchorinized
关键字在解锁时会把变量更新到主内存中。 - volitile变量的写操作
happens-before
后续对这个变量的读操作。 - 线程启动原则。线程A启动线程B,在线程B中可以看到线程B启动前线程A的操作。
- 线程加入原则。线程A执行过程中join线程B,并成功返回。则线程B的操作
happens-before
线程A。 - 传递性规则。如果A
happens-before
B,Bhappens-before
C,则Ahappens-before
C. 上面的原则如果理解了就很容易掌握。基于上述原则,看如下代码。
public class Demo7 {
private static int a = 0;
private static int b = 0;
public static void main(String[] args) {
a = 10;
b = a + 1;
new Thread(() -> {
if(b > 10) {
System.out.println(a);
}
}
).start();
}
}
答案是10.
相关文章
- 多线程合集(一)---信号量,锁,以及并发编程,自定义任务调度和awaiter
- 并发加锁是怎么实现的_JAVA并发编程
- MySQL-8 新语法 nowait 与 skip locked 优化并发写入性能
- 这些并发编程知识,一定要知道
- pthread_create 线程属性-Pthread并发编程之线程基本元素和状态的剖析
- 使用多线程编程来实现并发时,需要考虑并发所带来的哪些风险呢?
- 【Java 并发编程】CountDownLatch 使用场景示例
- 【Java 并发编程】线程共享变量可见性 ( volatile 关键字使用场景分析 | MESI 缓存一致性协议 | 总线嗅探机制 )
- 【Java 并发编程】线程池机制 ( 线程池执行任务细节分析 | 线程池执行 execute 源码分析 | 先创建核心线程 | 再放入阻塞队列 | 最后创建非核心线程 )
- java并发编程(十)
- java并发编程(十一)
- 高并发秒杀系统总结详解架构师
- Go语言圣经–示例: 并发的Echo服务详解编程语言
- Java并发编程之CountDownLatch详解编程语言
- Java并发编程之Lock详解编程语言
- Java并发编程之volatile关键字解析详解编程语言
- Python Asyncio并发编程详解
- 查看Oracle数据库并发隔离级别(oracle隔离级别查看)
- 实现高效并发编程:Linux系统的必备技能(linux实现并发)
- Oracle冲突解决数据更新并发性问题(oracle冲突域)
- 探究MySQL在高效率下维持万级并发量的技术(mysql 万 并发量)
- 解决方案Redis支持高并发消息处理(redis高并发消息)
- Redis配置实现高性能的并发处理(redis 配置 并发)
- Go语言并发模型的2种编程方案