转载

JMM必知必会

因为CPU处理的速度比内存读取的速度快很多,通过缓存可以极大的提升CPU处理速度。并且,多级缓存的设计,可以平衡缓存大小与芯片体积、成本,在现代CPU中广泛使用。在多核且多级缓存的条件下,如果多个核同时读写内存的同一行,如何保证数据的一致性?

在处理器级别,内存模型定义了什么条件下该核能够看到其他核的写入和该核的写入能够对其他核可见。有以下两种模型:

  1. 强一致内存模型,即任何时间任何核的写入都对其他核可见
  2. 弱一致内存模型,即通过一些特殊的内存屏障指令(Memory Barrier),来刷新内存或者失效本地核处理器缓存,来保证核间的可见性

现在弱一致内存模型越来越流行,因为对一致性的弱化为CPU的性能优化提供了更大的空间。

除了缓存的问题,编译器对 代码的重排序 更加加重了一致性问题。只要没有改变程序的语义,编译器可以自由的调整代码的执行顺序,提前或延后代码的执行,所以对内存的写入也会提前或延后。在真正写入前,其他核是无法看到对内存所做的读写的。

这不是Bug,设计就是这样。只要不违反内存模型的定义,编译器、运行时、硬件都可以自由的去调整执行顺序,来得到最优的性能。

举个例子:

class Reordering {
  int x = 0, y = 0;
  public void writer() {
    x = 1;
    y = 2;
  }

  public void reader() {
    int r1 = y;
    int r2 = x;
  }
}
复制代码

如果读写在两个线程中并发执行,并且读到了r1=2,那么,r2=1吗?

不一定,写线程可能做了代码重排序,如果执行顺序如下:

  1. 写线程写y=2
  2. 读线程读y=2,x=0
  3. 写线程写x=1

程序的执行结果是r1=2, r2=0

最后,引出我们问题的答案,JAVA内存模型定义了在多线程环境下什么样的行为是合法的,并且线程间是如何跟内存交互的。他描述了代码变量跟内存、寄存器处理这些变量的底层实现之前的关系。通过JAVA内存模型的定义,提供了一种使用多种硬件、多种编译器等优化方法仍然能正确运行代码的约定。

Java包含多个关键词volatile,final,synchronized,用来帮助程序员描述并发语义。JAVA内存模型定义了volatile和synchronized的行为,并且确保正确同步的代码在所有的处理器架构上都能正确执行。

为什么其他语言没有,如C++

其他的大部分语言,如C或C++,并没有对多线程提供直接的支持。在多种处理器架构、多种编译器下,多线程的正确执行严重依赖所使用的多线程类库,编译器和程序运行的硬件平台。

内存模型的历史

原来JAVA语言规范里面定义了一个老版本的Java内存模型,但是慢慢发现了很多缺陷,比如volatile的定义。随后又制定了现行的JAVA内存模型,即JSR133,提供了一系列内存模型正式的语义。

什么是指令重排序

第一篇文章已经讲了指令重排序的例子。代码实际执行时,访问变量的指令可能会因为以下原因与代码顺序不符:

  1. 编译器为优化性能重排指令
  2. 处理器在特定情况下重排指令执行顺序
  3. 数据在寄存器、处理器缓存、内存之间的移动顺序
  4. 其他的一些原因,如JIT等

指令重排,从单线程的角度来看,规范规定了不会影响输出结果。但如果一个变量被 多个线程同时访问 ,重排就会影响变量的一致性。为了能够在多线程环境下正确的访问变量,因此需要 正确 的Synchronization。

什么叫不正确的同步

  1. 一个线程写入一个变量
  2. 另外一个线程读取同一个变量
  3. 对这个变量的读写没有使用 同步机制 来决定顺序

所有违反上述条件的都会产生竞态,是不正确的同步。

同步机制是做什么的

同步主要有以下几种影响:

  1. 互斥排他 :同时只能有一个线程获得Monitor
  2. 内存可见性 :当一个线程释放同步锁的时候,会确保自己的写入对其他线程可见。可能是通过数据刷入内存、其他线程失效本地缓存等方式
  3. 禁止重排序 :在同步锁的获取和释放前后的代码块,不会重排序

新的Java内存模型在内存操作(读字段,写字段,lock,unlock)和线程操作(start,join)之间定义了顺序,叫做一种操作 happens before 其他操作。当一种操作happens before另外一种操作时,第一个操作被确保在第二个操作之前执行,而且操作内容对第二个操作可见。具体规则如下:

  1. 单线程里面每个操作 happens before 代码里面此操作后面的操作
  2. 对一个monitor的unlock操作 happens before 在 这个monitor 上所有后续的lock操作
  3. 对一个volatile字段的写入 happens before 对 这个字段 的所有后续读操作
  4. 对一个线程的start操作 happens before 此启动线程里面的任何操作
  5. 对一个线程使用join操作,被join线程里面的任何操作 happens before join() 调用的返回

所以,如果对一个monitor进行同步,所有释放monitor前的操作都对后续获取monitor的线程可见。因为所有的内存操作 happens before 所释放, 锁释放 happens before 接下来的锁获取。

P.S. Rule1特别解释:

rule1定义了单线程里面所有操作都是按照代码顺序执行的,那是不是就不会产生重排序了?因为重排序后就跟代码顺序不一样了。答案是, No,仍然会重排序 。具体可以参考stackoverflow链接 重排序与happens before

