总纲介绍:
1.并发编程会遇到的问题以及解决方案
2.Java并发编程的底层实现原理,CPU和JVM是如何帮助解决的
3.Java内存模型,java线程之间的通信
4.多线程技术带来的好处,多线程的生命周期的基本概念
5.Java并发包和锁相关的API和组件,以及这些API和组件的使用方式和实现细节
6.并发容器的实现原理
7.Java中的原子类操作
8.并发工具类
9.线程池的实现原理和使用建议
10.Executor框架和整体结构和成员组件
11.并发编程的实现
第一章
上下文切换:CPU通过实践片分配算法来循环执行任务,当前任务执行一个时间片之后就会切换到下一个时间片,并且会保存上一个任务的时间片
1.如何减少上下文切换
- 1.1 无锁并发编程.多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,比如讲数据的id按照Hash算法取模分段.不同的线程处理不同段的数据,这样线程 处理数据就变得高效了,ConcurrentHashMap就是使用锁分段技术来进行实现提高并发的效率的
- 1.2 CAS算法.java的Atomic包使用CAS算法来更新数据,而不需要加锁
- 1.4 使用最少线程.避免创建不需要的线程,创建了过多的线程会导致很多的线程处于等待状态,这样就导致了线程的堆积.可以通过配置线程池的中的线程队列长度降低,并且降低最大线程数来减少内存当中的线程
- 1.5 在单线程当中实现多任务的调度
2.死锁
当数据库当中有一个线程拿到了锁,但是在这个线程当中发生了异常,结果并没有释放锁,最终就会导致死锁,避免死锁的解决方案:
- 2.1 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 2.2 尝试使用定时锁,使用lock.tryLock来替代使用内部的锁机制,相对来说lock比sychnirioed更加能够避免死锁的情况
- 2.3 加锁和解锁必须在同一个数据库连接里面,否则会出现解锁失败的情况
3.资源限制的挑战:
- 3.1 资源限制是无论启动多少个线程共同访问一个资源,资源总量不变
- 3.2 资源限制引发多线程并发执行,在并发执行过程中,多了上下文切换和资源调度的问题,反而使得总体的效率变得更慢了
4.解决方案
资源限制的情况下,主要的影响因素是带宽和硬盘读写速度.合理的配置带宽和对应的线程数量,线程的数量要吻合数据库的连接数,如果线程的数量比数据库连接数大的过多,就会导致某些线程会被阻塞
第二章 java并发机制的底层实现原理
Java代码子啊编译后会变成java字节码,字节码文件被类加载器加载到JVM当中,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,java中所使用的并发机制依赖于JVM的实现和CPU的指令
1.volatile的应用
- 1.1 volatile是轻量级的synchronized,它相对执行难成本更低,不会引起线程的上下文切换和调度问题.作用是让一个线程在修改了一个共享变两个时,另外一个线程能读到这个修改的值
- 1.2 简而言之:volatile是为了确保共享变量能被准确和一致的更新而出现的,相对来说会比加锁更方便
2.Volatile的使用优化
从jdk7开始出现了Linked-TransferQueue,使用这个队列集合类来进行优化队列的出队和入队的性能,本质上是因为目前很多电脑系统处理器高速缓存行是64字节宽,这个类把使用volatile修饰的变量补足到64字节,就可以在CPU运行当中在高速缓存行当中执行,从而使得操作变快,但是P6和奔腾处理器他们的高速缓存行就是32字节宽的,所以此时就不是补足到64字节,而是补足到32字节就可以了.
3.Synchronized的实现原理与应用
- 3.1 Jdk’1.6之前是重量级锁,jdk1.6优化之后就偏向于轻量级锁
- 3.2 3种表现形式
- 对于普通方法,锁的是当前实例对象
- 静态同步方法,锁的是当前类的Class对象
- 对于同步方法快,锁的是花括号当中配置的对象
4.Synchronized的锁存放在java对象头当中
5.锁的升级和对比(4种状态)
- 5.1 无锁
- 5.2 偏向锁
- 5.3 轻量级锁
- 5.4 重量级锁(锁只能升级不能降级)
6.处理器如何保证原子性
- 6.1 总线锁
- 6.2 缓存锁
- 6.3 Java中的CASCAS的基本思路就是循环执行该线程,保证该线程一定能够正常执行,并且不会被打断
- CAS实现原子操作存在一定问题A->B->A问题,使用CAS去校验共享变量是否发生了变化,如果该变量从A->B->A,此时CAS就会检测没有变化,解决办法就是使用乐观锁,添加版本号的形式来避开解决的问题,所以最终的结果就会变成:1A->2B->3A
- 循环时间长,而且开销非常大.自选CAS如果长时间不成功,会给CPU带来非常大的执行开销.
- 只能保证 一个共享变量的原子操作
- 6.4 使用锁机制实现原子操作
第三章 java内存模型的基础
1.并发编程模型的两个关键问题
- 1.1 线程之间如何通信,线程之间通信机制:共享内存和消息传递
- 1.2 线程之间如何同步
java的并发采用的是共享内存模型
2.java内存模型的抽象结构
- 2.1 所有实例域,静态域,数组元素都存储在堆内存当中,这三块区域都是堆内存在线程之间的共享
- 2.2 Java线程之间的通信由内存模型JMM控制,JMM决定了一个线程堆共享变量的写入何时堆另一个线程可见.
线程之间的 共享变量储存在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)
JMM是一个抽象的概念,它包含了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化
3.线程之间通信的步骤
- 3.1 线程A把本地内存A中更新过的共享变量刷新到主内存当中去
- 3.2 线程B到主内存中去读取线程之前已经更新过的共享变量
4.从源代码到指令序列的重排序
java在执行程序的时候,为了提高性能,编译器和处理器常常会对指令做重新排序,充排序分成了3中类型
- 4.1 编译器优化的重排序.编译器在不改变单线程程序语义的前提下可以重新安排语句执行顺序
- 4.2 指令级并行的重排序.
- 4.3 内存系统的重排序.
总结:java的JMM会要求java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器排序.JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理平台上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性的保证
5.并发编程模型的分类
并发编程在做写操作的时候都是操作本地内存的缓冲区的缓存数据,每次缓存区域有了写操作之后就会把内容更新到本地内存当中,当读取的数据的时候就是直接从本地内存当中进行读取
从上面的推论我们可以得出,我们在高并发的时候会有很多的线程,可能有读取的线程和写入的线程,那么为了保证数据的完整性,JMM的优化就是发送指令给对应的处理器,让这些处理器对对应的线程进行重新排序,从而起到了优化数据的结果
6.hapens-before简介
从JDK5开始,Java使用新的JSR内存模型,JMM中规定如果一个操作执行的结果需要对另一个操作可见,那么两个操作之间必须要存在happens-beffore关系
- 6.1 happens-before规则
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
- 监视器锁规则
- volatile变量规则
- 传递性
happens-before本质上有点类似于保证程序的顺序执行
7.重排序
对于重排序而言,都必须遵循as-if-serial,意思是 不管怎么进行重排序,程序的执行结果不能被改变,但是要记住,重排序是针对于多线程并发,有多个线程的时候才存在重排序,对于单线程执行的时候,就不存在对单个线程进行重排序的问题
8.顺序一致性内存模型
- 8.1 一个线程中所有操作必须按照程序的顺序来执行
- 8.2 所有线程都只能看到一个单一的操作执行顺序,并且每个操作都必须要保持原子性
总结:JMM在不改变程序执行结果的前提下,尽可能的为比编译器和处理器的优化打开方便之门
9.未同步程序执行的特性
对于未同步或者未正确同步的多线程程序,JMM只是提供最小安全性,线程执行时读取到的值,要么是之前某个线程写入的值,幺妹是默认值(0,false,null),JMM能保证的是线程读取到的值不会无中生有
处理器处理内存的时候是使用总线调度机制,其实也没有真正的调度,本质上采用的还是抢占式调度
10.JSR-133的内存模型
在JDK5之前,内存模型会吧long/double的读和写都拆分成为两个32位的数来进行操作,在JSK5之后只允许写的时候把他们进行拆分成为两个32位,但是读取的时候就是按照64位进行读取的了
11.volatile的特性
- 11.1 可见性.对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入
- 11.2 原子性:对任意单个volatile的读/写具有原子性,但是volatile++这种情况下符合操作不具有原子性
- 11.3 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存中,简而言之就是一个轻量级的锁,保证数据的一致性和操作的原子性
- 11.4 线程A写一个volatile变两个,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息
- 11.5 JMM在遇到volatile修饰的变量时不能重排序的情况
- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序
- 当第一个操作是volatile读时,不管第二个操作是什么,都不能重新排序
- 当第一个操作是volatile写,第二个操作是volatile读时
- 11.6 为了实现volatile不能重排序的功能,编译器在生成字节码的时候,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序
- 11.7 volatile的内存屏障
- volatile写前插入StoreStore屏障
- volatile写后插入StoreLoad屏障
- volatile读前插入LoadLoad屏障
- volatile读后插入语LoadStore屏障
备注:在jdk1.5之前JMM是允许对volatile修饰的变量进行重排序的
12.锁的私房和获取的内存语义
锁的意义主要是锁住线程正在操作的本地内存区域,当锁释放的时候就把本地内存的变量刷新到主内存当中去,然后再发送消息给下一个要获取锁的线程发出了线程A已经对共享变量所做修改的消息
总结
- 1 线程A释放了一个锁,实质上是线程A向接下来要获取这个锁的某个线程发出消息
- 2 线程B获取一个锁,实质上是线程B接受了之前某个线程发出的消息
- 3 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息
备注:实际上使用volatile修饰的变量也是以这样的形式进行操作的
13.ReentranLock锁机制
- 14.1 在ReentranLock中,调用lock()方法获取锁,调用unlock()方法释放锁
- 14.2 ReentranLock的实现以来于java同步器框架AbstractQueuedSynchronizer(即是AOS),底层使用的是volatile来进行维持同步状态
- 14.3 ReentranLock分为了公平锁和非公平锁
- 14.4 在这个锁机制当中调用了本地方法使用C++的代码,所以这部分的内容其实就是在openjdk\hotspot\src当中,做了一些对于处理器的处理,在这些代码当中针对于不同的操作系统做了一些不同的操作,
在jdk当中内置了对于当前操作系统的一些判断,如果是单处理器,此时不存在并发操作,所以它在程序执行的时候就不会加锁,如果是多处理器,那就会对程序进行加锁
14.处理器当中对于Lock的说明
- 14.1 在不同的处理器当中对于锁Lock的处理方式也略有不同,例如在Pentium4以前对于锁的处理就是锁住了调度的总线,也就意味着只要有一个线程执行了,那么其它线程都无法进入总线当中进行调度
- 14.2 把写缓冲区中的所有数据都刷新到内存当中
总结公平锁和非公平锁
- 1.公平锁和非公平锁释放时,最后都需要写一个volatile变量state
- 2.公平锁获取时,首先会去读volatile变量
- 3.非公平锁获取时,首先会用CAS更新volatile变量
15.Concurrent包的实现
15.1 Java线程之间的通信的4种方式
- a) A线程写了volatile变量,随后B线程读这个volatile变量(备注:本质上就是线程A读取了本地内存当中的共享变量的副本,在释放锁的时候将本地内存当中修改的内容刷新到共享内存当中,并且通知下一个即将获得锁的线程B,从而形成了线程之间的通信)
- b) A线程写volatile变量,随后B线程使用CAS更新这个volatile变量(备注:CAS的本质就类似于用于保证程序的顺序执行,CAS的本质类似于锁)
- c) A线程使用CAS更新一个volatile变量,随后线程B用CAS更新这个volatile变量
- d) A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量
关于锁,volatile,CAS算法的总结:
- 1.锁机制存在的问题
- a) 多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时问题
- b) 一个线程持有锁会导致其它所有需要此锁的线程挂起直至该锁释放
- c) 如果一个优先级高的线程等待一个优先级低的线程释放锁,会出现优先级反转的问题,在JMM中会对线程进行重排序,将写线程优先于读线程,对于那些写线程和读线程进行了一定的标记
- d) 乐观锁和悲观锁:独占锁是一种悲观锁,synchronized就是一种独占锁,会导致所有需要锁的线程挂起
- 2.Volatile
相比于锁来说,volatile变量是一种轻量级的同步机制,在使用volatile的时候不会发生上下文的切换和调度问题
CAS内部以原子操作为基础,采用事务提交提交失败重试这样的特性,就是在多线程并发的时候采用抢占式的,如果线程A正常执行了,那么其它线程就表示失败的状态,但是其它线程会继续进行下一次的执行,直到所有的线程都执行完毕了,CAS就会停止
- 4.AQS,非阻塞数据结构和原子变量类,concurrent包中的基础类都是使用这种模式来实现的,
16.Final作用域的重排序规则
- 16.1 在构造函数内对一个final作用域写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
- 16.2 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序
17.写final域的重排序规则
- 17.1 JMM禁止编译器把final域的写重排序到构造函数之外
- 17.2 编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障
18.Final语义在处理器当中的实现
X86处理器中,final域的读/写不会插入任何内存屏障
19.JSR-133为什么要增强final语义
在以前旧的Java内存模型当中,final的初始化跟构造器同步,此时final修饰的变量是对应的默认值,直到线程执行的时候把这个final赋值,所以这个时候对于final赋值的变量而言就是会改变的
20.Happens-before
Happens-before是JMM最核心的概念,JMM把happens-before要求禁止的重排序分为了两类
- 20.1 会改变程序执行结果的重排序,JMM会要求编译器和处理器禁止这种操作
- 20.2 不会改变程序执行结果的重排序,JMM会要求编译器和处理器允许这种操作
简单总结JMM,JMM会对线程程序的执行进行重排序,但是如果会影响结果就会让编译器和处理器禁止这种重排序,如果不影响结果,就允许编译器和处理器执行这种重排序
对于happens-before的规则来说是要求程序必须要顺序执行,禁止重排序,JMM的设计思路也是跟这个类似相同,但是JMM对于不影响程序执行结果的重排序就是属于允许这种重排序的操作
21.Happens-before规则
在JSR-133中定义了如下happens-before的规则
- 21.1 程序顺序规则
- 21.2 监视器锁规则
- 21.3 Volatile变量规则
- 21.4 传递性
- 21.5 Start()规则
- 21.6 Join()规则
22.双重检查锁定的由来
在Java程序当中,有时候需要推迟一些高开销的对象初始化,并且只有在使用到的时候才去创建它,例如单例设计模式加上延迟初始化,例如下代码
public class UnsafeLazyIntialization{
private static UnsafeLazyIntialization instance;
public static UnsafeLazyIntialization getInstance(){
if(instance == null){ //第一次检查
synchronized(UnsafeLazyIntialization.class){ //第二次检查
if(instance == null){
instance = new UnsafeLazyIntialization();
}
}
}
return instance;
}
}
总结:以上就是双重锁定检查,相对来说降低了很多对于synchronized的开销
Java内存模型的总结;
1.处理器内存模型:
JMM和处理器内存模型会对顺序一致性做一些放松,如果完全按照顺序一致性模型来实现处理器和JMM,那么很多处理器和编译器优化都要被禁止
2.JSR-133对旧内存模型的修补
- 2.1 增强了volatile的内存语义.旧内存模型允许volatile变量和普通变量重排序.JSR-133严格限制volatile变量和普通变量的重排序,但是都是使用volatile变量修饰的
- 2.2 增强了final的内存语义.在旧内存模型中,多次读取同一个final变量的值可能会不相同,因此JSR-133位final增加了两个重排序规则规则,保证final引用不会从构造函数内溢出的情况下,final具有了初始化安全性