从零开始的高并发之路(三)

  1. JMM内存模型
  2. 缓存一致性协议MESI
  3. 并发三大特性
    1. 可见性
    2. 原子性
    3. 有序性
  4. 指令重排
  5. happens-before
  6. volatile
  7. 读写锁设计思路

JMM内存模型

Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。为了保证数据的一致性,会通过总线锁或缓存一致性协议解决。

JMMmemoryModel.png

缓存一致性协议MESI

此处不会做过多的了解,主要只,当一个线程的从主存中将数据加载到缓存中,当缓存中的数据被修改时,会第一时间回写到主存,并将其他线程缓存中的数据置为失效,需要重新从主存中获取。

  • M(Modified):这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。
  •   E(Exclusive):这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中。
  •   S(Shared):这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中。
  •   I(Invalid):这行数据无效。

并发三大特性

可见性

  可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

原子性

  即一个或者多个操作作为一个整体,要么全部执行,要么都不执行,并且操作在执行过程中不会被线程调度机制打断;而且这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换。

有序性

  即程序执行的顺序按照代码的先后顺序执行。

指令重排

程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序.

修改JAVA_HOME/jre/lib/i386/jvm.cfg, 将jvm调整为server模式 server模式相较于client模式启动较慢 ,但运行更快,性能更高, 在server模式也更容易出现可见性问题 将文件中的server调整到client上方。
-server KNOWN
-client KNOWN

happens-before

  • 顺序原则

一个线程内保证语义的串行性; a = 1; b = a + 1;

  • volatile规则

volatile变量的写,先发生于读,这保证了volatile变量的可见性,

  • 锁规则

解锁(unlock)必然发生在随后的加锁(lock)前.

  • 传递性

A先于B,B先于C,那么A必然先于C.

  • 线程启动法则

线程的start()方法先于它的每一个动作.

  • 线程终结法则

线程的所有操作先于线程的终结 或者从Thread.join()中调用成功返回或者Thread.isAlive返回false

  • 终结法则

对象的构造函数执行结束先于finalize()方法.

  • 中断法则
    一个线程调用另一个线程的interrupt happen-before 于被中断的线程发现中断 (通过抛出innterruptedException 或者调用isinterrupted 和interrupted)

volatile

  volatile可以保证数据的可见性,当一个线程中的数据修改之后,这个修改是对其他线程可见的。effective java 中描述, 当一个线程对某一变量具有读操作,而其他线程可能同时对该变量有写操作,使用volatile 或者当一个线程对某一变量有写操作,而同时其他线程可能对改变了有读操作是使用volatile。

读写锁设计思路

当需求中对共享资源的读写操作读操作比较多时,对读写同时加锁就难念会浪费大量的资源,我们知道,多个线程读操作之间并不会产生线程安全问题,那么我们可以这样考虑下,我们看下表 ,多个线程直接当都是读操作时,我们其实并不需要锁,当出现写操作时则一定要加锁保证数据安全问题。

读操作 写操作
读操作 false true
写操作 true true