并发编程
2020-04-21 14:56:27 3 举报
AI智能生成
登录查看完整内容
Java并发编程思维导图
作者其他创作
大纲/内容
并发编程
基础知识
理解CPU核心底层结构
理解CPU时间片轮转机制
理解线程与进程的区别
理解并行与并发的区别
Java多线程
创建线程
继承Thread类
实现Runnable接口
实现Callable接口
关闭线程
interrupt()方法
isInterrupted()方法
静态interrupted()方法
线程状态
新建(Start)
就绪(Ready)
运行(Running)
阻塞(Blocked)
死亡(Dead)
线程优先级
Thread.setPriority(priority)
守护线程
Thread.setDeamon(true)
线程间共享
Synchronized内置锁
对象锁
类锁
Volatile关键字
保证资源在修改时,线程间可见
可见性
防止指令优化时的重排序
有序性
ThreadLocal线程变量
线程间协作
wait()/notify()/notifyAll()
等待和通知的标准范式
等待方
获取对象的锁
循环里判断条件是否满足
条件不满足调用wait()方法
条件满足执行业务逻辑
通知方
改变条件
通知所有等待在对象上的线程
等待超时模式
join()方法
线程A执行了线程B的join()方法,则线程A必须等待线程B执行完成之后,线程A才能执行
yield()/sleep()/wait()/notify()等方法对锁的影响
线程在执行yield()方法以后,持有的锁是不释放的
sleep()方法被调用以后,持有的锁是不释放的
调用wait()方法之前,必须要持有锁,调用wait()方法之后,锁就会被释放当wait()方法返回当时候,线程会重新持有锁
调用notify()方法之前,必须要持有锁,调用notify()方法,本身不会释放锁
JUC工具类
Fork Join
定义:Fork/Join框架,就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行Join汇总
标准范式
pool = new ForkJoinPool();
MyTask myTask = new ForkJoinTask();
pool.invoke(myTask); //同步pool.execute(myTask); //异步
Result result = myTask.join(); //阻塞
CountDownLatch
定义:一个线程等待其他线程工作完成以后再执行,是一个加强版的join()
await() 用来等待
countDown() 负责计数器的减一
CyclicBarrier
让一组线程达到某个屏障,被阻塞,一直到组内最后一个线程达到屏障时,屏障开放,所有被阻塞的线程会继续运行
Semaphore
控制同时访问某个特定资源的线程数量,用在流量控制
Exchange
两个线程间的数据交换
阻塞方法
Callable/Future/FutureTask
Future.isDone()
无论是正常结束、异常结束,还是手动取消,都会返回true
Future.isCancelled()
任务完成前被取消,则返回true
Future.cancel(boolean mayInterruptIfRunning)
任务还没开始,返回false
任务已经启动,调用cancel(true),则会尝试中断正在运行的任务,中断成功,返回true;调用cancel(false),则不会中断正在运行的任务
任务已经结束,返回false
原子操作CAS
原子操作
Synchronized是基于阻塞的锁的机制,其局限性主要存在于:1、被阻塞的线程优先级很高;2、拿到锁的线程一直不释放锁;3、大量的竞争,消耗CPU,同时带来死锁或其他安全问题
CAS
原理:在指令级别保证这是一个原子操作
三个运算符:内存地址V,一个期望的值A,一个新值B
基本思路:如果地址V上的值和期望的值A相等,就给地址V赋一个新值B;否则,不做任何操作,并不断在循环(自旋)里面进行CAS操作
CAS的问题
ABA问题
解决方法:在资源上加一个版本号
开销问题
只能保证一个共享变量的原子操作
JDK原子类
更新基本类型类
AtomicBoolean/AtomicInteger/AtomicLong
更新数组类
AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray
更新引用类
AtomicReference/AtomicMarkableReference/AtomicStampedReference
AtomicMarkableReference的版本是一个Boolean类型,关注资源是否有被修改过
AtomicStampReference的版本是一个int类型的版本戳,关注资源被修改过多少次
原子更新字段类
AtomicReferenceFieldUpdater/AtomicIntegerFieldUpdater/AtomicLongFieldUpdater
显示锁和AQS
Lock接口与Synchronized关键字比较
Synchronized关键字代码简洁
如果有获取锁可以被中断,超时获取锁,尝试获取锁这三种需求,推荐适用Lock接口
尽量适用Synchronized关键字
公平锁和非公平锁
如果在时间上,先对锁获取的请求,一定先被满足,这个锁就是公平的,如果不满足,这个锁就是非公平的
非公平锁的效率一般来讲更高
ReentrantLock缺省构造函数时是非公平锁
排他锁和读写锁
ReentrantLock和Synchronized关键字都属于排他锁
读写锁:同一时刻允许多个读线程同时访问,但是写线程同时访问的时候,所有的读和写都被阻塞,最适合读多写少的情况
Conditon接口
LockSupport工具
作用
阻塞一个线程
唤醒一个线程
构建同步组建的基础工具
AQS(AbstractQueueSynchronizer)
AQS适用模版方法设计模式
AQS中的模版方法
acquire
acquireInterruptibly
tryAcquireNanos
tryAcquireShared
acquireSharedInterruptibly
tryAcquireSharedNanos
release
releaseShared
AQS中需要子类覆盖的流程方法
tryAcquire
tryRelease
tryAcquireRelease
isHeldExclusively
AQS中的同步状态
getState
setState
compareAndSetState
并发容器
ConcurrentHashMap
HashMap的put操作会引起死循环,HashMap里的Entry数据结构产生一个环形的数据结构
get()方法
定位Segment:key的hash进行再散列值的高位取模
定位Table:key的hash再散列值取模
依次扫描链表,要么返回元素,要么返回null
put()方法
Segment不扩容,扩容的是Segment下面的Table数组
ConcurrentHashMap初始化的时候只初始化第一个Segment的Table,在每次put()的时候都需要ensureSegment()来检查该Segment是否被初始化Table数组
size()方法
size()方法进行两次不加锁的统计,两次统计结果一致,则直接返回结果,如果统计结果不一致,则重新加锁再次统计,尽量避免适用size()方法
ConcurrentHashMap的弱一致性
ConcurrentHashMap的get()方法是没有加锁的,所以在取的时候,有可能有线程改变了原来的数据
与JDK1.7相比
取消了Segment数组,直接用table保存数据,锁的粒度更小,减小并发冲突的概率
保存数据采用数组+红黑树的形式,性能提升很大,链表转红黑树:某一链超过8个
主要数据结构和关键变量
Node类存放实际Key和Value值
sizeCtl
为负数表示进行初始化或扩容
-1表示正在初始化
-N表示有N-1个线程正在进行扩容
0表示table还没有被初始化
正数表示初始化或下一次进行扩容对阈值
TreeNode用在红黑树下面,TreeBin是实际放在Table数组中的,代表这个红黑树的根
初始化主要做了什么事
只是给成员变量赋了初始值,在put()时进行实际数组的填充
spread()方法对哈希进行再散列,使散列的更均匀
helpTransfer()方法帮助扩容
treefiyBin()方法由链表转为树
扩容操作
transfer()方法执行实际的扩容操作
采用翻倍形式,减少移动和重排的次数
untreefiy()非树化,由树转为链表,数量小于6个
helpTransfer()并发扩容机制,在数据插入的时候,如果发现正在扩容,则当前线程会先帮助进行扩容,再进行数据插入
估计的大致数量,不是精确数量
一致性
弱一致性
ConcurrentSkipListMap
TreeMap的并发版本
SkipList 跳表
设计思想:以空间换时间
概率数据结构
目的:链表结构提升访问速度
ConcurrentSkipListSet
TreeSet的并发版本
CurrentLinkedQueue
LinkedList的并发版本
无界非阻塞队列,底层是个链表,遵循先进先出原则
add()、offer()将元素插入到尾部
peek()拿头部的数据,但是不移除数据,poll()拿头部的数据,但是移除数据
写时复制容器 - CopyOnWriteArrayList
只能保证最终一致性,无法保证实时一致性
适用于读多写少的并发应用场景,例如白名单、黑名单
缺点:内存占用
写时复制容器 - CopyOnWriteArraySet
阻塞队列
概念:当队列满的时候,插入元素的线程被阻塞,直到队列不满;当队列为空的时候,获取元素的线程被阻塞,直到队列不空
生产者消费者模式
解决生产者和消费者之间能力不匹配的问题
加一个容器解决生产者和消费者之间的耦合
常用方法
插入数据
add()
抛出异常
offer()
返回true or false
put()
一直阻塞
offer(time)
超时退出
移除数据
remove()
poll()
take()
poll(time)
检查数据
element()
peek()
BlockingQueue接口
常用阻塞队列
ArrayBlockingQueue
一个由数组结构组成的有界阻塞队列
按照先进先出原则,要求设定初始大小
只有一个锁
直接插入元素
LinkedBlockingQueue
一个由链表结构组成的有界阻塞队列
要求设定初始大小,可以不设定初始大小,默认Integer.MAX_VALUE
用了两个锁
需要进行一次转换
PriorityBlocingQueue
一个支持优先级排序的无界阻塞队列
默认情况下,按照自然顺序,否则要没实现compareTo()方法,要么指定构造参数Comparator
不保证同优先级的元素的顺序
DelayQueue
一个使用优先级队列实现的无界阻塞队列
支持延时获取元素的阻塞队列
元素必须要实现Delayed接口
应用场景:缓存系统、订单到期、限时支付
SynchronousQueue
一个不存储元素的阻塞队列
每一个put操作都要等待一个take操作
LinkedTransferQueue
一个由链表结构组成的无界阻塞队列
transfer()
生产者在插入元素之前,先看看有没有消费者在等待,如果有消费者等待,则生产者直接把元素交给消费者,如果没有消费者等待,才把元素写入队列
tryTransfer()
与transfer()的不同:无论消费者是否接收,方法都立即返回,transfer()必须要消费者消费以后才会返回,哪怕元素写入了消费队列
LinkedBlockingDeque
一个由链表结构组成的双向阻塞队列
Deque代表双向
可以从队列的头和尾都可以插入和移除元素
可以在高并发环境下实现工作密取
方法名带了First从头取取,带了Last的从尾去取,add=addLast,remove=removeFirst,take=takeFirst
线程池
为什么要用线程池
降低资源的消耗 - 降低线程创建和销毁的资源消耗
提高响应速度
提高线程的可管理性
自己实现一个线程池
条件
线程必须在池中已经创建好了,并且可以保持住,要有容器保存多个线程
线程还要能够接受外部的任务,运行这个任务,要有容器来保持这个来不及运行的任务
JDK线程池和工作机制
线程池的创建
ThreadPoolExecutor JDK中所有线程池实现的父类
int corePoolSize
线程池中核心线程数,如果当前任务数 < corePoolSize,就会创建新线程;如果当前任务数 = corePoolSize,这个任务就保存在BlockingQueue中
如果调用prestartAllCoreThreads()方法会一次性启动corePoolSize数量的线程
int maximumPoolSize
线程池中允许的最大线程数,如果BlockingQueue满了,并且当前线程数 <maximunPoolSize的时候,就会再次创建新的线程
long keepAliveTime
线程空闲下来后,存活的时间,这个参数只在当前线程数 > corePoolSize的时候才有用
TimeUnit unit
存活时间的单位值
BlockingQueue<Runnable> workQueue
保存任务的阻塞队列
ThreadFactory threadFactory
创建线程的工厂,给新建的工厂赋名字
RejectedExecutionHandler handler
饱和策略
AbortPolicy 直接抛出异常 默认
CallerRunsPolicy 用调用者所在的线程执行任务
DiscardOldestPolicy 丢弃阻塞队列里面最靠前的任务
DiscardPolicy 当前任务直接丢弃
实现自己的饱和策略
提交任务
execute()
无返回值
submit()
有返回值
关闭线程池
shutdown()
设置线程池的状态,只会中断所有没有执行任务的线程
shutdownNow()
设置线程池状态,还会尝试停止运行或暂停任务的线程
合理配置线程池
根据任务的性质
计算(CPU)密集型
加密、大数分解、正则
线程数尽量小一点
最大推荐:计算机CPU核心数+1
防止页缺失
拿到计算机核心数:Runtime.getRuntime().availableProcessors()
I/O密集型
读取文件、数据库连接、网络通信
线程数适当大一点
推荐计算机CPU核心数*2
混合型
尽量拆分
队列的选择上,应该选择有界;无界队列可能造成OOM,有可能造成业务垮掉
JDK预定义线程池
FixedThreadPool
创建固定线程数量
适用于负载较重的服务器
使用无界队列,使用需注意
SigleThreadExecutor
创建单个线程
适用于需要保证顺序执行任务
不会有多个线程活动
CachedThreadPool
会根据需要创建新线程
使用于执行很多短期异步任务的程序
使用SynchronousQueue
WorkStealingPool (JDK1.7以后)
基于ForkJoinPool实现
ScheduledThreadPoolExecutor
需要定期执行周期任务
TImer不建议使用了
创建方法
new SingleThreadScheduledExecutor
只包含一个线程
适用于只需要单个线程去执行周期性任务,同时保证顺序执行各个任务
new ScheduledThreadPool
可以包含多个线程
方法说明
schedule
只执行一次,任务还可以延时执行
scheduleAtFixedRate
提交固定时间间隔的任务
两个相邻任务头部的时间间隔是一致的
任务超时,下一个任务马上开始执行:比如说每隔60s执行一次,有任务执行了80s,那么上个任务结束后,下个任务立马执行
scheduleWithFixedDelay
提交固定延时间隔执行的任务
前一个任务的尾部和后一个任务的头部的时间间隔是一致的
建议在提交给ScheduledThreadPoolExecutor的任务要捕捉异常,保证任务在下个周期能正常执行
Executor框架
CompletionService
并发安全
类的线程安全定义
如果多线程下使用这个类,不管多线程如何使用或调度这个类,这个类总是表现出正确的行为,可以说这个类是线程安全的
操作的原子性
内存的可见性
当一个类在多个线程之间共享状态的时候,就会出现线程不安全
实现类的线程安全
栈封闭
所有的变量都是在方法内部生命的,可以说这些变量都处于栈封闭状态
无状态
没有任何成员变量的类,就叫无状态的类
让类不可变
让状态不可变
1、加final关键字:对于一个类来讲,所有的成员变量应该是私有的,同样的,只要有可能,所有的成员变量应该加上final关键字
2、根本就不提供任何可供修改成员变量的地方,同时,成员变量也不作为方法的返回值
Akka框架
volatile关键字
保证类的可见性
保证类的有序性
最适合一个线程写,多个线程读的情景
加锁和CAS
安全的发布
ThreadLocal
引申:Servlet
不是一个线程安全的类
在需求上,很少有共享的需求
web容器接收到请求和接收应答的时候,都是由一个线程来负责的
线程不安全引发的问题
死锁
是指两个或两个以上的进程在执行的过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象,若无外力作用,它们都将无法推进下去,此时称系统处于死锁状态,或系统产生了死锁
竞争资源一定是多于一个,同时小于等于竞争的线程数,资源只有一个只会产生激烈的竞争,但不会产生死锁
怀疑发生死锁
通过jps查询应用的id
再通过jstack id查看应用的锁的持有情况
发生静态死锁的实质
获取锁的顺序不一致
活锁
线程饥饿
低优先级的线程,总是拿不到执行时间
加随机数,错开获得锁的时间
性能和思考
使用并发的目标是为了提高性能,引入多线程,会引入额外的开销
衡量应用程序性能
服务时间
多快
延迟时间
吞吐量
多少,处理能力的指标,完成工作的多少
可伸缩性
多少
多快和多少是相互独立的,有时又是相互矛盾的
对服务器应用来说,多少比多快更受重视
做应用的时候需要遵循的原则
先保证程序正确,确实达不到要求的时候,再提高速度
黄金原则
一定要以测试为基准
一个应用程序里,串行的部分是永远都有的
Amdahl定律:1/(F+(1-N)/N) F:必须被串行的部分,当N趋于无穷大时,最好的效果是1/F
影响性能的因素
上下文切换
5000~10000个时间周期 几微秒
内存同步
内存屏障
阻塞
挂起,包括两次额外的上下文切换
性能提升-减少锁的竞争
缩小锁的范围
对锁的持有,快进快出,尽量缩短持有锁的时间
避免多余的缩减锁的粒度
减小锁的粒度
使用锁的时候,锁所保护的对象是多个,多个对象之间是独立变化的时候,不如用多个锁来一一保护这些对象,注意避免发生死锁
锁分段
替换独占锁
使用读写锁
使用CAS操作
使用系统提供的并发容器
线程安全的单例模式
DCL
解决之道
懒汉式
类初始化模式,也叫延迟站位模式
饿汉式
在JVM中,对类的加载和初始化,由虚拟机保证线程安全
枚举
JMM和底层实现原理
0 条评论
回复 删除
下一页