It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.

final是怎么样工作的

只要对象是被 正确的构造 的,只要这个对象构造完成,赋值给final字段的值即使没有同步机制,对其他所有的线程也是可见的。即使final字段是其他对象或数组的引用,这些引用值也至少跟final字段一样是 up to date as of the end of the object's constructor

正确的构造的含义是指在构造过程中,该对象的引用没有泄露。具体可以参考链接 Safe Construction Techniques

简单举个没有正确构造的例子:

public FinalFieldExample() { // bad!
  x = 3;
  y = 4;
  // bad construction - allowing this to escape
  global.obj = this;
}
复制代码

虽然说了这么多,但如果一个线程创建了一个不可变对象(所有字段都是final),你想让其他线程能够正确看到这个对象, 你还是需要使用同步 。因为对这个对象的引用,如果你不使用同步机制,是无法保证被其他线程可见的。

volatile是干什么的

Volatile是用来线程间交换状态特殊关键字。每次 volatile 读都会读到其他任何线程上次写入的值。每次写入后,都会刷入内存。每次读取前,也会失效本地缓存,直接从内存读取。除此之外,还有特殊的限制,跟老的内存模型不同,新的内存模型不允许在volatile字段前后进行指令重排序。当线程 A 在写 volatile 字段 f 前所有可见的字段都会线程 B 读取 f 时可见。

举例:

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }

  public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
  }
}
复制代码

所以对volatile来说就是半个synchronized,在内存可见性方面保持一样,但不具有排他性。

double-checked locking

class Singleton{
	private static Something instance = null;
	
	public static Something getInstance() {
	  if (instance == null) {
	    synchronized (Singleton.class) {
	      if (instance == null)
	        instance = new Something();
	    }
	  }
	  return instance;
	}
}
复制代码

上面的写法是有问题的,大家可以根据学到的知识进行分析一下,哪些地方存在问题?如何解决?有没有更好的单例写法?

引用

JSR133 FAQ

饿了么蜂鸟物流招聘啦!

欢迎有意象的同学踊跃投递简历!

简历投递邮箱:wei.chensh@ele.me

资深Java工程师

在这里,你将能够深度参与行业领先的即时配送核心系统开发工作、了解饿了么微服务架构,更能够在日常开发工作中深度践行饿了么多活的核心技术 。

职责

1. 负责物流业务系统相关的需求分析、代码开发、代码审查工作
2. 配合架构师、技术Leader确保业务系统技术产出质量,对系统可用性进行设计,代码质量进行把控,确保系统稳定性等
复制代码

岗位要求

1. 本科及以上学历(985/211优先),扎实的计算机基础
2. 有过复杂、高并发交易系统的架构设计和优化经验,尤其是深度参与过互联网业务架构设计的优先,拥有和工作年限相称的广度和(或)深度
3. 3年及以上工作经验,长期使用JAVA及开源框架进行项目开发,并有一定得项目管理经验;深入使用Java,熟悉掌握常用的Java类库及框架,如多线程、并发处理、I/O与网络通讯,Spring、Mybatis等;有系统排障经验,可以快速排查定位问题
4. 至少对高并发、分布式、缓存、jvm 调优、序列化、微服务等一个或多个领域有过研究,并且有相关实践经验
5. 熟悉 MySQL 应用开发,熟悉数据库原理和常用性能优化技术,以及 NoSQL,Queue 的原理、使用场景以及限制。
6. 学习能力强,认真负责,对技术有热情有渴望
7. 具备良好的分析解决问题能力,能独立承担任务
8. 具有良好的沟通、团队协作、计划和主动性思考的能力,在互联网或业界有一定影响力公司的工作经验者优先
复制代码

资深测试工程师

职责

1.负责物流产品线的测试和质量保障工作;
2.参与所负责产品的需求分析,主导产品的相关测试,制定相应的测试策略及方案;
3.针对测试中的重复工作实现自动化,以提升工作效率;
4.理解系统架构,预测系统性能瓶颈,并能对关键路径做性能和压力测试;
5.了解持续集成,能够部署与维护测试环境;
6.与团队保持良好的沟通,对测试中发现的问题进行及时的跟踪与反馈,协助开发人员分析和解决问题;
7.尝试拓宽新的方法和工具,提高测试效率;
复制代码

岗位要求

1.两年以上web相关测试工作经验,熟悉测试流程和规范;
2.具有扎实而全面的测试理论、测试设计和分析等方法;
3.最好有互联网大型高并发分布式系统的开发测试经验;
4.熟练掌握java、python或go等至少一种编程语言;
5.熟悉常用的测试框架junit或testng,熟悉restful/soap/thrift自动化测试;
6.强大的求知欲和学习能力,优秀的团队合作精神,良好的沟通能力,责任心强;
7.熟悉j常用的工具jmeter、linux、mysql、redis、maxq等;
复制代码

阅读博客还不过瘾?

欢迎大家扫二维码通过添加群助手,加入交流群,讨论和博客有关的技术问题,还可以和博主有更多互动

JMM必知必会

博客转载、线下活动及合作等问题请邮件至 shadowfly_zyl@hotmail.com 进行沟通

原文  https://juejin.im/post/5ba0654be51d450e452a9650
正文到此结束
Loading...