目录
4. Thread类中 stop、interrupt、isInterrupt、interrupted 区别
10. yield、sleep、wait、notify 锁的释放
11. CountDownLatch & CyclicBarrier
11.3 CountDownLatch & CyclicBarrier 总结
16.5 AbstractQueuedSynchronized AQS同步器
16.5.1 AbstractQueuedSynchronized 需要自己实现的方法
16.5.2 AbstractQueuedSynchronized 其他方法
17.2 ReentrantReadWriteLock 实现原理
19.7 ConcurrentHashMap 1.7&1.8区别
20.1 ConcurrentSkipListMap & ConcurrentSkipListSet
20.2 CopyOnWriteArrayList&ConpyOnWriteArraySet 写时复制容器
21.4 JDK中默认线程池实现 Executors.new***()
1. 进程&线程是什么?
-
进程:操作系统分配资源的最小单位
-
线程:cpu调用最小单位
2. 并发&并行区别?
-
并行:并行 指某个时间的处理能力(4核cpu同一时刻能在运行4个线程那么并行就是4)
-
并发:并发 指某个时间段的处理能力(晚上十一点到十二点之间某平台下了1000w订单,那么并发就是1000w)
3. JAVA中创建线程几种方式
两种方式(jdk Thread类官方翻译两种 )
-
继承Thread类 实现run方法 第一种
-
实现Runable接口 第二种
-
实现Callable接口+FutureTask
4. Thread类中 stop、interrupt、isInterrupt、interrupted 区别
-
stop:stop方法调用 会直接强制中断
-
interrupt:标记这个线程需要中断 仅仅是标记 需自己实现中断逻辑
-
isInterrupted:判断线程是否需要中断
-
interrupted:Thread类中的静态方法,判断当前线程是否需要中断,判断后会将当前线程中断状态标记为false(不建议使用)
5. 线程生命周期
-
新建:线程刚被创建new Thread()
-
就绪:start方法调用 排队等待cpu执行
-
运行:run方法被调用
-
阻塞:调用 wait、sleep
-
死亡:run方法执行完
Thread类相关方法
-
start 启动当前线程
-
join 假设A线程调用B线程join(A放弃cpu执行权给B线程-start方法调用后调用否则失效)
-
yield 释放当前cpu执行权 重新去排队获取(可能当前线程会立即获取cpu执行权)
-
setDaemon 设置当前线程为守护线程(守护线程 finally快不一定执行)
6. synchronized内置锁 与 volatile
-
synchronized 锁定某个资源(类、方法、实例)使其同一时刻只能由一个线程访问
-
volatile volatile 关键字修饰属性,每次都是查询直接内存而不是访问线程中内存副本 (保正属性可见性)
PS:synchronized 关键字 锁的对象不能改变,改变会导致锁失效,不能保证线程安全(基本类型 参考反编译文件)
如
.java文件中一段代码
int num=0;
num++;
那么编译后 就会变成这段
Integer num=0;
num=Integer.valueOf(num.intValue());
valueOf 实际上是new 了一个对象
如果我们用synchronized(Integer num) 的时候锁就失效了,不是同一个对象
7. 并发工具ThreadLocal
7.1 ThreadLocal 实现原理
ThreadLocal get set remove 操作,都会去调用Thread.currentThread()拿到当前线程
而每个Thread对象中,都有ThreadLocal.ThreadLocalMap threadLocals 变量,
这个threadLocals map key是当前这个ThreadLocal 对象
所以每次ThreadLocal操作,最终会去操作当前线程中threadLocals 变量中的值
7.2 ThreadLocal 内存泄漏
ThreadLocalMap 中 key 是一个ThreadLocal 的弱引用 当jvm内存不够用是可能会回收掉 key 导致,key 一直是空的,一直会保留在内存中,无法释放
threadLocal=null 会将ThreadLocal变成一个弱引用,可能会被gc释放(假设被gc放), 会导致ThreadLocalMap中存留key=null value=object 这样一个结构,导致永远无法被gc 释放
7.3 ThreadLocal 使用注意
ThreadLocal 每次使用后 不要忘记 使用remove 释放内存 或者 申明Thread变量为static f inal的 这样这个ThreadLocal 一直都是强引用
ThreadLocal set的值 确保不是共享的,会导致线程不安全
8. 强、软、弱、虚 引用
-
强引用:Object object=new Object() ;对象引用数量=0 才会被释放
-
软引用:SoftReference jvm内存不足时,释放弱引用,内存还不够用,再释放软引用
-
弱引用:WeakReference jvm内存不足时 一定会释放
-
虚引用:
9. wait、notify、notifyAll 使用
-
wait wait方法调用 当前线程会阻塞
-
notify|notifyAll 唤醒阻塞线程 重新排队获取cpu执行权
生产者:
synchronized(object){
while(业务条件不满足)
//释放锁 阻塞当前线程
object.wait();
}
消费者:
synchronized(object){
//业务操作,操作完之后通知其他等待的线程,唤醒其他线程
object.notifyAll(); //同步代码块最后面调用
}
PS:建议使用notifyAll ,notify方法 只唤醒一个线程
10. yield、sleep、wait、notify 锁的释放
-
yield: 不释放
-
sleep: 不释放
-
wait: 释放锁
-
notify 不释放
11. CountDownLatch & CyclicBarrier
11.1 CountDownLatch 使用
假设有20个用户需要插入数据库,一个线程太慢我们用10个线程处理 每个线程插入2个(不一定是2个)
11.2 CyclicBarrier 使用
假设同样需求20个用户 10个线程执行 每个线程插入2个
11.3 CountDownLatch & CyclicBarrier 总结
-
CountDownLatch: CountDownLatch 仅仅是一个多线程 计数器工具,跟调用多少次countLatch()方法有关,跟线程数量没有任何关系,只跟countLatch()方法调用次数有关,每调用一次countLatch()计数器-1;计数器>0 外面业务线程一直被阻塞,计数器=0 外面线程恢复正常,调用其他业务逻辑;
-
CyclicBarrier: 多线程协调工具,他的值=线程数量。
12. Semaphore 使用
假设我们有个login业务,这个登录接口某个时间段(并发量)只允许10个用户登录
PS:Semaphore 使用注意点,可能有坑
由于代码逻辑问题,先调用release()释放许可证,再去调用acquire 去拿许可证
假设许可证数量 为10 ,由于程序逻辑bug
先调用release
再调用qcquire
会导致这个工具完全失效,永远不会限制许可证数量
13. Exchange 数据交换器
假设,有两个同学正在写作业,小明、小红,小明不学好作业没写,小红作业写好了,小明使坏
小明:小红 我作业写的很好,要不我们交换下吧,你拿我作业我能保证你明天拿一个小红花
小红:ヾ(≧▽≦*)o 好的好的!
14. FutureTask & Callable 使用
假设业务场景:我与张三一起玩耍,不开心了, 张三不讲武德,上来就给了我一拳,我又打不过他,怎么办?我就回去叫我家大黄(汪汪汪~),让我家大黄替我教训他(咬他-启动线程托管,教训张三的事情我不管了),大黄放出去了,但是不能乱咬啊,如果咬错了,我要上门道歉啊,所以就得问大黄,他今天咬谁(返回值)了。
又假设业务场景,大黄今天发疯了,一直咬,一直咬个不停,只有我能管得住(良民大大的,不能把事情闹大,得把大黄阻止住)cancle()方法调用
PS:cancle()方法调用其实就是调用Thread.interrupt() 具体中断逻辑得自己实现
14. Fork/Join 使用
-
RecursiveTask 有返回值
-
RecursiveAction 没返回值
假设计算0-100w数据可分割多个任务,交给多个线程去执行,再将执行结果合并
15. 原子操作CAS
15.1 什么是原子操作CAS
谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何cpu上下文切换 (来源百度百科)
15.2 原子操作原理
原子操作不是由操作系统 编程语言 支持,而是由硬件cpu支持的
如果原子操作不成功,他会一直循环直到成功
15.3 原子操作存在的问题
-
ABA问题 变量的修改 不知道有没有修改过 修改过几次?
-
开销问题 原子操作再一个do while中(操作不成功会一直循环,直到成功)
-
只能保证一个共享变量的原子操作
15.4 Jdk中提供一些原子操作类
-
基本类型 AtomicBoolean AtomicInteger AtomicLong ......
-
数组 AtomicIntegerArray AtomicLongArray AtomicReferenceArray ......
-
引用类型
-
AtomicReference
-
AtomicMarkableReference 提供是否被修改过的原子操作
-
AtomicStampReference 提供版本的原子操作
-
-
filed字段类型 AtomicReferenceFiledUpdate AtomicIntegerFiledUpdate AtomicLongFieldUpdate ......
16. 显示锁&AQS同步器
16.1 什么是显示锁
显示锁(编程式锁)上锁、释放锁,由程序员通过coding决定,是再代码中能看见的
16.2 公平锁&非公平锁
-
公平锁 ABCD四个线程抢锁,Z线程占用锁,这时候又new了一个E线程(拿到 cpu执行权,拿不到锁 先挂起),这时候Z线程释放锁,那么获取锁会 从我们队列开头(线程A)去拿锁,A拿锁又要进行cpu上下文切换,A 拿到cpu执行权后再去拿锁。这种拿锁方式是一个队列形式(A-B-C-D- E)
-
非公平锁 ABCD四个线程抢锁,Z线程占用锁,这时候又new了一个E线程(拿到 cpu执行权),这时候正好Z线程释放锁,那么E线程也刚好获取锁,E 线程 正好获取到。这种拿锁方式跟队列无关
-
非公平锁性能>公平锁
-
ReentrantLock 默认是非公平锁
16.3 共享锁与排他锁&读写锁
-
共享锁 允许多人上锁
-
排他锁|独占锁 不允许其他人上锁
-
读锁 允许其他读线程上锁 共享锁
-
写锁 不允许其他任何读写线程上锁 排他锁
-
读锁,n个读线程上锁,写锁,只能一个线程上锁,读写互斥(上了读锁不能上写锁,上了写锁就不能上读锁)
16.4 了解LockSupport 工具类中方法
-
park*() 阻塞线程
-
unpark*() 唤醒线程
16.5 AbstractQueuedSynchronized AQS同步器
AbstractQueuedSynchronized 是java 提供给开发者 实现 显示锁的一个模板
16.5.1 AbstractQueuedSynchronized 需要自己实现的方法
-
tryAcquire() 获取排他锁
-
tryRelease() 释放排他锁
-
tryAcquireShared() 获取共享锁
-
tryReleaseShared() 释放共享锁
-
isHeldExcluively() 是否属于排他锁|独占锁
16.5.2 AbstractQueuedSynchronized 其他方法
-
getState 获取同步器状态
-
setState 设置同步器状态
-
compareAndSetState 使用CAS方式设置同步器状态
-
setExclusiveOwnerThread 设置当前占用锁的线程
16.5.3 Node&ConditionObject
-
Node 当一个线程拿锁失败打包一个Node对象(双向链表)以便其他线程释放锁唤醒其他线程
-
waitStatus几种状态
-
CANCELLED 线程等待超时,或者被中断(从队列移除)
-
SIGNAL 表示需要通知后续节点拿锁
-
CONDITION 当前节点处于等待队列中
-
PROPRGTE 表示当前节点被唤醒后,同时也要唤醒后续节点
-
INITALIZE =0 初始化状态
-
-
head节点 头节点会一直自旋去拿锁
-
tail节点 每当有线程获取锁失败 放到tail节点
-
-
ConditionObject 当有线程去释放锁,通过ConditionObject 去唤醒在等待 队列 (Node)中线程
-
singal*() 唤醒线程
-
await*() 阻塞线程
-
16.5.4 独占锁原理&流程
释放锁流程:
tryRelease() 返回true 释放锁成功,唤醒头节点的下一个节点
拿锁流程:
-
tryAcquire()调用成功直接返回,调用不成功打包成Node
-
addWaiter() 将当前获取锁失败的节点 加到尾节点
-
acquireQueued() 拿到自己前置节点 如果前置节点等于队列头节点,一直自旋拿锁,直到拿锁成功,拿到锁之后将自己设置为头节点。如果前置节点不是头节点,wait阻塞当前线程。
17. JDK中显示锁
-
Lock接口 JDK实现锁的规范接口
-
ReentrantLock 一个可以避免死锁的显示锁(迭代锁)
-
ReentrantReadWriteLock 读写锁
17.1 ReentrantLock 如何避免死锁
加锁:
解锁:
17.2 ReentrantReadWriteLock 实现原理
ReentrantReadWriteLock 关键在于 AQS 同步器
ReadLock WriteLock 构造函数都需要一个同步器,而这个同步器是使用了同一个的
而这个同步器的一个state 状态 是int类型(Java 一个int类型 是 32bit)
state 分左半边(高位)表示共享锁|读锁数量 右半边(低位)排他锁|写锁 数量
读锁:拿读锁时判断(低位不等于0)是否有排他锁|写锁,没有排他锁时 才能拿到,有排他锁加入队列里,再阻塞当前线程
写锁: state 不等于0 说明有锁 ,什么锁 不知道 去计算w(写锁|排他锁)数量
第一种 情况:w写锁数量等于0 说明有读锁存在 返回false 阻塞
第二种 情况:w不等于0(有写锁)如果当前线程不等于持有锁线程 返回false 阻塞
能走第二个if:说明 当前这个写锁 是自己加的
state 等于0 说明读锁写锁都没有 直接原子操作去维护状态和持有锁线程
18. JDK 1.7 ConCurrentHashMap
18.1 数据结构
Concurrent 中 segments属性 Segment数组 组成
Segment 中 table属性 HashEntry组成
Segment 继承 ReentrantLock 本身是一个锁
HashEntry 是一个单向链表
18.2 初始化
ConcurrentHashMap Segment默认大小为16 默认并发量为16
Segement中table 默认大小为2
ConcurrentHashMap Segment默认初始化1个
18.3 Hash值计算
key.hashCode 再进行散列(Wang/Jenkins hash)
18.4 元素定位
通过自己hash()算法 再与segementShift 和 segmentMask 计算取余 定位具体segement
再通自己hash()算法 与 segment中table 数量计算取余 定位具体 HashEntry
遍历HashEntry 判断 key 是否相等
18.5 HashEntry[] table 扩容
table 扩容 每次扩容是翻倍x2
扩容后 会 reHash()重新计算(部分)HashEntry落点
18.6 如何保证线程安全?
put操作时由于Segement extents ReentrantLock 代表他自己本身就是一个锁
put时 会将自己上锁保证同一时刻只能有一个线程put成功
可见性 由 JDK中 volatile 关键字实现
18.7 Size实现
不加锁情况便利每个元素 (2次)
如果两次的值一样 返回
如果两次值不一样 锁住所有Segment 在进行统计返回
19. JDK1.8 ConcurrentHashMap
19.1 数据结构
Node 数组
Node=链表|红黑树结构TreeBin
链表长度为8时,转换为红黑树结构
19.2 初始化
1.8中 没做任何事情
初始容量也是16
table 初始化在put操作时 判断 table 是否为空 仅new 了一个 Node[]
初始化table后 sizeCtl 默认为(16*0.75)12
Node初始化 每次put时 定位Table[i] 为空 再初始化Node
19.3 Hash值计算
再散列(散列规则与1.7不一样Wang/jenkins)
同样是再散列
19.4 table 扩容
翻倍扩容
扩容时判断 Node节点大小 >8 转树结构 <8 转链表结构
多个线程put 时,会由多个线程并发扩容
19.5 如何保证线程安全
通过Synchronized 内置锁,锁住Node
可见性 同样使用volatile
19.6 Size 实现
内部 使用 ConterCell [] 数组 count累加 根据当前线程计算坐标于哪个ConterCell
然后会 进行快速一次 cas操作
如果失败会调用fullAddCount 方法 自旋cas
而size方法 统计 就是 遍历 这个ConterCell数组 累加value属性
19.7 ConcurrentHashMap 1.7&1.8区别
-
1.7 使用 链表结构
-
1.8 使用 链表+红黑树结构
-
初始化
-
1.7 一开始就实例化Segrament
-
1.8 什么事都没做,初始化在put时候实时的
-
-
hash值计算
-
两者都是 再hash 1.7 与 1.8 再散列算法不一样
-
-
扩容
-
都是按照双倍扩容
-
1.8中使用多线程扩容,到了阈值75%就扩容 1.7>阈值
-
-
如何保证线程安全
-
1.7 使用 ReentrantLock 锁住 segrament
-
1.8 使用 Synchronized 内置锁
-
两个put性能几乎一样 可能1.8慢点 1.8中还要维护count
-
-
size()
-
1.7 会先遍历整个map 2次,如果2次结果不一样,锁住整个map 再统计
-
1.8 put时根据当前线程去维护CounterCell数组,统计时遍历 CounterCell数组
-
两者都是弱一直性
-
1.8 比 1.7 快 准确性1.7高一点(锁整个map) 可用性1.8高一点,因为1.8中仅仅是遍历CounterCell数组
-
20. 跟多并发容器
20.1 ConcurrentSkipListMap & ConcurrentSkipListSet
-
TreeMap TreeSet 并发版本
-
SkipList 以空间换时间的一种数据结构 Skip List--跳表(全网最详细的跳表文章没有之一) - 简书
-
redis zSet 使用
-
性能接近于 平衡二叉树
-
使用更多空间 保存 索引(空间换时间
20.2 CopyOnWriteArrayList&ConpyOnWriteArraySet 写时复制容器
-
原理 调用add方法 使用ReentrantLock 上锁,复制一份List 进行add
-
读操作任何时刻不影响(不会加锁 ,只有写操作加锁)
-
只能保证最终一直性 ,不能保证实时一直性
-
读多写少场景,对数据实时性无要求(黑白名单)
20.3 阻塞队列
-
无界阻塞队列 队列大小没有限制
-
有界阻塞队列 队列大小有限制
-
ArrayBlockingQueue
-
由数组结构组成的有界阻塞队列
-
按照先进先出原则
-
使用一个锁
-
要求设置初始化大小
-
-
LinkedBlockingQueue
-
由链表结构组成的有界阻塞队列
-
按照先进先出原则
-
可以不设置大小 默认= Integer.MAX_VALUE;
-
使用两个锁
-
-
PriorityBlockingQueue
-
支持优先级排序的无界阻塞队列
-
默认按照自然顺序
-
可以实现自己的排序规则
-
-
DelayQueue
-
使用优先级排序的无界阻塞队列
-
支持延时获取的队列
-
-
SynchronousQueue
-
一个不存储数据的阻塞队列|同步队列
-
每一个put操作都要等待一个take操作
-
要求生产者&消费者 处理能力相等
-
-
LinkedTransferQueue
-
由链表结构组成的无界阻塞队列
-
可以将消息直接发送给消费者,省去放入队列操作
-
transfer() 必须消费者消费后才会返回,反之阻塞
-
tryTransfer() 无论消费者是否接受,方法立即返回
-
-
LinkedBlockingDeque
-
由双向链表结构组成阻塞队列
-
可以从队列 头&尾 拿消息
-
21. 线程池
21.1 为什么使用线程池
线程创建-上下文切换(wait notify)时非常消耗cpu的
项目中如果频繁去创建线程,用一次就丢掉,会浪费大量cpu性能
如果将n个线程做统一管理 可以避免频繁创建线程而浪费cpu性能
21.2 线程池中参数含义
-
corePoolSize
-
当由任务提交时,会创建corePoolSize个线程执行任务
-
-
maxnumPoolSize
-
有任务提交时 当前线程数量>maxnumPoolSize 时不会创建线程,将任务放到阻塞队列中
-
-
BlockingQueue<Runnable> workQueue 具体队列的实现
-
RejectedExecutionHandler handle 阻塞队列满时采用哪种方式处理
-
Jdk默认实现策略
-
DiscardOldersPolicy 丢弃队列最靠前任务执行当前任务
-
AbortPolicy 抛出异常
-
CallerRunspolicy 谁提交任务由谁执行
-
DiscardPolicy 直接丢弃当前任务
-
-
keepAliveTime 线程空闲时间 到期回收
21.3 如何合理配置线程数量
-
三种场景
-
CPU密集型 大量cpu计算 (正则)
-
IO密集型 大量下载文件,保存文件(磁盘,网络)
-
混合型 计算+IO
-
-
cpu密集型
-
cpu核心数量+1
-
-
IO密集型
-
最佳线程数=NCPU*UCPU*(1+W/C)
-
NCPU 处理器核心数
-
UCPU CPU期望利用率(0~1)
-
W/C 等待时间比计算时间 比率
-
不确定W/C 2*CPU核心数
-
等待时间与计算时间在Linux vmstat或者top命令查看
-
-
混合型
-
尽量拆分处理
-
21.4 JDK中默认线程池实现 Executors.new***()
-
FixedThreadPool
-
创建固定线程数量的线程池
-
内部使用LinkedBlockingQueue
-
-
SingleThreadExecutor
-
corePoolSize&maxnumPoolSize 都等于1 单线程的线程池
-
内部使用LinkedBlockingQueue
-
-
CachedThreadPool
-
线程最大值=Integer.MAX_VALUE();
-
内部使用同步队列SynchronousQueue
-
使用需谨慎
-
-
WorkStealingPool
-
默认大小等于当前cpu逻辑线程数
-
使用Fork/Join
-
-
ScheduledThreadPoolExecutor
-
延时任务式线程池
-
内部使用DelyedWorkQueue
-
schedule() n时间后执行一次
-
scheduleWithFixedDelay() 每n时间后执行
-
前一个任务超时影响后一个任务执行时间
-
Runable 中 发生异常会影响整个任务队列 (catch 异常)
-
-
CompletionService & CompletionFutuer
22. PS
仅仅自己对并发编程一些理解,不喜勿喷。