java并发脑图
2020-11-10 13:52:57 1 举报
AI智能生成
java并发知识的脑图
作者其他创作
大纲/内容
JUC
常用类
CountDownLatch
作用类似于Thread.join(),可以使线程阻塞等待
计数器来实现,创建CountDownLatch需要指定数量,await方法会阻塞,知道数量为0,countDown每次减1
API
CountDownLatch(int count)
构造函数,count为计数器初始值
await()
调用await()方法的线程会被挂起,它会等待知道count值为0才继续执行
和await()类似,只不过等待一定时间后count值还没变为0的话就会继续执行
countDown()
将count值减1
CyclicBarrier
相对于CountDownLatch,计数器有重置功能,计数器为0可以出发回调函数(先出发回调函数,再执行await代码后面的逻辑)
CyclicBarrier(int paries)
无回调的构造函数
有回调的构造函数
线程阻塞,待计数器为0时执行后面代码
Semaphore
指定多少个线程可以进入临界区,可以实现限流器
Semaphore(int permits)
默认是非公平信号量,非公平信号量即获得锁的顺序与线程启动的顺序无关
公平信号量是获得锁的顺序与线程启动的顺序有关,但不代表100%地获得信号量,仅仅是在概率上得到保证。
acquire()
计数器的值减1,如果此时计数器的值小于等于0,则当前线程被阻塞,否则线程可以继续执行
release()
计数器的值加1,如果此时计数器的值小于或者等于0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。
并发队列
阻塞队列
ArrayBlockingQueue
基于数组的并发阻塞队列
有界队列
内部实现是数组
先进先出(FIFO)
LinkedBlockingQueue
基于链表的FIFO阻塞队列
无界队列,可指定队列长度
内部实现是链表
LinkedBlockingDeque
基于链表的FIFO双端阻塞队列
可以从队列两端插入或移除元素
SynchronousQueue
并发同步阻塞队列
队列内部只允许容纳一个元素,当一个线程出入一个元素后会被阻塞,除非这个元素被另一个线程消费。
DelayQueue
延期阻塞队列
无界队列
加入对垒的元素必须实现Delayed接口
添加元素会触发Delayed接口的compareTo方法按照到期时间排序,排在头部的是最早到期的
PriorBlockingQueue
带优先级的无界阻塞队列
默认情况下元素采用自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则
其他
ArrayDeque
数组双端队列
容量无限制
线程不安全
可以作为栈来使用,效率高于Stack
也可以作为队列来使用,效率高于LinkedList
PriorityQueue
优先级队列
ConcurrentLinkedQueue
基于链表的并发队列
先进先出
java内存模型(JMM)
共享变量存储于主内存中,每个线程都可以访问
每个线程都私有的工作内存或者称为本地内存
工作内存只存储该线程对共享变量的副本
线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存
工作内存和java内存模型一样也是一个抽象的概念,它其实不存在,它涵盖了缓存、寄存器、编译器优化以及硬件等
Happens-before原则
程序次序规则
锁定规则
volatile变量规则
传递规则
线程启动规则
线程中断规则
线程的终结规则
对象的终结规则
Volatile关键字
语义
保证不同线程对共享变量操作的可见性
禁止对指令进行重排序
不保证原子性
原理
volatile关键字修饰的变量存在一个“lock”前缀,lock相当于是一个内存屏障
确保指令重排序时不会将其后面的代码排到内存屏障之前
确保指令重排序不会将其前面的代码排到内存屏障之后
确保在执行到内存屏障修饰的指令时前面的代码全部执行完成
强制将线程工作内存中的值刷新到主内存中
如果是写操作,则会导致其他线程工作内存中的缓存数据失效
volatile和synchronized区别
使用上的区别
volatile关键字只能用于修饰实例变量或者类变量,不能用于修饰方法以及方法参数和局部变量常量等
synchronized关键字不能修饰变量,只能用于修饰方法或者代码块
volatile修饰的变量可以为null,synchronized同步语句块的monitor对象不能为null
对原子性的保证
volatile无法保证原子性
synchronized是一种排他机制,被synchronized修饰的同步代码是无法被中途打断的,可以保证代码的原子性
可见性保证
两者均可保证共享资源在多线程间的可见性
对有序性的保证
volatile关键字禁止JVM编译器记忆处理器对其重排序,所以能够保证有序性
synchronized是通过串行化换来的,synchronized修饰的代码块中可能会出现重排序情况,最终输出结果和代码编写顺序一致
volatile不会使线程陷入阻塞
synchronized会使线程陷入阻塞
wait和sleep的区别
wait和sleep都可以使线程进入阻塞状态
wait和sleep方法均是可中断方法,被中断后都会收到中断异常
wait是Object的方法,sleep是Thread的方法
wait需要再同步方法中执行,sleep不需要
线程在同步方法中执行sleep方法,并不会释放monitor锁,而wait方法会释放monitor锁
sleep方法短暂休眠后会主动退出阻塞,而wait方法(没有指定wait时间)则需要被其他线程中断后才能退出阻塞
ThreadLocl
常用API
set
get
remove
ThreadLocalMap中key是当前ThreadLocal实例,value是设置的值
key是弱引用,GC前会被回收掉,使用不当可能会出现内存泄露情况,在使用结束的时候最好能调用remove方法
Hook线程
Hook线程只有在收到退出信号的时候会被执行,如果在kill的时候使用了参数-9,那么Hook线程是不会得到执行,进程将立即退出,因此lock文件将得不到清理
Hook线程中也可以执行一些释放动作,比如关闭文件句柄、socket链接、数据库connection等。
尽量不要再Hook线程中执行一些耗时非常大厂的操作,会导致程序迟迟不能退出。
java并发脑图
多线程简介
创建线程的三种方式
1. 继承Thread
2.实现Runnable
3.使用Callable和Future创建线程,有返回值
线程的生命周期
New:NewThread()创建一个Thread对象,线程已经被创建,但是还不允许分配CPU执行。操作系统层面线程还未真正创建
Runnable:thread.start()此时操作系统层面线程创建,等待CPU调度
Running:CPU分配资源,线程执行
BLOCKED:线程阻塞
sleep:不会释放资源
wait:释放锁资源
Terminated:线程结束
线程正常运行结束
线程运行出错意外结束
JVM Crash导致所有的线程都结束
Thread的api
interrupt 打断线程阻塞状态(wait、sleep、join、interruptibleChannel的io操作、Select偶然的wakeUp方法都会使线程进入阻塞状态)
sleep 线程休眠指定毫秒数、不会放弃锁的所有权
yield 提醒调度器放弃当前CPU资源,如果CPU资源不紧张,则会忽略这种提醒
join join方法会使当前线程永远的等待,直到期间别另外的线程中断,或者join的线程执行完毕。也可指定join毫秒数
getId 获取线程ID
getName 获取线程名称
setPriority 设置线程执行优先级,大于1小于10,默认线程优先级为0。避免业务严重依赖线程优先级
守护线程
一般用于处理后台工作,正常线程结束后,守护线程也会结束
thread.setDaemon(true);设置为守护线程,只能在线程启动前才会生效
thread.isDaemon();判断是否为守护进程
如何关闭一个线程
线程结束生命周期正常结束
捕获中断信号关闭线程。通过isInterrupted方法检查线程interrupt的标识来决定是否退出
使用volatile开关控制。由于线程的interrupt标识可能会被擦除,或者逻辑单元中不会调用任何中断方法,所以使用volatile修饰flag开关关闭线程也可行。
异常退出。抛出异常结束生命周期
线程安全
synchronized
提供了一种锁的机制,能确保共享变量的互斥访问,防止出现数据不一致问题
包括monitor enter和monitor exit两个JVM指令能够保证任何时候线程执行到monitor enter成功之前必须从主内存中获取数据,在monitor exit运行成功之后,共享变量被更新后的值必须刷新到主内存中
严格遵守happens-before规则,一个monitor exit指令之前必须有一个monitor enter
可以用作代码块或者方法上
死锁
死锁原因
交叉锁可导致死锁
一问一答的数据交换
数据库锁
死循环引起死锁
文件锁
内存不足导致死锁
预防死锁,破坏任意一条
互斥。共享资源X和Y只能被同一个线程占用
占用且等待。线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X
不可抢占。其他线程不能强行抢占线程T1占有的资源
循环等待。线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源
线程间通信
单线程通信
wait
必须拥有该对象的monitor,也就是必须在同步方法中使用
执行wait方法后会放弃该monitor的所有权,并且进入与该对象关联的wait set中,其他线程也有机会继续争夺该monitor的所有权
notify
唤醒单个正在执行该对象的wait方法的线程
如果有某个线程由于执行该对象的wait方法而进入阻塞则会被唤醒,如果没有则会忽略
多线程间的通信
notifyAll
notify只能唤醒一个线程,notifyAll可以同时唤醒全部阻塞的线程,同样唤醒的线程需要重新抢夺monitor的锁
线程池
为什么使用线程池
主要目的是为了重复利用线程,提高系统效率
创建Thread是一个重量级的资源,创建、启动以及销毁都是比较耗费系统资源的
核心参数
corePoolSize
线程池保有的最小线程数。即使整个线程池都很闲,没有任务可执行,也需要保留corePoolSize线程。
maximumPoolSize
线程池创建的最大线程数。当任务很多时,就需要增加线程来处理了,最多增加到maximumPoolSize个线程。当任务减少线程空闲下来时,会回收线程,保留corePoolSize个线程。
workQueue
工作队列。当线程数达到corePoolSize数量时,再有任务提交,会被放入内存队列中,如果该队列是有界队列,当该队列满了时,会继续创建不超过maximumPoolSize线程数的线程。当有线程执行完任务会从该队列中取任务进行执行,使用无界队列会有OOM的风险。
threadFactory
通过这个参数可以自定义如何创建线程。例如:你可以给线程指定一个有意义的名字
handler
拒绝策略。通过该参数可以自定义任务的拒绝策略。如果线程池内的线程都在忙碌,并且workQueue队列也已经满了(前提是队列是有界队列),此时提交线程线程池就会执行拒绝策略,至于拒绝的策略是什么,可以通过handler这个参数来指定。
ThreadPoolExecutor提供的四种拒绝策略
CallerRunsPolicy:提交任务的线程自己去执行该任务。
AbortPolicy:默认的拒绝策略,会throwsRejectedException。
DiscardPolicy:直接丢弃任务,没有任何异常抛出
DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后新任务加入到工作队列。
创建多少个线程合适
CPU密集型
CPU核数+1
I/O密集型
单核计算方式:最佳线程数 = 1+(I/O耗时/CPU耗时)
多核计算方式:最佳线程数 = CPU核数*[1 + (I/O耗时/CPU耗时)]
锁
锁的简介
悲观锁
悲观锁悲观的认为每一次操作都会造成更新丢失问题,在每次查询时都会加上排他锁
乐观锁
乐观锁乐观的认为每次查询都不会造成更新丢失问题,利用版本号子弹进行控制。
重入锁
重入锁又名递归锁。指的是同一线程,外层函数获得锁后,内层递归函数仍然获取该锁。
读写锁
两个线程同时读取一个共享资源没有任何问题,所以允许多个线程同时读取共享资源。但如果有一个线程想要写这些共享资源,就不应该被其他线程对这个资源进行读写操作。(读-读可以,读-写不行,写-写不行)
自旋锁
自旋锁是采用让当前线程不停的在循环体内执行实现的,当循环的条件被其他线程改变时才能进入临界区
CAS无锁
非阻塞性,免疫死锁问题。线程间的相互影响较小,没有锁竞争带来的系统开销,也没有线程频繁调度带来的开销。
分布式锁
需要第三方组件来实现,redis实现分布式锁、Zookeeper实现、数据库实现等
公平锁
多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
锁优化
自旋锁与自适应锁
自适应的自旋锁,自适应意味着自旋的时间不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者状态来决定。
锁消除
如果在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把他们当做栈上数据对待,认为他们是线程私有的,同步加锁也就无须进行
锁粗化
如果虚拟机探测到有这样一串零碎的操作都会对一个对象加锁,将会把加锁的同步范围扩展到整个操作序列的外部
轻量级锁
在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量
在无竞争的情况下使用CAS操作去消除同步使用的互斥量
偏向锁
在无竞争的情况下把整个同步都消除掉,连CAS的操作都不做了
偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要进行同步
0 条评论
回复 删除
下一页