Java解决可见性和有序性问题:Java内存模型

什么是 Java 内存模型?

Java 内存模型是个很复杂的规范,可以从不同的视角来解读,站在我们这些程序员的视角,本质上可以理解为,

Java 内存模型规范了 JVM 如何提供按需禁用缓存编译优化的方法。具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则。

Happens-Before 规则:前一个操作的结果对后续操作可见。

前面一个操作的结果对后续操作是可见的
前面一个操作的结果对后续操作是可见的
  • 程序的顺序性规则

    程序的顺序性规则
    程序的顺序性规则
    程序的顺序性规则

    程序的顺序性规则

    •   在一个线程中,按照程序顺序,对前部分代码的操作对后面的操作是可见的
  • volatile规则
    •   对volatile变量的写相对于volatile变量的读可见
  • 传递性规则
    •   a对b可见,b对c可见,则a对c可见
  • 管程中锁的规则

    •   对一个锁的解锁对后续对这个锁的加锁可见
  • 线程 start() 规则
    • 指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
    •   
       1 package com.jeek_time.java并发编程实战.二.happensbefore;
      2
      3 /**
      4 * 测试happens-before中的start规则
      5 * 主线程中start子线程B,在start B线程时可看到主线程对共享变量的操作, 0 -> 1
      6 */
      7 class TestStart{
      8 static int i=0;
      9 public static void main(String[] args) {
      10 i=1;
      11 final Thread subThreadB = new Thread(() ->{
      12 System.out.println(TestStart.i);
      13 });
      14 subThreadB.setName("子线程B");
      15 subThreadB.start();
      16 }
      17 }
  • 线程 join() 规则
    •  指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作(共享变量)
    • 下面这段代码的结果:先0后1,刚好可印证结论,很有趣。 
    •  1 package com.jeek_time.java并发编程实战.二.happensbefore;
      2
      3 import lombok.SneakyThrows;
      4
      5 /**
      6 * 测试happens-before中的join规则
      7 * 指主线程等待子线程 B 完成(主线程通过调用子线程 B 的 join() 方法实现),
      8 * 当子线程 B 完成后(主线程中 join() 方法返回),主线程能够看到子线程B的操作(共享变量)
      9 */
      10 public class TestJoin{
      11 static int i = 0;
      12 @SneakyThrows
      13 public static void main(String[] args) {
      14 final Thread subThreadB = new Thread(() ->{
      15 TestJoin.i = 1;
      16 try {
      17 Thread.sleep(1000);
      18 } catch (InterruptedException e) {
      19 e.printStackTrace();
      20 }
      21 });
      22 subThreadB.setName("子线程B");
      23 subThreadB.start();
      24
      25 System.out.println("返回前:" + TestJoin.i);
      26 subThreadB.join();
      27 System.out.println("返回后:" + TestJoin.i);
      28 }
      29
      30 }
final
  final 修饰变量时,初衷是告诉编译器:这个变量生而不变,可以可劲儿优化。
  问题类似于上一期提到的利用双重检查方法创建单例,构造函数的错误重排导致线程可能看到 final 变量的值会变化。
  在 1.5 以后 Java 内存模型对 final 类型变量的重排进行了约束。现在只要我们提供正确构造函数没有“逸出”,就不会出问题了。
 
  比如,在构造函数里面将 this 赋值给了全局变量 global.obj,这就是“逸出”,线程通过 global.obj 读取 x 是有可能读到 0 的。因此我们一定要避免“逸出”。
  

2 final int x;
3 // 错误的构造函数
4 public FinalFieldExample() {
5 x = 3;
6 y = 4;
7 // 此处就是讲this逸出,
8 global.obj = this;
9 }
 

  Happens-Before 规则最初是在一篇叫做 Time, Clocks, and the Ordering of Events in a Distributed System 的论文中提出来的,在这篇论文中,Happens-Before 的语义是一种因果关系。在现实世界里,如果 A 事件是导致 B 事件的起因,那么 A 事件一定是先于(Happens-Before)B 事件发生的,这个就是 Happens-Before 语义的现实理解。
  在 Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。
 
课后思考
有一个共享变量 abc,在一个线程里设置了 abc 的值 abc=3,你思考一下,有哪些办法可以让其他线程能够看到abc==3?
  •   声明共享变量abc,并使用volatile关键字修饰abc
  •   声明共享变量abc,在synchronized关键字对abc的赋值代码块加锁,由于Happen-before管程锁的规则,可以使得后续的线程可以看到abc的值。
  •   A线程启动后,使用A.JOIN()方法来完成运行,后续线程再启动,则一定可以看到abc==3

摘自极客时间王宝令老师的课程

