赞
踩
从Java代码到CPU指令的变化过程是怎样的?
*.Java文件
*.Java文件
会变出一个新的Java字节码文件
,即*.class文件
JVM
会执行刚才生成的*.class字节码文件
,并把字节码文件转化为机器指令最终的程序执行
而不同的JVM实现会带来不同的“翻译”,不同的CPU平台的机器指令干差万别,所以我们在Java代码层写的各种Lock,其实最后依赖的是JVM的具体实现(不同版本会有不同实现)和CPU的指令,才能帮我们达到线程安全的效果。但是为了能在不同的 JVM 中,不同的CPU 上,同一段代码能达到同样的效果,这就需要一种规范,来屏蔽掉各种硬件和操作系统的内存访问差异,这时候就衍生出一种Java内存模型(Java Memory Model,JMM),它可以帮助我们实现让Java程序在各种平台下都能达到一致的内存访问效果。
运行时数据区域
。并发编程
有关。虚拟机中的表现形式
。对象自身在虚拟机中的存储模型,因为Java是面向对象的,所以每一个对象的存储都有一定的存储结构。
JMM是什么?
JMM: Java Memory Model,JMM是是一组规范,各种JVM的实现都需要遵守JMM规范,再加上CPU、编译器需要对该规范进行配合,使得开发者更方便地开发多线程程序
为什么需要JMM?
如果不存在JMM,比如 C 语言就不存在,这就只能依赖处理器本身的内存一致性模型,这样很多并发操作在不同处理器上运行结果不一样,无法保证并发安全,因此需要一个标准,让多线程运行在不同处理器上的结果都能达到预期。这个标准就是 JMM。
很多工具类的底层原理都是基于JMM实现的:
volatile、synchronized、Lock等的原理都是JMM,如果没有JMM,那就需要我们自己指定什么时候用内存栅栏等,那是相当麻烦的,幸好有了JMM,让我们只需要用同步工具和关键字就可以开发并发程序。
**JMM最重要的三点内容:**重排序、可见性、原子性。
/** * 演示重排序的现象 * 重排序不是100%发生,所以需要多次重复,直到达到某个条件才停止 */ public class OutOfOrderExecution { private static int x,y=0; private static int a,b=0; public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(1); Thread thread1 = new Thread(new Runnable() { @Override public void run() { //加上栅栏 try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } a = 1; x = b; } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } b = 1; y = a; } }); thread2.start(); thread1.start(); //放开闸门 latch.countDown(); thread1.join(); thread2.join(); System.out.println("x:" + x + "," + "y:"+y); } }
上面使用了 CountDownLatch 工具类,countDown()放开闸门,await()设置闸门,之所以使用,是因为线程thread1和线程thread2,他们的运行顺序会影响到最后的x、y的值,为了让两个线程里面的指令重排序,需要让两个线程的指令同时进行。
对于两个子线程,其实共有四行有效的代码,如下:
a = 1;
x = b;
b = 1;
y = a;
由于两个子线程的执行是并发的,有的执行快有的执行慢,所以初步推测有以下三种结果:
a=1;x=b;b=1;y=a; 最终结果是x=0,y=1 // 线程thread1先执行,thread2后执行
b=1;y=a;a=1;x=b; 最终结果是x=1,y=0 // 线程thread2先执行,thread1后执行
b=1;a=1;x=b;y=a; 最终结果是x=1,y=1 // 线程thread1和线程thread2交叉执行指令
实际执行结果为:
上面的分析都是默认同一个线程内的两行代码是按照顺序执行的,那有没有可能同一个线程中,下面的代码先执行,上面的后执行呢,也就是下列情况(以下都是代码执行顺序颠倒后出现的可能结果):
y=a;a=1;x=b;b=1; 最终结果是x=0,y=0
x=b;b=1;y=a;a=1; 最终结果是x=0,y=0
x=b;y=a;a=1;b=1; 最终结果是x=0,y=0
这里加上循环,来测试一下,只有当满足条件 x=0,y=0,才能跳出循环:
/****** @author 阿昌 @create 2021-05-28 22:23 ******* * 演示重排序的现象 * 重排序不是100%发生,所以需要多次重复,直到达到某个条件才停止 */ public class OutOfOrderExecution { private static int x, y = 0; private static int a, b = 0; public static void main(String[] args) throws InterruptedException { int count = 0;//计数 CountDownLatch latch = new CountDownLatch(1); for (; ; ) { count++; //数据重置 x = 0; y = 0; a = 0; b = 0; Thread thread1 = new Thread(new Runnable() { @Override public void run() { //加上栅栏 try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } a = 1; x = b; } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } b = 1; y = a; } }); thread2.start(); thread1.start(); //放开闸门 latch.countDown(); thread1.join(); thread2.join(); String result = "第"+count+"次 "+ "(x:"+x+", y:"+y+")"; //修改代码部分 //死循环结束条件 if (x == 0 && y == 0) { System.out.println(result); break; } else { System.out.println(result); } } } }
可以看到,出现了这个结果:
这也就表示,发生了上面所说的,代码执行顺序颠倒了,两行代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,这就是重排序。
这是因为重排序有一个好处,可以提高处理速度,比如下面这个例子:
减少了对a的读取和对a的写入指令的次数:
/** * 演示可见性带来的问题 */ public class FielidVisibility { int a = 1; int b = 2; private void change() { a=3; b=a; } private void print() { System.out.println("b:"+b+",a:"+a); } public static void main(String[] args) { while (true){ FielidVisibility test = new FielidVisibility(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } test.change(); } }).start(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } test.print(); } }).start(); } } }
两个子线程共有三行代码,如下:
a=3;
b=a;
System.out.println("b:"+b+",a:"+a);
初步推测可能的执行结果如下:
a=3;b=a;System.out.println("b:"+b+",a:"+a); 最终结果是 b:3,a:3
a=3;System.out.println("b:"+b+",a:"+a);b=a; 最终结果是 b:2,a:3
System.out.println("b:"+b+",a:"+a);a=3;b=a; 最终结果是 b:2,a:1
b=a;System.out.println("b:"+b+",a:"+a);a=3; 最终结果是 b:1,a:1 // 这个是指令重排序的结果,几率比较低,这里不做考虑
即最终结果只有四种,不太可能出现 b=3,a=1 的情况。经过多次尝试,发现实际结果中出现了 b=3,a=1
那这是为什么呢?
这里就要涉及到主内存和本地内存的概念了,因为当第二个线程读取到b=3后,如果第一个线程还没有把a=3这个值从本地内存同步到主内存时,第二个线程获取到的a就是初始值1。这就是线程的可见性问题造成的。
怎么解决这个问题呢?
可以使用 volatile 关键字修饰变量,强制每次线程被修改后都会立即被其他线程可见。
/** * 解决可见性问题方案:使用 volatile */ public class FielidVisibility { volatile int a = 1; volatile int b = 2; private void print() { System.out.println("b:"+b+",a:"+a); } private void change() { a=3; b=a; } public static void main(String[] args) { while (true){ FielidVisibility test = new FielidVisibility(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } test.change(); } }).start(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } test.print(); } }).start(); } } }
以上代码无论执行多少次,都不会再出现 b=3,a=1 的结果了。因为 第一个线程在对 a 和 b 的值修改之后,第二个线程读取这俩变量时,volatile 会将修改后的值强制同步到主内存中,保证了这俩变量的可见性。
可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
为什么会有可见性问题:
CPU有多级缓存,如果缓存cache没有及时同步到主内存,就可能导致其他线程读取的数据是过期的。之所以使用缓存,是因为执行速度快,仅次于寄存器,在CPU和主内存之间加了Cache层,可以更高效的读取数据。
最主要的原因是:
线程间的对于共享变量的可见性问题不是直接由多核CPU引起的,而是由多层缓存
引起的。如果所有的cpu都只用一个缓存,那么也就不存在内存可见性问题。每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些cpu核心读取的值是一个过期的值。
Java 作为高级语言,屏蔽了这些底层细节,用JMM 定义了一套读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存的问题,但是,JMM 抽象了主内存和本地内存的概念,这里说的本地内存并不是真的是一块给每个线程分配的内存,而是对于寄存器、一级缓存、二级缓存等的抽象。
线程工作在WorkingMemory
中,他不与主内存直接沟通,而是通过Buffer缓冲区
与主内存进行同步,线程间的交互最终也就是通过主内存实现的;
总结来说,所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。
**什么是happens-before原则 **:先行发生原则,动作A发生在动作B之前,B保证能看见A,这就是happens-before。
**什么不是happens-before原则:**两个线程没有相互配合的机制,所以代码X和Y的执行结果并不能保证总被对方看到的,这就不具备happens-before。
happens-before的作用:
影响JVM重排序。如果两个操作不具备happens-before,那么JVM是可以根据需要自由排序的,但是如果具备happens-before(比如新建线程时,run方法里面的语句一定发生在thread.start()之前),那么JVM也不能改变它们之间的顺序。
锁操作(synchronized 和 Lock)
线程B在加锁的时候,能看到线程A解锁之前的所有操作。
synchronized 介绍>>:synchronized 关键字
volatile变量
该变量只要有写入,后续的读取都能看到。
volatile 介绍>>:volatile 关键字
volatile有一个特性:近朱者赤。他不仅可以帮助自己可见性,也可以帮助在他进行赋值之前进行的操作也具有可见性
/** * 描述: 演示可见性带来的问题 */ public class FieldVisibility { volatile int a = 1; volatile int b = 2; private void change() { a = 3; b = a; } private void print() { System.out.println("b=" + b + ";a=" + a); } public static void main(String[] args) { while (true) { FieldVisibility test = new FieldVisibility(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } test.change(); } }).start(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } test.print(); } }).start(); } } }
此时 不仅 b 保证了可见性,a 也由于 b 的 volatile,也保证了可见性。
使用了 join ,主线程会等待one、two两个子线程执行完,再执行
如果第一行代码的运行结果能被第二行看到,第二行的运行结果能被第三行看到;那么第一行运行的结果就能被第三行看到。
一个线程被其他线程interrupt时,那么检测中断(isInterrupted)或者抛出InterruptedException一定能被其他线程看到。
就是说,A被中断了,那么B线程就能因可见性而看到A被中断了。
线程安全的容器get一定能看到在此之前的put等存入动作CountDownLatch线程池CyclicBarrier
CountDownLatch
Semaphore
Future
线程池
CyclicBarrier
是指对于一系列操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。
long和double所占的都是64位,所以在 32 位的 JVM 中,他会被写入两次,第一次32位,第二次32位,因此就不具备原子性
,但是在64位的JVM上是原子的。不过在实际开发中无需考虑这个问题,商用Java虚拟机中已经考虑到,默认保证了long和double的原子性。
简单的把原子操作组合在一起,并不能保证整体依然具有原子性。比如一个操作组合:先取值,然后再赋值;如果是这两个操作都是原子性的,但是两个操作合在一起,就不是原子性,不能保证线程安全,所以需要进行额外的保护。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。