java 8根据某个字段去重_面试经典概念:Java内存模型以及线程安全的可见性问题...

上次线程池已经说过了,从今天开始一起了解下JVM内存模型详解。

23a65686d9e9bb630c39e81d99f5da82.png

(一)容易误解的部分

老铁很容易把JAVA的内存区域、JAVA的内存模型,GC分代回收的老年代和新生代也容易搞混,绕进去绕不出来。学习多线程之前一定要搞明白这些问题,可能在你的内心一直认为多线程就是一个工具,所有的底层都是C++来写的,没办法去看,为什么要有java,java其实就是屏蔽了底层的复杂性。

  • ① GC内存区域

堆的概念,老年代,新生代,Eden,S0,S1

  • ② JAVA的内存区域

JVM运行时的区域:java编译生成class,线程共享部分(方法区,堆内存),线程独占部分(虚拟机栈,本地方法栈,程序计数器)

  • ③ JAVA的内存模型(概念)

针对多核多CPU,多线程而制定的一套规范规则,不是一种开发技术。

(二)多线程中的问题

  1. 所见非所得(你看到的并不是所想的)、
  2. 无法肉眼去检测程序的准确性(多线程下,完全看不出来正常不正常)。
  3. 不同的运行平台又不同的表现。
  4. 错误很难重现。

(三)工作内存和主内存

  • ① 主内存

创建一个对象在堆里面,也可以称之为主内存,不仅仅是在堆,存在一个对象X,就存在主内存

  • ② 工作内存

线程运行在工作内存, 虚拟机栈,程序计数器,CPU,高速缓存。

工作内存和主内存只是一个逻辑上的划分,概念上的东西。

  • ③ 奇妙的现象

主内存的flag传输到工作内存flag的时候,存在CPU缓存的情况,CPU缓存可能导致非常短的时间内不一致,本身CPU厂家底层是要做一致处理的,但是存在短时间内的不一致。

be8355594b2a35cc9ca26386d946521c.png

(四)指令重排

  • ① 介绍

Java语言规范JVM线程内部维持顺序花语义,即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。

  • ② 意义

使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率。

重排序,只能保证单个线程的,如果是多线程的话,就没有爆发保证重排序。

// 线程1 a = d; b = 2// 线程2 c = a; d =3//重排序后//线程1 b = 2 ; a =d;//线程2d = 3 ; c =a;

编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

(五) 如何不进行指令排序

  • ① 介绍

The Java volatile keyword is used to mark a Java variable as "being stored in main memory". More precisely that means, that every read of a volatile variable will be read from the computer's main memory, and not from the CPU cache, and that every write to a volatile variable will be written to main memory, and not just to the CPU cache。 CPU不缓存。

  • ② 实例
public class VisibilityDemo2 { // 状态标识 (不用缓存) private volatile boolean flag = true; // 源码 -> 字节码class // JVM 转化为 操作系统能够执行的代码 (JIT Just In Time Compiler 编译器 )(JVM -- client , --server) public static void main(String[] args) throws InterruptedException { VisibilityDemo2 demo1 = new VisibilityDemo2(); new Thread(new Runnable() { public void run() { int i = 0; while (demo1.flag) { i++; } System.out.println(i); } }).start(); TimeUnit.SECONDS.sleep(2); // 设置is为false,使上面的线程结束while循环 demo1.flag = false; System.out.println("被置为false了."); }}

不添加volatile ,就不会打印i的值。

(六) 内存模型

  • ① 介绍

内存模型描述程序的可能行为。JAVA编程语言内存模型通过检查执行跟踪中的每个读操作,并根据某些规则检查该操作观察到的写操作是否有效来工作。

只要程序的所有执行产生的结果都可以由内存模型预测,具体的实现者任意实现,包括操作的重新排序和删除不必要的同步。

内存模型决定了在程序的每个点上可以读取什么值。

  • ② 共享变量描述

可以在线程之间共享的内存称为共享内存或堆内存。所有实例字段,静态字段和数组元素都存储在堆内存中。如果至少有一个访问是写的,那么对同一个变量的两次访问(读或写)是冲突的。线程1修改过共享变量后,将共享变量刷到主内存,然后,线程2从主内存读取该共享变量,将该共享变量载入到工作内存中。