java并发编程实战《二》java内存模型的更多相关文章

  1. java并发编程(9)内存模型

    JAVA内存模型 在多线程这一系列中,不去探究内存模型的底层 一.什么是内存模型,为什么需要它 在现代多核处理器中,每个处理器都有自己的缓存,定期的与主内存进行协调: 想要确保每个处理器在任意时刻知道 ...

  2. JAVA并发编程的艺术 JMM内存模型

    锁的升级和对比 java1.6为了减少获得锁和释放锁带来的性能消耗,引入了"偏向锁"和"轻量级锁". 偏向锁 偏向锁为了解决大部分情况下只有一个线程持有锁的情况 ...

  3. java并发编程实战(java concurrency in practice)

    第一章   线程共享进程范围内的资源,但每个线程都有各自的程序计数器.栈以及局部变量等. 多个线程可以同时调度到多个CPU上运行.   线程的优势? 在服务应用程序中,可以提升资源利用率以及系统吞吐率 ...

  4. 【Java并发编程实战】----- AQS(二):获取锁、释放锁

    上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...

  5. Java并发编程实战——读后感

    未完待续. 阅读帮助 本文运用<如何阅读一本书>的学习方法进行学习. P15 表示对于书的第15页. Java并发编程实战简称为并发书或者该书之类的. 熟能生巧,不断地去理解,就像欣赏一部 ...

  6. 【Java并发编程实战】-----“J.U.C”:ReentrantLock之一简介

    注:由于要介绍ReentrantLock的东西太多了,免得各位客官看累,所以分三篇博客来阐述.本篇博客介绍ReentrantLock基本内容,后两篇博客从源码级别分别阐述ReentrantLock的l ...

  7. java并发编程实战学习(3)--基础构建模块

    转自:java并发编程实战 5.3阻塞队列和生产者-消费者模式 BlockingQueue阻塞队列提供可阻塞的put和take方法,以及支持定时的offer和poll方法.如果队列已经满了,那么put ...

  8. 《java并发编程实战》笔记

    <java并发编程实战>这本书配合并发编程网中的并发系列文章一起看,效果会好很多. 并发系列的文章链接为:  Java并发性和多线程介绍目录 建议: <java并发编程实战>第 ...

  9. 《Java并发编程实战》文摘

    更新时间:2017-06-03 <Java并发编程实战>文摘,有兴趣的朋友可以买本纸质书仔细研究下. 一 线程安全性 1.1 什么是线程安全性 当多个线程访问某个类时,不管运行时环境采用何 ...

随机推荐

  1. 19、Haystack

    Haystack 1.什么是Haystack Haystack是django的开源全文搜索框架(全文检索不同于特定字段的模糊查询,使用全文检索的效率更高 ),该框架支持Solr,Elasticsear ...

  2. C++ 基础 1:C++ 对 C 语言的增强

    1 namespace 命名空间 1.1 C++ 命名空间的定义 C++标准 引入了关键字 namespace(命名空间),可以更好地控制标识符的作用域. namespace name { ... } ...

  3. java enum 枚举值

    public enum PieChartEnum { PIE00("pie00"), PIE10("pie10"), PIE11("pie11&quo ...

  4. sk_buff结构--转载

    套接字缓存之sk_buff结构 https://www.cnblogs.com/wanpengcoder/p/7529486.html 来此此处 sk_buff结构用来描述已接收或者待发送的数据报文信 ...

  5. MySQL死锁问题(转)

    线上某服务时不时报出如下异常(大约一天二十多次):"Deadlock found when trying to get lock;". Oh, My God! 是死锁问题.尽管报错 ...

  6. python-redmine获取redmine平台的信息

    前言 最近做redmine的bug平台的分析,统计一些需要用到的数据,这里把相关调用的地方记录下来以备后用 相关的获取接口 #! /usr/bin/python # -*- coding:UTF-8 ...

  7. http服务器文件名大小写忽略

    问题 文件从windows里面放到nginx里面去的时候,文件在windows下面是大小写忽略,也就是不论大小写都可以匹配的,而到linux下面的时候,因为linux是区分大小写的,也就是会出现无法忽 ...

  8. TypeScript 引入第三方包,报无法找到模块错误

    以 react-router-dom 模块为例 1. npm加上 @types/ 根据报错提示尝试安装该库的TypeScript版本 (该库的 ts 声明文件),也就是在该库的名称前加上 @types ...

  9. JavaScript复习大纲

    1. HTML.CSS和JavaScript各自在网页设计中的作用. 1.HTML生成结构. 2.CSS样式美化. 3.JavaScript的作用: (1) 操作HTML及CSS,让网页具有动态行为. ...

  10. 学会网页制作,web app开发,必须先从语法基础开始学习

    做软件开发,是从事编程开发工作,必须先从语法基础开始学习,通过语法组成产品效果.      前端开发的基础语法,由HTML+CSS+JavaScript组成,这是前端开发最基本的3个语言.       ...