今日八股——JUC篇(二)
谈谈对 volatile 的理解-可见性
- 保证线程间的可见性
- 禁止进行指令重排序
保证线程间的可见性
用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见。
解决方案一:在程序运行的时候加入vm参数-Xint表示禁用即时编译器,不推荐,得不偿失(其他程序还要使用)
解决方案二:在修饰stop变量的时候加上volatile,当前告诉 jit,不要对 volatile 修饰的变量做优化
volatile 禁止指令重排序
用 volatile 修饰共享变量会在读、写共享变量的时候加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排列的效果。
volatile使用技巧:
- 写变量让volatile修饰的变量的在代码最后位置
- 读变量让volatile修饰的变量的在代码最开始位置
什么是 AQS
全称是 AbstractQueuedSynchronizer
,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架。
AQS 与 Synchronized 的区别:
AQS常见的实现类
- ReentrantLock 阻塞式锁
- Semaphore 信号量
- CountDownLatch 倒计时锁
工作机制
最初线程 0 获取到锁,state 改为 1。此后,新的线程试图获取当前锁,无法得到,进入队列(先进先出)中等待,直到线程 0 释放锁,state 改为 0, 唤醒队列中 head 线程,head 线程获取锁,修改 state,以此往复。
AQS 多个线程共同去抢这个资源是如何保证原子性的?
AQS 是公平锁吗,还是非公平锁?
可以实现非公平锁,也可以实现公平锁。
非公平锁
具体表现为,线程 0 释放锁后,同时新来线程 5,线程 5 跟队列中的等待线程抢夺资源,是非公平锁。
公平锁
具体表现为,在上述过程中,线程 5 直接到队列末尾排队等待锁的释放。讲究先来后到。而线程 1 直接获取锁,也就是公平锁。
最终回答
- 是多线程中的队列同步器。是一种锁机制,它是做为一个基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的
- AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程
- 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相等于获取了资源
- 在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性
线程池
说一下线程池的核心参数
- corePoolSize 核心线程数目
- maximumPoolSize 最大线程数目 = (核心线程 + 救急线程的最大数目)
- keepAliveTime 生存时间 - 救急线程的生存时间, 生存时间内没有新任务, 此线程资源会释放
- unit 时间单位 - 救急线程的生存时间单位, 如秒、毫秒等
- workQueue 当没有空闲核心线程时, 新来任务会加入到此队列排队, 队列满会创建救急线程执行任务
- threadFactory线程工厂 - 可以定制线程对象的创建, 例如设置线程名字、是否是守护线程等
- handler拒绝策略 - 当所有线程都在繁忙, workQueue 也放满时, 会触发拒绝策略
线程池的执行原理
- AbortPolicy: 直接抛出异常,默认策略;
- CallerRunsPolicy: 用调用者所在的线程来执行任务;3. DiscardOldestPolicy: 丢弃阻塞队列中靠最前的任务,并执行当前任务;
- DiscardPolicy:直接丢弃任务;
线程池有哪些常见的阻塞队列?
workQueue - 当没有空闲核心线程时, 新来任务会加入到此队列排队, 队列满会创建救急线程执行任务
- ArrayBlockingQueue: 基于数组结构的有界阻塞队列, FIFO。
- LinkedBlockingQueue: 基于链表结构的有界阻塞队列, FIFO。
- DelayedWorkQueue : 是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的
- SynchronousQueue: 不存储元素的阻塞队列, 每个插入操作都必须等待一个移出操作。
ArrayBlockingQueue 和 LinkedBlockingQueue 区别
LinkedBlockingQueue | ArrayBlockingQueue |
---|---|
默认无界,支持有界 | 强制有界 |
底层是链表 | 底层是数组 |
是懒惰的,创建节点的时候添加数据 | 提前初始化 Node 数组 |
入队会生成新 Node | Node需要是提前创建好的 |
两把锁(头尾) | 一把锁 |
如何确定核心线程数?
N 指 CPU 核数
线程池的种类有哪些?
在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见就有四种
- 创建使用固定线程数的线程池
- 核心线程数和最大线程数相等,没有救急线程
- 阻塞队列是
LinkedBlockingQueue
,最大容量为Integer.MAX_VALUE
适用于任务量已知,相对耗时的任务。
- 单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行
- 核心线程数和最大线程数都是 1
- 阻塞队列是
LinkedBlockingQueue
,最大容量为Integer.MAX_VALUE
适用于按照顺序执行的任务
- 可缓存线程池
- 核心线程数为0
- 最大线程数是Integer.MAX_VALUE
- 阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
适用于任务数比较密集,但每个任务执行时间较短的情况
- 提供了“延迟”和“周期执行”功能的 ThreadPoolExecutor
为什么不建议使用 Executors 创建线程池
使用场景
线程池使用场景(项目哪里使用了线程池)?
CountDownLatch
多线程使用场景一(es 数据批量导入)
不讲这个,就按照自己项目代码讲
其他
谈谈对 ThreadLocal 的理解
基本使用
ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal同时实现了线程内的资源共享。
- set(value)设置值
- get()获取值
- remove()清除值
实现原理&源码分析
set 方法
首先获取当前线程对象,获取 ThreadLocalMap,如果 map 存在,进行数据存储。如果不存在,执行 createMap
,与 HashMap 相似,经过位运算取得索引,然后将数据存到 table 对应索引。
get 方法
ThreadLocal 内存泄露问题
Java对象中的四种引用类型:强引用、软引用、弱引用、虚引用
- 强引用:最为普通的引用方式,表示一个对象处于有用且必须的状态,如果一个对象具有强引用,则GC并不会回收它。即便堆中内存不足了,宁可出现OOM,也不会对其进行回收。
- 弱引用:表示一个对象处于可能有用且非必须的状态。在GC线程扫描内存区域时,一旦发现弱引用,就会回收到弱引用相关联的对象。对于弱引用的回收,无关内存区域是否足够,一旦发现则会被回收。
每个 Thread 维护一个 ThreadLocalMap,在 ThreadLocalMap 中的 Entry 对象继承了 WeakReference。其中 key 为使用弱引用的 ThreadLocal 实例,value 为线程变量的副本。
如果这篇文章对你有帮助,请点赞、评论、收藏,创作不易,你的支持是我创作的动力。