  • ③ 线程操作的定义
  1. write要写的变量以及要写的值。
  2. read 要读的变量以及可见的写入值(由此,我们可以确定可见的值)。
  3. lock 要锁定的管程。
  4. unlock 要解锁的管程。
  5. 外部操作(socket等等)。
  6. 启动和终止。

如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的。这是重排序必须要遵守的规则。

(七)对于同步规则的定义

  • ① 对于监视器m的解锁与所有后续操作对于m的加锁同步

synchronized 在同步关键字,在内存中都明确定义了,保持可见,及时反馈给主内存中。一环扣一环,想可见,必须反馈到主内存。既有同步的语义,还有保持可见性的功能。

import java.util.concurrent.TimeUnit;public class VisibilityDemo1 { // 状态标识 private static boolean is = true; public static void main(String[] args) { new Thread(new Runnable() { public void run() { int i = 0; while (VisibilityDemo1.is) { synchronized (this) { i++; } } System.out.println(i); } }).start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } // 设置is为false,使上面的线程结束while循环 VisibilityDemo1.is = false; System.out.println("被置为false了."); }}
  • ② 对volatile 变量v的写入,与所有其他线程后续对 v 的读同步

变量标识了volatile ,后面不管哪个线程来读,都是同步的,都是可见的。

public class VisibilityDemo {

private volatile boolean flag = true;

public static void main(String[] args) throws InterruptedException {

VisibilityDemo demo1 = new VisibilityDemo();

Thread thread1 = new Thread(new Runnable() {

public void run() {

int i = 0;

// class -> 运行时jit编译 -> 汇编指令 -> 重排序

while (demo1.flag) { // 指令重排序

i++;

}

System.out.println(i);

}

});

thread1.start();

TimeUnit.SECONDS.sleep(2);

// 设置is为false,使上面的线程结束while循环

demo1.flag = false;

System.out.println("被置为false了.");

}

}

  • ③ 启动线程的操作与线程中的第一个操作同步
  • ④ 对于每个属性写入默认值(0,false,null)与 每个线程对其操作的同步
  • ⑤ 线程T1的最后操作与线程T2发现线程T1已经结束同步(isAlive,join可以判断线程是否终结)
  • ⑥ 如果线程T1终端了T2,那么线程T1的中断操作与其他所有线程发现T2倍中断了同步,通过抛出InterruptedException异常,或者调用Thread.interrupted 或者 Thread.isInterrupted。

(八)Happyens-before先行发生原则

  • ① 介绍

强调两个有冲突的动作之间的顺序,以及定义数据争用的发生时机。

  • ② 原则
  1. 同一个线程里面对数据做了变动,后面的动作可以及时的看到,其实还是可见性。
  2. 某个monitor上的unlock动作 happens-before 同一个monitor上后续的lock动作。
  3. 对某个volatile 字段的写操作 happens-before 每个后续对该 volatile 字段的读操作。
  4. 在某个线程对象上调用start() 方法 happens-before 该启动了的线程中的任意动作。
  5. 某个线程中的所有动作 happens-before 任意其他线程成功从该线程对象上的join() 中返回。
  6. 如果某个动作 a 在happens-before 动作 b,b 在happens-before 动作 c,则 a happens-before c。

(九) final 在JMM中的处理

  • ① final在该对象的构造函数中设置对象的字段,当线程看到该对象时,将始终看到该对象的final字段的正确构造版本。
  • ② 如果在构造函数中设置字段后发生读取,则会看到该final字段分配的值,否则它将看到默认值。
  • ③ 读取该共享对象的final成员变量之前,先要读取共享对象。
  • ④ 通常static final 是不可以修改的字段。然而System.in, System.out 和 System.err 是static final 字段,遗留原因,必须允许通过set方法改变,这些字段称为写保护,以区别于普通的final字段。

PS:使用了volatile,unlock和lock的时候,就可以保证代码不进行重排序。内存模型java进阶得一个核心点,这个理解了,其实比写多少年的业务代码要重要很多。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值