第一章 并发编程的挑战
- 上下文切换.CPU通过给每个线程分配时间片来实现对多线程的支持.当前任务执行一个时间片后会切换到下一个任务.但是,切换前会保存上一个任务的状态以便下次再切换到这个任务时,可以再加载这个任务的状态.任务从保存到再加载的过程就是一次上下文切换.
- 死锁. 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象.
- 资源限制的挑战.资源限制是指在进行并发编程的时候,程序的执行速度受限于计算机的硬件资源或者软件资源.硬件限制如:带宽的上传/下载速度,硬盘的读写速度,CPU的处理速度;软件资源限制如:数据库的连接数,socket连接数等.
第二章 java并发机制的底层实现原理
java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM中,JVM执行字节码,最终需要转化为汇编指令在CPU上执行.Java中所使用的并发机制依赖于JVM的实现和CPU的指令.
1.volatile 应用
可见性:当一个线程修改某个共享变量的时候,另一个线程能够读到这个修改的值.
volatile是一种轻量级的synchronized,它在多处理器开发中保证了共享变量的"可见性".如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的.
volatile变量修饰的共享变量在进行写操作的时候,转换成汇编代码会添加lock
前缀指令.这个指令引发了下面两件事:
- 当前处理器缓存行的数据写回到系统内存.
- 这个写回到内存的操作会使其他CPU里缓存了该内存地址的数据无效.
2.synchronized应用
利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁.具体表现为一下三种形式:
- 对于普通同步方法,锁是当前实例对象.
- 对于静态同步方法,锁是当前类的class对象.
- 对于同步代码块, 锁是Synchronized括号里配置的对象.
那么,锁到底存在哪里,锁里面会存储什么信息呢?
synchronized用的锁是存在Java对象头里的.虚拟机用三个字宽存储数组类型对象的对象头,用两个字宽存储非数组对象的对象头.一字宽等于4字节,即32bit.Java对象头里的Mark Word部分默认存储对象的HashCode,分代年龄和锁标记位.Mark Word里存储的数据会随着锁标志位的变化而变化.
Java中锁一共有四种状态,级别从低到高:无锁状态,偏向锁,轻量级锁,重量级锁.这几种状态会随着竞争的情况逐渐升级.锁可以升级,但是不能降级.
3.原子操作的实现原理
原子操作:不可被中断的一个或一系列操作.
处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性.
java中可以通过锁和循环CAS的方式来实现原子操作.
- 循环CAS实现原子操作:JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的.自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止.CAS存在的三大问题:ABA问题,循环时间长开销大,只能保证一个共享变量的原子操作.
- 使用锁机制实现原子操作:锁机制保证了只有获得锁的线程才能操作锁定的内存区域.有意思的是:除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想要进入同步块的时候使用循环CAS的方式获取锁,当它退出同步块的时候使用循环CAS释放锁.
第三章 Java内存模型JMM(Java Memory Model)
1.Java内存模型的基础
在并发编程中,需要处理两个关键问题:线程之间如何通信以及线程之间如何同步.通信是指线程之间以何种机制来交换信息;同步是指程序中用于控制不同线程间操作发生的相对顺序的机制.
在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递.
java内存模型
Java的并发采用的共享内存模型:线程之间共享程序的公共状态,通过读写内存中的公共状态进行隐式通信.同步是显式的,程序员显式指定某个方法或者某段代码需要在线程之间互斥执行.
Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见.从抽象的角度来看:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本.线程A和线程B之间要进行通信,必须经过两个步骤:1. 线程A把本地内存中更新过的共享变量刷新到主内存中.2.线程B到主内存中去读取线程A之前已经更新过的共享变量. 整个通信过程必须经过主内存,JMM通过控制主内存于每个线程的本地内存之间的交互,来为java程序提供内存可见性保证.
指令的重排序
在执行程序的时候,为了提高执行效率,编译器和处理器常常会对指令做重排序.重排序分三种:1.编译器优化的重排序;2.指令级并行的重排序;3.内存系统的重排序.第一个属于编译器重排序,后两个属于处理器重排序.这些重排序可能导致多线程程序出现内存可见性问题,对于编译器,JMM的编译器重排序规则会禁止一些特定类型的编译器重排序.对于处理器,JMM的处理器重排序规则会要求java编译器在生成指令序列的时候插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序.
happens-before简介
在JMM中,如果一个操作执行的结果必须要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系.这里的两个操作,既可以是一个线程之内的,也可以是不同线程之间.
happens-before的定义:
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的顺序排在第二个操作之前.
2.两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要求按照happens-before关系指定的顺序来执行.如果重排序之后的执行结果与按happens-before关系来执行的结果一致,那么这种排序并不非法.(JMM允许这种排序)
与程序员密切相关的happens-before规则如下:
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作.
- 监视器规则:对于一个锁的解锁,happens-before于随后对这个锁的加锁.
- volatile变量规则:对于一个volatile域的写,happens-before于任意后续对这个volatile域的读.
- 传递性:A happens-before B, Bhappens-before C,则A happens-before C.
数据依赖性:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,那么这两个操作之间就存在数据依赖性.如图所示:
上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果就会改变.
编译器和处理器在做重排序的时候,要遵守数据依赖性,编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序.这里的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作.编译器会禁止这种会改变程序执行结果的重排序.
as-if-serial 语义
as-if-serial语义的意思是:无论怎么重排序,(单线程)程序的执行结果不能被改变.为了遵循as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作重排序,因为这种重排序会改变执行结果
2.顺序一致性
数据竞争:在一个线程中写一个变量,在另一个线程中读同一个变量,而且写和读没有通过同步来排序.那么就会存在数据竞争.
顺序一致性:顺序一致性内存模型是一种理想化的模型,它有两大特性:
- 一个线程中的所有操作必须按照程序的顺序来执行.
- (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序.在顺序一致性模型中,每个操作都必须原子执行且立刻对所有线程可见.
JMM对同步正确的多线程程序的内存一致性做了如下保证:如果程序是正确同步的,程序的执行结果具有内存一致性--即程序的执行结果与程序在顺序一致性内存模型中的执行结果相同.
3.volatile读-写的内存语义
- volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存.
- volatile读的内存语义:当读一个volatile变量时,JMM会把对应的本地内存置为无效.线程接下来将从主内存中读取共享变量.
4.锁的释放和获取的内存语义
- 当线程释放锁时,JMM会将该线程中对应的本地内存中的共享变量刷新到主内存中.
- 当线程获取锁时,JMM会将该线程中对应的本地内存置为无效.从而使得被监护器保护的临界区代码必须从主内存中读取共享变量.
由此看见,锁的释放如volatile的写具有相同的内存语义,锁的获取于volatile的读具有相同的内存语义.