一 并发编程基础知识
1.1 概念
并发编程是你编写的代码有可能在多线程环境中执行,
1.2 为什么要用并发编程模型?
- 更加充分的利用多多个处理器的计算能力
- 使得应用程序获得更快的响应时间
- 为程序员提供更好的编程模型
1.3 并发量是越大越好吗?
程开多了不一定好,因为有线程上下文切换的时间开销;有可能多线程程序时间开销更长
- 如何减少线程的上下文切换次数?
- 无锁并发编程;例如currenthashmap一定程度上就是采用这种策略
- CAS算法:(campare and swap)
- 使用最少的线程
- 使用协程(单线程里面维持多个任务的切换)
1.4 Java的内存模型JMM
如上图所示,每一个线程都保存有共享变量的一份备份,并发编程的关键之出就是在于怎么保证每一个现成的本地变量和主存中共享变量的值统一
1.5 顺序一致性和指令重排序
- 指令重排序:java 编译器和cpu再保证单线程中最终语义不变的情况下为了提高程序的执行效率会改变程序指令的执行顺序,
class RecorderExample{
int a = 0;
boolean flag = false;
public void writer() {
a = 1; //1
flag = true //2
}
public void reader() {
if(flag) { //3
int i = a*a; //4
}
}
}
如上程序,单线程环境重步骤1和步骤2完全是可以重排序的,但是在多线程环境下,步骤1和步骤2重排序就有可能会导致错误。
- 顺序一致性
- a) 一个线程中所有的操作必须按照程序的顺序来执行
- b)所有线程都只能看到一个单一的操作执行顺序。
1.6 Happens-before简介
在JMM中如果一个操作的结果需要对另外一个操作可见,那么这两个操作之间必须存在happens-before关系; 这两个操作既可以是在同一个线程中,也可以在不同的线程中。具体原则如下:
- 1 程序顺序规则:一个线程中每个操作happens-before于该线程中任意后续操作
- 2 监视器锁规则:对一个锁的解锁happens-before于随后对这个锁的加锁操作
- 3 volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 4 传递性:如果A happens-before于B,且B happens-before C,那么A happens-before C
- 5 start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()(启动线程B)操作happens-before于线程B中的任意操作。
- 6 join() 规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
二 Java如何实现并发编程
2.1 多线程的实现方式
- 1 继承Thread类创建线程 单继承,多个线程不能共享同一个资源
- 2 实现runnable接口 可以再继承其他类,多个线程可以共享统一资源
- 3 实现Callable接口
2.2 线程的状态
- 新建态 New
- 就绪态 Ready
- 运行态 Running
- 死亡态 Terminated
- 阻塞态 Blocked 等待锁
- 等待态 Wating 需要等待其他线程做特定的操作(唤醒或者中断)
- 超时等待 TimedWaiting
查看某一个进程的状态,先通过jps查看所有的Java进程,然后通过jstack分析查看每一个线程的状态。
2.3 线程之间的通讯
-
1 通过共享内存
- 1.1 volatile和sychrnized关键字
- 由于线程有部分自己的存户空间存储共享变量的copy,所以看到的变量的值不一定是最新的。volatile关键修饰的变量可以告诉线程,访问某一个变量的值必须要去主存进行读取。
- sychronized 修饰的方法或者代码块确保同一时刻只有一个线程执行方法或者块中的代码,保证线程对变量的访问的可见性和排他性
- 代码块:使用monitorenter和monitorexit指令实现加锁
- 方法:依靠方法上面的ACC_SYNCHRONIZED来完成
- 本质上都是对一个对象的监视器(monitor)进行获取这个监视器是排他的一次只能有一个线程获取。
- 1.1 volatile和sychrnized关键字
-
2 通过信号(本质上还是共享内存)
- 2.1 等待通知机制
- 对象的wait()/notify
- 释放对象锁,等待其他线程唤醒或者中断
- 通知一个/所有在对象上等待的线程,让其从wait()方法中返回;前提是拿到对象的锁
- condition.await()/condition.signal()
- 释放lock锁,等待其他线程唤醒或者中断
- 通知一个/所有在lock上等待的线程,让其从await()方法中返回;前提是拿到对象的锁
- 经典范式
- 等待方
- 获取锁对象
- 如果条件不满足,调用对象的wait()方法,被通知后仍然要检查条件
- 条件满足则执行相应的逻辑
- 通知方
- 获得对象的锁
- 改变条件
- 通知所有等待在该对象上的线程
- 等待方
- 对象的wait()/notify
- 2.1 等待通知机制
-
3 join的使用
- thread.join的含义:当前线程A等待thread线程终止之后才从thread.join()返回
三 并发编程中如何保证线程的安全性
3.1 保证线程安全的含义
- 可见性:保证某一个线程对共享资源的更该可以被其他线程看到
- 原子性:保证某一个线程对共享资源的一次更新是不会被中断的
3.2 Java保证线程安全实现方式
-
1 通过锁
- 1.1 sychronized关键
- a. 普通方法,锁的是当前调用该方法的实例
- b. 对于静态同步方法锁的是,当前的class对象
- c. 对于同步代码块锁的是Sychronized括号里面配置的对象
JVM基于进入和退出Monitor对象来实现方法和代码块同步的,两者的实现细节不一样;代码块同步使用的是,monitorenter和monitorexit指令实现,方法同步通过另外一种方式实现,但方法同步也可以通过该方式实现
任何一个对象都有一个monitor与之关联,当一个monitor被持有之后,它将处于锁定状态
JVM锁种类:偏向锁;轻量级锁--->CAS 机制实现;互斥锁 ---> CAS 机制实现 - 1.1 sychronized关键
- 1.2 Lock接口的实现类
- 1.2.1 和sychronized的对象锁区别
- 尝试非阻塞的方式获取锁 trylock()
- 能够被中断的方式获取锁 lockInterruptibly()
- 超时获取锁 tryLock(time)
- 灵活性更高,自己是控制释放
- 1.2.2 队列同步器AbstractQueuedSynchronizer
- AbstractQueuedSynchronizer 用来构建锁和其他同步组件的基础框架
- 锁是面向使用者的,定义了使用者和锁交互的接口,隐藏了实现细节,同步器面向的是锁得实现者,简化了锁的实现方式,屏蔽了同步状态管理,线程的排队,等待和唤醒等底层操作,锁和同步器很好的隔离了使用者和实现者所需要关注的领域。
- 队列同步器的实现
- 通过一个内部队列实现,同步器拥有这个队列的头结点和尾节点,如果获取锁失败则将该线程加入到同步队列中
- 独占式同步状态的获取和释放
- 在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到这个队列中并在队列中进行自旋 移出同步队列(停止自旋)的条件是前驱节点为头结点并且成功获取了同步状态;
- 释放同步状态时,同步器调用tryReleases()方法,然后唤醒头结点的后继节点。
- 1.2.3 Lock接口实现实现类图
- 1.2.1 和sychronized的对象锁区别
- 2 通过volatile+cas操作
-
2.1 volatile
- 可见性,
一个volatile变量的读总能看到任意线程对这个变量的最后一次写入; - 原子性
对任意单个volatile变量读/写具有原子性,但类似于volatile++这种复合操作不具有原子性 - volatile写的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主存中 - volatile读的内存内存语义
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主存中读取共享变量 - 通过插入内存屏障禁止指令重排序达到目的
- 可见性,
-
2.2 CAS操作保证操作的原子性
- 2.2.1 循环的进行CAS操作指导成功为止,
- 2.2.2 CAS 实现原子操作的三大问题
- A:ABA问题:
问题描述:即线程A将共享变量的值从A变到B再从B变到A,线程B认为没有发生变化
解决办法:增加版本号机制,每次更该版本号+1 - B:循环时间长开销大问题
如果JVM支持pause指令来解决
a. 可以延迟流水线执行指令,使得CPU不会消耗过多的资源
b. 它可以避免在退出循环的时候,因内存顺序冲突而引起的CPU流水线被清空,从而提高CPU的执行效率 - C:只能保证一个共享变量的原子操作
多个变量封装成一个对象来解决
- A:ABA问题:
-
四 Java线程池的实现原理
1 为什么要使用线程池?
- 降低资源的消耗
- 提高响应速度
- 提高线程的可管理性
2 组成线程池的关键元素
- maxinumPool 最大线程数
- corePool 核心线程数
- BlockingQueue<Runnable> 阻塞队列用来存储任务的
- Time 线程的空转最大时间
- RejectedExecution : 当线程数量达到最大线程数量,阻塞队列也满了,怎么处理新提交的任务
- 1、直接丢弃(DiscardPolicy)
- 2、丢弃队列中最老的任务(DiscardOldestPolicy)。
- 3、抛异常(AbortPolicy)
- 4、将任务分给调用线程来执行(CallerRunsPolicy)。
3 一个线程具体的提交流程如下:
4 如何关闭线程池
- shutdownNow()
先将线程池设置成stop状态,遍历线程调用每一个线程的Interrupt方法来中断线程 - shutDown()
将线程池设置为SHUTDUWN状态,然后中断所有没有任务执行的线程
5 合理配置线程池
- 任务性质:CPU密集型,IO密集型
- 任务的优先级:高中低
- 任务的执行时间长度:长中短
- 任务的依赖性:是否依赖其他系统资源,
五 Executor框架简介
Java5开始讲线程的任务由原来的即负责工作单元也复制执行机制拆分为,工作单元由:Runnable和Callable负责,而执行机制由Executor负责
5.1 Executor框架简介
Executor框架由三部分组成
- 5.1.1 任务:实现Runnable和Callable接口的任务
- 5.1.2 任务的执行:继承自Executor接口的ExecutorService接口,框架又由两个核心的实现接口(ThreadPoolExecutor和ScheduledThreadPoolExecutor)
-
5.1.2.1 ThreadPoolExecutor
一般使用工厂类Executors来创建,主要有三种类型的ThreadPoolExecutor- FixedThreadPool 创建固定线程数量的线程池
- SingleThreadPoolExecutor 单个线程的线程池,顺序地执行各个任务,
- CacheThreadPool 大小无界的线程池:适用于很多短期异步任务的小程序
-
5.1.2.1 ScheduledThreadPoolExecutor
- ScheduledThreadPoolExecutor 包含若干线程的 ScheduledThreadPoolExecutor;适合多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的数量的应用场景
- SingleThreadScheduledPoolExecutor 单个后台线程执行周期任务
-
-
5.1.3 异步计算的结果:Future和实现Future接口的FutureTask类
5.2 ThreadPoolExecutor详解
关键组成要素前一章节已经阐述过,这里主要阐述3种线程池的区别
- FixedThreadPoolExecutor
阻塞队列LinkedBlockingQueue, 线程池中的线程不会达到corePoolSize - SingleThreadPoolExecutor
LinkedBlockingQueue 无界队列, - CacheThreadPoolExecutor
使用SynchronouusQueue作为线程池的工作队列,该队列不存储元素
5.3 ScheduledThreadPoolExecutor详解
- 用来执行周期性任务
- 使用delayQueue这个无界队列来实现,该队列封装了ProrityQueue会对队列中的元素会进行排序
-
提交的任务为ScheduledFutureTask类型的任务
5.4 FutureTast详解
- 1 FutureTask实现了Runable和Future两个接口,有三种状态:
- 未启动
- 已启动
-
已完成
FutureTask处于未启动或者已启动时调用get方法将会导致调用点成阻塞,FutureTask处于完成状态是则返回对应的结果或者跑出异常
-
2 FutureTask实现原理
FutureTask是基于AbstractQueueSynchronizer同步器实现。
参考文献
《Java并发编程的艺术》