1、Java中线程的实现方式
继承Thread类,重写run方法
实现Runnable接口,重写run方法
实现Callable,重写call方法,配合FutureTask
基于线程池构建线程
-
线程的原理
- 均实现了Runnable接口
2、Java中线程的状态
-
现象上5种
新建:new
就绪:调用start方法
运行:CPU调度线程
等待:wait、sleep、锁
结束
-
Thread里面的state有6种
NEW:创建
RUNNABLE:调用start和被调用都是这个状态
BLOCKED:锁,在锁池
WAITING:在等待池
TIMED_WAITING:sleep
TERMINATED:执行结束
3、Java中如何停止线程
线程结束方式很多,最常用的就是让线程的run方法结束,无论是return结束,还是抛出异常,都可以
-
stop方法(不用,已经过时了)
- 强制结束
-
使用共享变量(很少会用)
- 通过修改共享变量,结束线程里面执行的逻辑,线程也就停止执行了
-
interrupt方式:中断标记,默认false
interrupt():将中断标识位修改为true
isInterrupted:获取中断状态
interrupted:获取并重置中断状态
4、Java中sleep和wait方法的区别
sleep属于Thread中的static方法,wait属于Object类的方法
sleep属于TIMED_WAITING状态,自动被唤醒,wait属于WAITING,需要手动唤醒
sleep方法在持有锁时执行,不会释放锁,wait在执行后,会释放锁资源
-
sleep可以在持有或不持有锁时执行,wait方法必须在持有锁时才可以执行
- wait方法会将持有锁的线程放到等待池,这个操作需要修改ObjectMonitor对象,该对象是锁对象,只有持有锁时才能操作
5、并发编程的三大特性
-
原子性:一个操作是不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到它
synchronized
CAS
Lock锁
-
可见性:CPU三级缓存导致数据无法及时同步到主内存
volatile:强制写到主内存
synchronized:加锁时可以保证
Lock:加锁时可以保证
final:数据无法变更
-
有序性:指令重排导致
volatile:添加内存屏障
happens-before
6、什么是CAS?有什么优缺点?
compare and swap:先查看内存中的值是否与预期的值一致,如果一致,执行替换操作。这个操作是一个原子性操作。
优点:整个过程一直在循环,没有用户态切换内核态
-
缺点:
-
如果并发量大,自旋时间过长,会消耗CPU资源
- 可以指定CAS一共循环多少次,如果超过,直接失败或者挂起线程
-
会产生ABA问题:线程修改数据前,该数据已经被其它线程从A修改为B,又从B修改成A,符合条件,但是当前的数据已经不是之前该线程读取的数据了
- 可以通过添加版本号来解决,修改前对比版本号
-
7、@Contended注解有什么用?
解决伪共享问题:缓存行中可能存在其它线程的值,如果修改了,需要重新去主存中查,这个过程影响性能
会将当前类中的属性独占一个缓存行,从而避免缓存行失效造成的性能问题
8、Java中的四种引用类型
-
强:最常见,强引用的对象不会被垃圾回收机制回收
- 是造成java内存泄漏的主要原因
-
软:系统内存足够时,不会被回收;一般作为缓存使用
- SoftRefrence创建
弱:只要垃圾回收机制运行,必然回收
虚:不能单独使用,必须和引用队列联合使用。主要作用是跟踪对象被垃圾回收的状态
9、ThreadLocal的内存泄漏问题
-
原理
每个Thread对象内部会存储一个成员变量,ThreadLocalMap
ThreadLocal本身不存储数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap
ThreadLocalMap本身就是基于Entry[]实现的,因为一个线程可以绑定多个ThreadLocal,这样一来,可能需要存储多个数据,所以采用Entry[]的形式实现。
每一个线程都有自己独立的ThreadLocalMap,再基于ThreadLocal对象本身作为key,对value进行存取
ThreadLocalMap的key是一个弱引用,每次GC必然会被回收。这里是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,对导致ThreadLocal对象无法被回收
-
ThreadLocal内存泄漏问题
如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法被获取到
只需要在使用完毕ThreadLocal对象后,及时的调用remove()方法,移除Entry即可
10、Java中锁的分类
-
可重入锁、不可重入锁
java中提供的synchronized、ReentrantLock等都是可重入锁
重入:当前线程获取到A锁,在获取之后尝试再次获取A锁是可以直接拿到的
不可重入:当线程获取到A锁,在获取之后尝试再次获取A锁,无法获取到,因为A锁被当前线程占用着,需要等待自己释放锁再次获取锁
-
乐观锁、悲观锁
java中提供的synchronized、ReentrantLock等都是悲观锁
java中提供的CAS操作,就是乐观锁的一种实现
-
悲观锁:获取不到锁资源时,会将当前线程挂起(进入BLOCKED,WAITING),线程挂起会涉及到用户态和内核态的切换,而这种切换是比较消耗资源的
用户态:JVM可以自行执行的指令,不需要借助操作系统执行。
内核态:JVM不可以自行执行,需要操作系统才可以执行。
乐观锁:获取不到锁资源,可以再次让CPU调度,重新尝试获取锁资源
-
-
公平锁、非公平锁
Java中提供的synchronized只能是非公平锁
Java中提供的ReentrantLock、ReentrantReadWriteLock可以实现公平锁和非公平锁
公平锁:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,锁被A持有,同时线程B在排队,C直接排到B的后面,等待B拿到锁资源或者是B取消后,才可以尝试去竞争锁资源。
-
非公平锁:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,先尝试竞争一波
拿到锁资源:插队成功
没有拿到锁资源:依然要排到B的后面,等待B拿到锁资源或者B取消后,才可以尝试去竞争锁资源。
-
互斥锁、共享锁
Java中提供的synchronized、ReentrantLock是互斥锁
Java中提供的ReentrantReadWriteLock,有互斥锁也有共享锁
互斥锁:同一时间点,只会有一个线程持有着当前互斥锁
共享锁:同一时间点,当前共享锁可以被多个线程同时持有
11、synchronized在JDK1.6中的优化
锁消除:在synchronized修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,即使写了synchronized,也不会触发
锁膨胀:如果在一个循环中,频繁的获取和释放锁资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗。
-
锁升级:ReentrantLock的实现,是先基于乐观锁的CAS尝试获取锁资源,如果拿不到锁资源,才会挂起线程。synchronized在1.6之前,如果获取不到锁,立即挂起当前线程,所以synchronized性能比较差。JDK1.6之后对synchronized做了锁升级的优化
无锁、匿名偏向:当前对象没有作为锁存在。
-
偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断当前指向的线程是否是当前线程。
如果是,直接拿着锁资源走
如果当前线程不是,基于CAS的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级成轻量级锁。(偏向锁状态出现了锁竞争的情况)
-
轻量级锁:会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁)
如果成功获取到,拿着锁资源走
如果自旋了一定次数,没有拿到锁资源,锁升级为重量级锁
重量级锁:就是最传统的synchronized方式,拿不到锁资源,就挂起当前线程
12、synchronized实现原理
synchronized是基于对象实现的,存在于对象头的MarkWord中
无锁的地址指针为空
偏向锁地址指针指向当前线程
轻量级锁指向线程栈中LockRecord的指针
重量解锁指向互斥量(ObjectMonitor)的指针
-
ObjectMonitor:该对象存储了当前锁的所有相关信息
MarkWord
竞争锁的线程个数
wait的线程个数
当前线程重入锁次数
当前线程
等待池(wait状态)
等待获取资源的线程队列
13、什么是AQS?
AQS就是AbstractQueueSynchronizer抽象类,AQS其实就是JUC包下的一个基类,JUC下的很多内容都是基于AQS实现了部分功能,比如ReentrantLock,ThreadPoolExecutor,阻塞队列,CountDownLatch,Semaphore,CyclicBarrier扽等都是基于AQS实现的
首先AQS中提供了一个由volatile修饰,并且采用CAS方式修改的int类型的state变量。
其次AQS中维护了一个双向链表,有head,有tail,并且每个节点都是Node对象
14、AQS唤醒节点时,为何从后往前找
从前往后找,可能会错过某个节点
-
场景一:某个线程节点插入队列
先把当前节点的prev指向队列的最后一个节点
然后把队列的最后一个节点指向当前节点
此时前一个节点的下一个节点还没有指向当前节点,极端情况下会从前往后找会漏掉当前节点
-
场景二:某个节点取消
先把下一个节点的prev指向当前节点的上一个节点
此时如果从前往后,由于上一个节点的下一个节点没有指向下一个节点,会漏掉后面的节点
15、ReentrantLock和synchronized的区别
-
使用层面
synchronized:锁的是对象,无需释放资源
ReentrantLock:锁需要创建锁对象,锁的是代码块,需要释放资源
-
原理层面
synchronized:是关键字,有锁升级概念,锁升级之后不能降级,重量级锁底层基于ObjectMonitor对象,只支持非公平
ReentrantLock:是类,基于AQS,支持公平和非公平;因为是类,所以能实现的功能更全面
16、ReentrantReadWriteLock的实现原理
读写锁:读操作并行,写操作互斥
-
基于state操作
读锁操作:基于state的高16位
写锁操作:基于state的低16位
-
ReentrantReadWriteLock是可重入锁
写重入锁:读写锁中的重入方式,基本和ReentrantLock一致,没什么区别,依然是对state进行+1,只要确认持有锁资源的线程,是当前写锁线程即可。唯一区别是state范围减小
读重入锁:因为读锁是共享锁。读锁再获取锁资源操作时,是要对state的高16位进行+1操作。因为读锁是共享锁,所以同一时间会有多个读线程持有读锁资源。这样一来,多个读操作在持有读锁时,无法确认每个线程读锁的重入次数。为了去记录读锁重入的次数,每个读操作的线程,都会有一个ThreadLocal记录锁重入的次数
-
写锁的饥饿问题:读锁时共享锁,当有线程持有读资源时,再来一个线程想要获取读锁,直接对state修改即可。在读锁资源先被占用后,来了一个写锁资源,此时,大量的需要获取读锁的线程来请求资源,如果可以绕过写锁,直接拿资源,会造成写锁长时间无法获取到写锁资源。
- 所以,读锁在拿到锁资源后,如果再有读线程需要获取读锁资源,需要去AQS队列排队。如果队列的前面有需要写锁资源的线程,那么后续的读线程时无法拿到锁资源的。持有读锁的线程,指挥让写锁线程之前的读线程拿到锁资源
17、JDK中提供了哪些线程池?
-
newFixedThreadPool
- 线程数固定,都是核心
-
newSingleThreadExecutor
- 只有一个核心线程
-
newCachedThreadPool
- 0核心,线程先入队列
-
newScheduleThreadPool
- 定时任务线程池,延迟或周期性执行
-
newWorkStealingPool
分治
每个线程都有自己的阻塞队列
如果当前线程队列里面都执行完了,会去获取其它线程的任务
18、线程池的核心参数有什么?
corePoolSize:核心线程数
maximumPoolSize:最大工作线程数
keepAliveTime:非核心线程在阻塞队列位置等待时间
unit:非核心工作线程在阻塞队列位置等待时间的单位
workQueue:任务在没有核心工作线程处理时,任务先扔到阻塞队列中
threadFactory:构建线程的线程工厂,可以设置thread的一些信息
-
handler:拒绝策略,核心、队列、工作线程都满了
AbortPolicy:抛异常
CallerRunsPolicy:将任务交给调用者处理
DiscardPolicy:将任务丢弃
DiscardOldestPolicy:将队列中最早的任务丢弃,将当前任务尝试交给线程池处理
自定义Policy:根据业务,可以将任务扔到数据库,也可以做其它操作
19、线程池的状态?
-
AtomicInteger属性
高3位:线程池状态
低29位:线程数
-
状态
RUNNING(-1):线程池new出来以后就是RUNNING
SHUTDOWN(0)(shutdown()):不会接收新任务,正在处理的任务正常进行,阻塞队列的任务也会做完
STOP(1)(shutdownNow()):不会接收新任务,正在处理任务的线程会被中断(中断标识位interrupt),阻塞队列的任务一个都不管
TIDYING(2):一个过渡的状态,提供了一个扩展方法,在线程处理完之后执行
TERMINATED(3):线程池没了
20、线程池的执行流程
-
描述
添加任务到线程池
判断是否能获取或者创建核心线程,可以就正常执行,不能就扔进队列
判断是否能获取或者创建非核心线程,可以就执行,不行就走拒绝策略
-
需要判断的点
任务是否为null
核心线程是否可以添加
线程池状态是否正常
非核心线程是否可以添加
21、线程池添加工作线程的流程
主要是addWorker方法
校验线程池的状态以及工作线程个数
添加工作线程并且启动工作线程
22、线程池为何要构建空任务的非核心线程
场景:阻塞队列有任务,没有工作线程
-
原因:
核心线程数为0
核心线程手动修改了超时参数,正好超时了
23、线程池使用完毕为何必须shutdown()?
核心线程无法回收,核心线程创建指向了线程池内部类Worker,所以导致整个线程池无法被回收,导致内存泄漏
- SHUTDOWN(0)(shutdown()):不会接收新任务,正在处理的任务正常进行,阻塞队列的任务也会做完
24、线程池的核心参数如何设置?
核心线程数:这个最重要,做压测调整
阻塞队列
拒绝策略
25、ConcurrentHashMap在1.8中做了什么优化
存储结构:数组+链表+红黑树
-
存储操作:CAS+synchronized
- 主要是针对数组的位置,如果数组当前位置为null,用CAS写node,如果不为null,用synchronized
扩容:多线程扩容
线程安全、弱一致
26、ConcurrentHashMap扩容的流程
-
1.7
- 基于Segment分段:每个分段相当于一个Map(只hash一次),所以扩容是类似Map扩容,每个Segment单独判断是否扩容
-
1.8
- 基于Node:多线程扩容,不同线程负责不同的Node节点进行扩容
27、ConcurrentHashMap读取数据的流程
先查数组,再查链表或红黑树
如果正在扩容,查新数组或者扩容时保留的双向链表