1.概述
2.硬件的效率与一致性
计算机cpu进行计算必定会关联到内存的读写操作,实际情况是计算机存储设备与处理器的运算速度有几个数量级的差距,现代计算机不得不加入一层读写速度与处理器接近的高速缓存来作为内存与处理器之间的缓冲。
引入高速缓存区带来了一些数据同步方面的复杂度。如何保证多核cpu之间缓存的一致性问题,引出了一些缓存一致性的规则即一致性协议。(MSI、MESI、MOSI Synapse 、Firefly)
(如图 处理器 高速缓存 主内存间的交互关系)
3.java内存模型
java 内存模型(JMM)是为了屏蔽各种硬件和操作系统的内存访问差异,java虚拟机规范中试图定义一种java内存模型。定义java内存模型并非一件容易的事情,既要保证严谨性避免并发内存访问操作产生歧义,又要足够的宽松使jvm的实现有足够的自由空间利用硬件的各种特性(寄存器,高速缓存和指令集中某些特有的指令)
3.1主内存与工作内存
java内存模型的主要目标是定义程序中各个变量的访问规则,从jvm中存储变量到内存和从内存中取出变量这样的底层细节。(这里的变量和java中的变量还有些不一致,这里的变量不包括方法参数和局部变量,因为以上变量是线程独有的)成员变量,类变量和构成数据的静态元素
java内存模型规定所有变量存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中存储着该线程用到主内存变量的副本拷贝
(如图 java主内存工作内存与线程之间的关系)
tips: 这里所讲的主内存与工作内存 与内存区域中的堆栈方法区并不是一个层次的内存划分实际上两者没有关系。 程序运行主要读写的是工作内存
3.2内存间交互操作
主内存和工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之类的实现细节,java内存模型定义了8中操作。jvm的实现必须保证8种操作都是原子的。
lock 、unlock、 read、 load、 use、 assign(赋值)、store、 write
1、lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
2、unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
3、read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
4、load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
5、use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
6、assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
7、store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
8、write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
3.3对于volatile型变量的特殊规则
关键字volatile 可以说是java虚拟机提供的最轻量级的同步机制,但是它并不容易完全被正确完整地理解以至于许多程序员都习惯不去使用它,遇到需要处理多线程的情况一律使用Synchronized来进行同步。
java内存模型对volatile专门定义了一些特殊的访问规则,当一个变量被定义为volatile 后将具备两种特性
1. 保证此变量对所有线程的可见性,
一个线程修改了volatile变量后,新值对于其它线程来说是可以立即得知的。普通变量不能做到只一点,普通变量只能通过主内存来完成线程之间变量值的共享。 (Thread A 更改了变量,然后同步到主内存,Thread B 通过读取主内存实现共享)
2.第二个语义是禁止指令重排序优化
所谓禁止指令重排所指的重排序优化是机器级的优化操作,提前执行是指这句话对应的汇编代码被提前执行。
为什么添加了volatile辨识后会达到这种效果?
简单的理解就是有volatile修饰的变量,赋值后多执行了一个 lock xxx 操作,这个操作相当于一个内存屏障(memory barrier 或 memory fence),排序时不能把后面的指令重排序到内存屏障之前的位置。
tip: lock 属于字节码命令,对当前操作加锁,一个cpu访问内存时,不需要内存屏障,但如果多个cpu访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性了
对于volatile 与Synchronized 如何选择?
来自于volatile的语义能否满足使用场景的需求
3.4对long和double型变量的特殊规则
[long 和 double 的非原子性协定] ,在32位和64位操作系统下,java内存模型,允许虚拟机将没有被volatile修饰的64位数据的读写操作划分两次32位操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load store read 和 write 操作的原子性
3.5原子性、可见性与有序性
3.5.1 原子性
java内存模型是围绕着在并发过程中如何处理原子性可见性和有序性这3个特征来建立的
对于基本类型的访问读写 有内存模型来直接保证,通过 内存间交互操作 8种操作符号实现,如果需要更大范围的原子性操作,java内存模型还提供了lock 和 unlock,虽然没有放开给用户直接调用,但却提供了更高层次的字节码指令 monitorenter monitorexit 来隐式的使用这两个操作,反映到java代码中就是同步快 Synchronized。
3.5.2 可见性
当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。 volaile 变量中java内存模型通过在变量修改后将新值同步回主内存,并在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性,普通变量也是通过主内存作为传递媒介,但是区别是volatile修饰的变量,由volatile的特殊规则保证了新值能立即同步到主内存,而普通变量不行。
synchronized 和 final 也能实现可见性,一个使用lock unlock,同步块可见性是由对一个变量执行unlock操作之前必须先把次变量同步回主内存中(store,write操作)这条规则获得,final 修饰字段在构造器一旦初始化完成,其他线程就能看到这个final字段的值,(注意有 this 引用逃逸问题)
3.5.3 有序性
java 内存模型的有序性可以总结一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序,前半句指的“线程内表现串行的语义”,后半句是指“指令重排”现象和工作内存与主内存同步延迟现象。
java语言提供了volatile和synchronized两个关键字保证线程之间操作的有序性,volatile本身就包括禁止指令重排序的语义,而synchornized 是由一个变量在同一个时刻只允许一条线程对其进行lock操作,这条规则获得的,这条规则决定了持有同一锁的两个同步快只能串行进入。
3.6先行发生原则
4.java与线程
并发并一定要依赖多线程(入php中很常见的多进程并发),但是在java里面谈论并发,大多数都与线程脱不开关系。
4.1线程的实现
我们知道线程是比进程更轻量级的调度执行单位,线程是cpu调度的基本单位,现成的引入可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址,文件i/o)又可以独立调度。
主流操作系统都提供了线程的实现,我们注意到thread类的大部分的java api都是native的。
实现线程的方法主要有三种方式:
使用内核线程实现
如图: 轻量级进程 lwp,与内核线程之间 1:1的关系
使用用户线程(除了内核线程其他的都是用户线程)
用户线程加轻量级进程混合实现。
4.2java线程调度
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式调度和抢占式线程调度
协同式调度,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后要主动通知系统切换到另一个线程上
抢占式调度的多线程系统,每个线程将有系统分配执行时间,线程的切换不由线程本身决定。
线程优先级的引入,我们可以建议系统给某些线程多分配一些执行时间另一些线程可以少分配一些时间,java语言一共10个级别优先级。
tip: 线程优先级并不靠谱,原因是java的线程是通过映射到原生线程上来实现的,比如window 系统只有7种优先级。所有会有 一些优先级相同的情况发生。
4.3状态转换
小结
这里了解了jvm的java内存模型的结构及操作,然后讲解了原子性、可见性、有序性在java内存模型中的体现,最后介绍了线性发生原则的规则及使用。另外还了解了线程在java语言之中是如何实现的。