The Art of Java Concurrency Programming I
第一章 并发编程的挑战
- 单核处理器通过时间片分配达到并发,时间片一般几十毫秒
- 时间片切换一次则保存上个任务状态,加载下个任务,任务从保存到加载则完成一次上下文切换
- 减少上下文切换:无锁并发,CAS算法,减少线程数,协程
- 避免死锁:避免一个线程获取多个锁,避免一个锁占用多个资源,使用定时锁,数据库锁加解锁必须针对同一个连接
第二章 Java并发的底层原理
- volatile:底层汇编使用lock前缀:
- 将当前处理器缓存行数据写回系统内存
- 此操作使得其他cpu核心缓存的该数据无效
- 多处理器系统实现缓存一致性协议,每个cpu核心的缓存都一致
- 处理器使用嗅探技术嗅探总线传播的数据以检查自己的数据是否过期,如果发现其他处理器写内存地址,则标记自己的缓存行无效,下次使用重新取内存数据
在上图中,有两个处理器,每个处理器有两个核心,每个核心有两个线程。线程们共享一级缓存。核心(以深灰色表示)有独立的一级缓存,同时共享二级缓存。处理器(淡灰色)之间不共享任何缓存。这些信息很重要,特别是在讨论多进程和多线程情况下缓存的影响时尤为重要。 追加字节方式使用volatile不一定可用,缓存行非64字节或者共享变量不会频繁写(锁的几率小),以及新的编译器可能使这种优化无效
synchronized
- 普通方法,锁为当前实例,静态方法为Class对象,同步方法块为括号中的对象
- 锁的状态:无锁,偏向锁,轻量级锁,重量级锁,可升级不会降级
3、原子操作的实现原理
- 自动保证基本的内存操作原子性,一个处理器读取一个字节,其他处理器不能访问此内存地址
- 复杂内存操作的原子性保证:总线锁&缓存锁
- 总线锁:LOCK#信号使得一个处理器发出信号时,其他处理器请求阻塞,开销大
- 缓存锁:(目前的一般情况,intel做了优化)如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态(参考)),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。(例外:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行,处理器会调用总线锁定。又或者处理器不支持缓存锁定)
第三章 Java内存模型
3.1 基础
- 命令式编程中线程通信机制有两种:共享内存与消息传递
- Java采取共享内存机制,Java中的线程通信隐式进行,整个过程对coder透明
- Java中所有实例域,静态域和数组元素都存储在堆内存中,堆内存线程间共享
- Java线程之间的通信由Java内存模型控制,称为JMM,其定义了线程与主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程有一个私有的本地内存,存储了共享变量的副本(本地内存是JMM的抽象概念)
- 线程通信的步骤:
- 程序执行时,编译器和处理器会对指令进行重排序:编译器优化重排序;指令级并行重排序(现代处理器的指令级并行技术,ILP);内存系统的重排序(缓存与读写缓冲区)
- Java代码到指令序列,经过3种重排序:编译器->指令级->内存系统重排序
- 指令级重排序与内存系统重排序属于处理器重排序,JMM的重排序规则会干预重排序的操作,包括编译器重排序与处理器重排序(加入内存屏障)
每个处理器的写缓冲区只针对所在的处理器可见,用以减少占用内存总线,使得对内存操作顺序产生影响,处理器对内存的写读顺序不一定与内存实际发生的顺序一致
12345处理器1: a=1; x=b;处理器2: b=2; y=a;结果:初始状态:a=b=0处理器可能得到结果:x=y=0(x=b与y=a先执行了)内存屏障:LOADLOAD、STORESTORE、LOADSTORE、STORELOAD,确保屏障前的操作先于屏障后的操作,比如Store1;StoreLoad;Load2 保证内存屏障前的写操作刷新至内存以对其他处理器可见
3.2 重排序
- as-if-serial:不管怎么重排序,(单线程)程序的执行结果不能变,编译器、runtime、处理器必须准守此语义,因此处理器不会对数据依赖关系的操作重排序
A->C;B->C 中 A、B可重排序3.3 顺序一致性
- 加锁之后的线程得到顺序一致性保障,在临界区内可以进行重排序以提高效率
- JMM不保证64位的数据类型有原子性,每次数据通过处理器与内存之间的总线进行通讯,通过一系列操作完成,称为总线事务,包括读事务与写事务,64位数据在32位处理器会拆成两次操作进行,java5开始允许写操作拆开,而读操作必须有原子性(在一个事务中)
3.4 volatile的内存语义
- volatile变量的读,总是能看到任意线程对这个变量最后的写入
- 对单个volatile变量的读写有原子性,而复合操作不具有原子性,如v++
- 内存读写的语义:
- 线程A写一个volatile变量,实质是A向接下来读取的线程发出修改消息;
- 线程B读一个volatile变量,实质是B接收到了某个线程在写此变量之前对共享变量所做修改的消息;
- A写一个变量,B读取一个变量,实质上是线程A通过主内存向B发送消息
- volatile内存语义通过内存屏障实现,写前插入SS,写后插入SL,读后插入LL,读后插入LS(读后的LL和LS可以选择性省略,比如读后没有读,读后没有写),
每个写操作之后都有SL(保守策略,因为编译器无法得知后续是否需要插入SL屏障,比如写后立即return) - X86处理器只会对写-读进行重排序,而不会对读-读,读-写与写-写作重排序,此时JMM只在写后插入SL即可,所以volatile的写操作在X86处理器中比读操作开销大很多(执行SL开销大,当前处理器需要把写缓冲数据全部刷新至内存)
- java 5开始对volatile变量增强语义,严格限制编译器和处理器对volatile与普通变量的重排序,不可以破坏volatile的内存语义
3.5 锁的内存语义
- 锁的释放与获取内存语义:
- (1)线程A释放一个锁标识其向接下来获取此锁的线程发出线程A对共享变量所做修改的消息
- (2)线程B获取一个锁表示B接受了一个线程发出的释放此锁之前对共享变量所作修改的消息【获取锁的线程会将本地内存的共享变量置为无效,强制从主内存获取】
- (3)线程A释放锁,随后线程B获取这个锁,表示A通过主内存向线程B发消息
- 锁的实现依赖AQS,见后续章节
3.6 final域的内存语义
- 构造函数内对final域的写入(JMM禁止编译器将final域的写重排序到构造函数之外,写之后在return之前插入一个SS屏障,此屏障禁止处理器将final的写重排序到构造函数之外)与将其引用赋值给一个引用变量,不可重排序
- 初次读一个包含final域的对象引用与随后初次读这个final域,不可重排序(在两者之间加LL屏障)【如果一个对象不为null,则引用对象的final域一定已经初始化了】
- 如果final域为一个引用类型,则构造函数内对其的写与构造函数外将其赋值给一个引用变量不可重排序
- 构造函数中不可将this指针溢出。12345678910111213141516171819202122232425262728293031package concurrent.concurrentPractice;/*** Created with IntelliJ IDEA.* User: pingansheng* Date: 2016/5/11* Time: 17:08*/public class ThisEscape {private String name;public ThisEscape() throws Throwable{new Thread(new EscapeRunnable()).start();Thread.sleep(1000);name="123";}private class EscapeRunnable implements Runnable {public void run() {// 通过ThisEscape.this就可以引用外围类对象, 但是此时外围类对象可能还没有构造完成, 即发生了外围类的this引用的逃逸System.out.println(ThisEscape.this.name);//可能会出现令人疑惑的错误 name=null}}public static void main(String[] args) throws Throwable{new ThisEscape();}}
- X86处理器不重排序写写操作与有间接依赖的操作,所以不加任何屏障
3.7 happens-before
- 程序顺序原则:一个线程中的每个操作先于后续的任意操作
- 监视器锁原则:对一个锁的解锁,先于随后对这个锁的加锁
- volatile变量原则:对一个volatile域的写先于后续对其的读
- 传递性:A happens-before B + B happens-before C=A ->C
- start原则:如果A线程执行ThreadB.start(),则其先于B中的任何操作
- join原则:若A执行ThreadB.join()成功返回,则B中任何操作先于Thread.join()的返回
3.8 双重检查与延迟初始化
- 执行类初始化时,JVM会去获取一个锁,每个类或接口都有一个唯一的初始化锁LC,每个使用这个对象的线程必须至少一个获取此锁保证类已经被初始化,可用于延迟初始化
第四章 Java并发编程基础
4.1线程
- 现代操作系统调度最小单元是线程,一个进程中可以创建多个线程,,每个线程拥有各自的计数器,堆栈和局部变量
- main方法就是一个名称为main的线程,一个Java的程序运行不仅仅是main方法的运行,而是main线程和多个其他线程的同时运行
- Java线程的状态变迁,(jps查看进程,jstack xxx查看进程的线程信息)
- Daemon线程是一种支持型线程,一个Java虚拟机中不存在非Deamon线程时候,Java虚拟机将会退出,构建Daemon线程不可以依赖Finally语句块做资源关闭逻辑
4.2启动与终止
- 中断:表示运行中的一个线程是否被其他线程进行了中断操作,Thread.interrupted()进行中断标识复位,如果线程处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧返回false
- 不可以使用suspend、resume等启停线程,其会带着资源的锁进入休眠,容易引发死锁,stop不保证线程的资源正确释放,应使用等待通知机制
- 终止线程应该通过标识变量等方式令程序顺利完成并执行资源清理逻辑(run方法正常结束)
4.3线程间通信
- volatile与synchronized
- 每种方式都是对一个对象的监视器进行获取,获取过程排他,任意一个对象都有监视器
等待/通知
(1)wait notify notifyall都需要对调用对象加锁
(2)wait之后,线程由running变为waiting,并将当前线程放入对象的等待队列
(3)notify()与notifyAll()方法调用后,等待线程依旧不会从wait放回,需要调用notify(),或notifyAll()的线程释放锁之后,等待的线程才会从wait返回
(4)notify方法将等待队列中的一个等待线程从等待队列中移到同步队列,notifyall()方法将等待队列中所有的线程全部移到同步队列,被移动的线程状态由waiting变为blocked
(5)从wait方法返回的前提是获得了调用对象的锁thread.join表示线程A等待Thread线程终止之后才从thread.join返回
1234main(){thread.join()} //main线程等待thread返回后才继续一种利用CountDownLatch的开始开关
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364package concurrent;import java.util.concurrent.CountDownLatch;import java.util.concurrent.TimeUnit;/*** 一种利用CountDownLatch的开始开关* Created with IntelliJ IDEA.* User: pingansheng* Date: 2016/8/24* Time: 10:15*/public class CountDownLatchController {static CountDownLatch start = new CountDownLatch(1);static CountDownLatch end;public static void main(String[] args) throws Exception {int threadCount = 10;end = new CountDownLatch(threadCount);for (int i = 0; i < threadCount; i++) {Thread a = new Thread(new Runner(i));a.start();}TimeUnit.SECONDS.sleep(2);System.out.println("准备");//计数器--之后发出开始指令start.countDown();System.out.println("开始");//等待所有线程计数器完成end.await();System.out.println("结束");}static class Runner implements Runnable {private Integer index;public Runner(Integer index) {this.index = index;}public void setIndex(Integer index) {this.index = index;}public void run() {try {System.out.println("线程" + index + "等待指令");//线程到此等待信号器计数start.await();} catch (Throwable e) {}System.out.println("Run" + index);end.countDown();}}}