在并发编程中,需要解决两个问题:线程间如何通信&线程间如何同步

线程同步:控制不同线程操作顺序的机制

解决这两个问题的方案有两种:共享内存&消息传递

  共享内存:通过使用共享内存,隐式通信和同步;这里程序员必须显式的指定某个方法或代码块要在线程间互斥执行

  消息传递:通过发消息来通信和同步;由于接收消息必须在发送消息之后,因此算是隐式的设置了同步

而JAVA采用的是共享内存模型。

JMM(Java内存模型)如下

如上图所示,JMM定义了线程和主内存之间的关系:线程之间的共享内存都存储在共享内存中,每个线程有自己的本地内存,本地内存存储了该线程以读/写共享变量的副本。

JMM是通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证(例如有共享变量x=1,线程A和线程B都要修改这个共享变量,那么线程A将x设置为2,同时将x回写到共享内存,那么线程2拿到的就是x=2)

一、概念

说到JMM,就不得不提重排序、happends-before、as-if-serial

1、重排序

  处理器对代码的执行顺序并非按照程序编写的源代码顺序执行,而是编译器和处理器会对源代码做重排序,处理器按照重排序后的结果执行,从而提高性能。

  重排序分为三类:

    编译器优化的重排序:编译器在不改变单线程程序语义的情况下重排序

    指令并行重排序:如果不存在数据依赖,处理器可以重排序

    内存系统重排序:处理器使用缓存和读写缓存区,这使得加载和存储操作看起来乱序。

  从JAVA源代码到最终执行的指令序列所经历的重排序如下:

其中1是属于编译器的重排序,2和3属于处理器的重排序。

上述重排序中,对于内存系统的重排序,由于读写缓存只对当前线程可见,因此会造成多线程运行时的结果异常,因此处理器一般都只会允许对读写进行重排序。

2、happends-before

如果A happends-before于B,那么A的执行一定在B之前

3、as-if-serial

  不管怎么重排序,单线程的执行结果不能被改变。所有的重排序都要遵循这个原则。

二、内存语义

但是如果要是多线程运行,那么由于指令重排和线程的读写缓存问题,会造成执行结果异常,因此就需要使用volatile、锁、final来处理

1、volatile的内存语义

  可见性:对一个volatile修饰的变量的读,总能看到任意线程对这个volatile修饰的变量最后的写入

  原子性:对于任意由volatile修饰的变量的读和写操作都是原子操作;但是对于volatile++这样的操作是非原子性的。

  当写一个volatile修饰的变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到内存中(只要有一个共享变量是volatile修饰,所有的共享变量都会被刷新到主内存中)

  当读一个volatile修饰的变量时,JMM会把该线程对应的内存置为无效,线程将从主内存读取共享变量。

2、锁的内存语义

  当锁释放时,JMM会把该线程对应的共享内存刷新到主内存中

  当获取锁时,JMM会把该线程对应的本地内存置为无效

  由以上两点可见,释放锁的内存语义和volatile的写拥有相同的内存语义,获取锁的内存语义和volatile的读有相同的内存语义。

3、final的内存语义

  写final重排序:禁止把final域的写重排序到构造函数之外。这样可以确保在每次引用为任意线程可见之前,对象的final域已经正确初始化。

  读final重排序:在一个线程中,初次读对象的引用和读该对象中的fianl域之间禁止重排序。

  为什么要将fianl的写限制在构造方法之内:比如一个线程看到一个整形的final域为0(还未初始化,默认值),过一段时间去读取时,发现已经变为1(被初始化完成后的值),因此会造成获取final修饰的值不一致的问题,为了修复该问题,限制了final修饰的写必须在构造方法之内。

三、happends-before

  happends-before是JMM最核心的概念,JMM为了平衡程序员(要求强内存模型,保证内存可见性)和处理器(要求若内存模型,处理器可以自行优化)的需求,设计了happends-before。

  1、happends-before定义:

    a、如果一个操作happends-before另一个操作,那么第一个操作的结果将对第二个操作结果可见,且第一个操作必须在第二个操作之前。

    b、两个操作之间存在happend-before关系,并不意味着java平台具体的实现必须按照happends-before的顺序来执行。如果重排序的结果与happends-before的执行结果一致,那么重排序并不非法。

  上述a是对程序员做出的承诺:如果A happends-before B,那么A的操作必须对B可见,且A的操作在B之前。

  b是对处理器的约束原则:只要不改变程序运行结果,编译器和处理器可以自行优化(这里的程序指的是单线程程序或正确同步的多线程程序)

  2、happends-before规则

    a、程序顺序规则:一个线程中的每个操作,happends-before于该线程任意的后续操作

    b、监视器锁规则:对于一个锁的解锁操作,happends-before于后续对该锁的加锁操作

    c、volatile规则:对于一个volatile修饰的写,hanppends-before于对这个volatile修饰的读

    d、传递性:如果A happends-before B,B happends-before C,那么A hanppends-before C

    e、start()规则:如果线程A中调用了线程B的start方法,那么线程A中调用B线程的start方法的操作happends-before于线程B的任何操作

    f、join()规则:如果线程A调用线程B的join方法,那么线程B中的任意操作happend-before于线程A从B.join()操作返回成功

四、双重检查锁定与延迟初始化

  对于很多场景,都会延迟加载类,来降低初始化类和创建对象的开销,待使用时在延迟加载。

  其实这里就是我们常说的单例模式。

  

public class InitDemo {
private static InitDemo instance; public static InitDemo getInstance(){
if(instance == null){
instance = new InitDemo();
}
return instance;
} public static InitDemo getInstance1(){
synchronized (InitDemo.class){
if(instance == null){
instance = new InitDemo();
}
}
return instance;
} public static InitDemo getInstance2(){
if(instance == null){
synchronized (InitDemo.class){
if(instance == null){
instance = new InitDemo();//有问题
}
}
}
return instance;
}
}

以上写法都是有问题的:

  getInstance方法的问题:非线程安全,可能会创建多个实例(俗称:懒汉式)

  getInstance1方法的问题:每次使用都加锁,性能消耗大

  getInstance2方法的问题:同样是非线程安全的

解决方案:

解决方案一:使用volatile

public class InitDemo {
private volatile static InitDemo instance; public static InitDemo getInstance(){
if(instance == null){
synchronized (InitDemo.class){
if(instance == null){
instance = new InitDemo();//有问题
}
}
}
return instance;
}
}

由于使用了volatile,因此多线程(都是第一次访问)创建对象时,就可以保证线程安全。

解决方案二:基于类初始化方式

public class InitDemo {
private static class InitDemoFactory{
public static InitDemo instance = new InitDemo();
} public static InitDemo getInstance(){
return InitDemoFactory.instance;
}
}

优缺点对比:

  基于类加载模式的方案,代码更简洁;但是只能对静态字段实现延迟加载

  使用volatile,除了可以对静态字段做延迟加载外,还可以对实例字段实现延迟加载。

并发02--JAVA内存模型的更多相关文章

  1. 高效并发一 Java内存模型与Java线程(绝对干货)

    高效并发一 Java内存模型与Java线程 本篇文章,首先了解虚拟机Java 内存模型的结构及操作,然后讲解原子性,可见性,有序性在 Java 内存模型中的体现,最后介绍先行发生原则的规则和使用. 在 ...

  2. Java并发编程-Java内存模型

    JVM内存结构与Java内存模型经常会混淆在一起,本文将对Java内存模型进行详细说明,并解释Java内存模型在线程通信方面起到的作用. 我们常说的JVM内存模式指的是JVM的内存分区:而Java内存 ...

  3. 【深入理解JAVA虚拟机】第5部分.高效并发.1.Java内存模型与线程。

    1.概述 摩尔定律:描述处理器晶体管数量与运行效率之间的发展关系.Amdahl定律:通过系统中并行化与串行化的比重来描述多处理器系统能获得的运算加速能力. 从摩尔定律到Amdahl定律的转变,代表了近 ...

  4. 02 | Java内存模型:看Java如何解决可见性和有序性问题

    什么是 Java 内存模型? 导致可见性的原因是缓存,导致有序性的原因是编译优化,那解决可见性. 有序性最直接的办法就是禁用缓存和编译优化,但是这样问题虽然解决了,我们程序的性能可就堪忧了.   合理 ...

  5. 并发编程-Java内存模型

    将之前看过的关于并发编程的东西总结记录一下,本文简单记录Java内存模型的相关知识. 1. 并发编程两个关键问题 并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步. (1)在命令式 ...

  6. 02 java内存模型

    java内存模型 1.JVM内存区域 方法区:类信息.常量.static.JIT (信息共享) java堆:实例对象 GC (信息共享) OOM VM stack:JAVA方法在运行的内存模型 (OO ...

  7. 并发编程 —— Java 内存模型总结图

    关于 Java 内存模型的类似思维导图. 如有错误,还请指正.

  8. 【死磕Java并发】-----Java内存模型之happend-before

    在上篇博客([死磕Java并发]-–深入分析volatile的实现原理)LZ提到过由于存在线程本地内存和主内存的原因,再加上重排序,会导致多线程环境下存在可见性的问题.那么我们正确使用同步.锁的情况下 ...

  9. 【死磕Java并发】-----Java内存模型之happens-before

    在上篇博客([死磕Java并发]-–深入分析volatile的实现原理)LZ提到过由于存在线程本地内存和主内存的原因,再加上重排序,会导致多线程环境下存在可见性的问题.那么我们正确使用同步.锁的情况下 ...

  10. 【死磕Java并发】-----Java内存模型之重排序

    在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件: 在单线程环境下不能改变程序运行的结果: 存在数据依赖关系的不 ...

随机推荐

  1. hdl - 软件编程与硬件建模

    https://mp.weixin.qq.com/s/Y75I9e3s4fDPZuv-CyiNDA   介绍软件编程思维与硬件建模思维的不同与相似之处.   ​​   1. 软件编程思维   编程,意 ...

  2. Splay代码简化版

    皆さん.こんにちは.上一篇文章,我们讲了Splay如何实现.这一篇我们来让我们的伸展树短一点. 上一篇Splay讲解的链接:リンク. 首先还是变量的定义,在这里呢,我把一些小函数也用Define来实现 ...

  3. MySQL国内镜像下载地址

    最近重新下载MySQL发现官网下载速度不是一般的慢,官网下载要几个钟而且一不注意就被取消下载了,实在受不了 可以使用sohu的镜像:http://mirrors.sohu.com/mysql/MySQ ...

  4. 判断IP地址的合法性

    每台计算机都有独一无二的编号,称为ip地址,每个合法的ip地址由‘.’分隔开的4个数字组成,每个数字的取值范围为0--255 输入一个字符串,判断其是否为合法的IP地址,若是输出‘YES’,否则输出‘ ...

  5. Java实现 LeetCode 187 重复的DNA序列

    187. 重复的DNA序列 所有 DNA 都由一系列缩写为 A,C,G 和 T 的核苷酸组成,例如:"ACGAATTCCG".在研究 DNA 时,识别 DNA 中的重复序列有时会对 ...

  6. Java实现 LeetCode 98 验证二叉搜索树

    98. 验证二叉搜索树 给定一个二叉树,判断其是否是一个有效的二叉搜索树. 假设一个二叉搜索树具有如下特征: 节点的左子树只包含小于当前节点的数. 节点的右子树只包含大于当前节点的数. 所有左子树和右 ...

  7. Java实现 LeetCode 52 N皇后 II

    52. N皇后 II n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击. 上图为 8 皇后问题的一种解法. 给定一个整数 n,返回 n 皇后不同的解决方案 ...

  8. Java学习的一般过程

    伴随着科学技术的不断发展,世界开始走向信息化.网络化.大数据化.自然而然,计算机专业变得十分热门.尽管如此,计算机专业人才对社会来说仍然是供不应求,当然,这里指的是高层次技术人才.因此,对于我们这些占 ...

  9. jQuery - Ajax ajax方法详解

    $.ajax()方法详解 jquery中的ajax方法参数总是记不住,这里记录一下. 1.url: 要求为String类型的参数,(默认为当前页地址)发送请求的地址. 2.type: 要求为Strin ...

  10. Quartz SpringBoot 简单整合一下

    一次简单的代码整合记录. 数据库准备 如果是MySQL可能出现一些小问题.比如联合主键长度超限制,已经记录解决办法了. CREATE TABLE QRTZ_JOB_DETAILS ( SCHED_NA ...