Java线程与锁常考知识点
- 基础篇
- 1. 创建线程的几种方式
- 2. 线程池创建的方式
- 3. 线程池提交任务的流程
- 4. 线程池有哪些状态
- 进阶篇
- 1. 说说对线程安全的理解
- 2. 对守护线程的理解
- 3. ThreadLocal的底层原理
- 4. 并发、并⾏、串⾏之间的区别
- 5. Java死锁如何避免?
- 6. 线程池的底层⼯作原理
- 7. 线程池为什么是先添加列队⽽不是先创建最⼤线程?
- 8. ReentrantLock中的公平锁和⾮公平锁的底层实现
- 9. ReentrantLock中tryLock()和lock()⽅法的区别
- 10. CountDownLatch和Semaphore的区别和底层原理
- 11. Sychronized的偏向锁、轻量级锁、重量级锁
- 12. Sychronized和ReentrantLock的区别
- 13. 谈谈你对AQS的理解,AQS如何实现可重⼊锁?
基础篇
1. 创建线程的几种方式
继承Thread类
,重写run方法,调用start方法启动线程实现Runnable接口
,实现run方法。
a. 实例化一个Thread类,传入Runnable实现类,调用Thread类的start方法启动线程。
b. 匿名内部类实现Runnable接口传入一个实例化的Thread类
c. lambda表达式实现Runnable接口传入一个实例化的Thread类实现Callable接口
。构造一个FutureTask任务,传入当前实现类。构造一个Thread类,传入FutureTask,调用start方法启动线程,可以使用FutureTask的get方法获得执行结果
线程运行的本质就是Thread中的实现的Runnable的run方法被执行,如果没重写Thread中的run方法,就会执行构造线城时传入的Runnable接口的run方法
2. 线程池创建的方式
线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors
创建的,1 种是通过ThreadPoolExecutor
创建的):
Executors.newFixedThreadPool
:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;Executors.newCachedThreadPool
:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;Executors.newSingleThreadExecutor
:创建单个线程数的线程池,它可以保证先进先出的执行顺序;Executors.newScheduledThreadPool
:创建一个可以执行延迟任务的线程池;Executors.newSingleThreadScheduledExecutor
:创建一个单线程的可以执行延迟任务的线程池;Executors.newWorkStealingPool
:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。ThreadPoolExecutor
:最原始的创建线程池的方式,它包含了 7 个参数可供设置。
不建议使用
Executors
方式创建线程池,因为其底层使用的是阻塞队列,容易出现内存泄漏(OOM),所以建议直接使用ThreadPoolExecutor
直接创建线程池,可以灵活控制
详细分析参考:ThreadPoolExecutor创建线程池
3. 线程池提交任务的流程
ThreadPoolExecutor提交任务的流程如下:
- 当线程数小于核心线程数时,创建线程。
- 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
- 当线程数大于等于核心线程数,且任务队列已满:若线程数小于最大线程数,创建线程;若线程数等于最大线程数,抛出异常,拒绝任务。
4. 线程池有哪些状态
-
RUNNING
:这是最正常的状态,接受新的任务,处理等待队列中的任务。线程池的初始化状态是RUNNING。线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0。 -
SHUTDOWN
:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的shutdown()方法时,线程池由RUNNING -> SHUTDOWN。 -
STOP
停止:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。调用线程池的shutdownNow()方法时,线程池由(RUNNING or SHUTDOWN ) -> STOP。 -
TIDYING
清空:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理;可以通过重载terminated()函数来实现。
当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。 -
TERMINATED
结束:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
进阶篇
1. 说说对线程安全的理解
线程安全指的是,我们写的某段代码,在多个线程同时执⾏这段代码时,不会产⽣混乱,依然能够得到正常的结果,⽐如i++,i初始化值为0,那么两个线程来同时执⾏这⾏代码,如果代码是线程安全的,那么最终的结果应该就是⼀个线程的结果为1,⼀个线程的结果为2,如果出现了两个线程的结果都为1,则表示这段代码是线程不安全的。
所以线程安全,主要指的是⼀段代码在多个线程同时执⾏的情况下,能否得到正确的结果
。
2. 对守护线程的理解
线程分为⽤户线程
和守护线程
,⽤户线程就是普通线程
,守护线程就是JVM的后台线程
,⽐如垃圾回收线程就是⼀个守护线程,守护线程会在其他普通线程都停⽌运⾏之后⾃动关闭
。我们可以通过设置thread.setDaemon(true)
来把⼀个线程设置为守护线程。
3. ThreadLocal的底层原理
线程本地存储机制
。ThreadLocal是Java中所提供的线程本地存储机制,可以利⽤该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意⽅法中获取缓存的数据ThreadLocalMap
。ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值线程池使用需要手动清除
。如果在线程池中使⽤ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使⽤完之后,应该要把设置的key,value,也就是Entry对象进⾏回收,但线程池中的线程不会回收,⽽线程对象是通过强引⽤指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收,Entry对象也就不会被回收,从⽽出现内存泄漏,解决办法是,在使⽤了ThreadLocal对象之后,⼿动调⽤ThreadLocal的remove⽅法
,⼿动清除Entry对象连接管理
。ThreadLocal经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅法之间进⾏传递,线程之间不共享同⼀个连接)
4. 并发、并⾏、串⾏之间的区别
- 串⾏:⼀个任务执⾏完,才能执⾏下⼀个任务
- 并⾏(Parallelism):两个任务同时执⾏
- 并发(Concurrency):两个任务整体看上去是同时执⾏,在底层,两个任务被拆成了很多份,然后⼀个⼀个执⾏,站在更⾼的⻆度看来两个任务是同时在执⾏的
5. Java死锁如何避免?
造成死锁的⼏个原因:
- ⼀个资源每次只能被⼀个线程使⽤
- ⼀个线程在阻塞等待某个资源时,不释放已占有资源
- ⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺
若⼲线程形成头尾相接的循环等待资源关系
如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
在开发过程中避免死锁:
- 要注意
加锁顺序
,保证每个线程按同样的顺序进⾏加锁 - 要注意
加锁时限
,可以针对所设置⼀个超时时间 - 要注意
死锁检查
,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决
6. 线程池的底层⼯作原理
线程池内部是通过队列+线程
实现的,当我们利⽤线程池执⾏任务时:
- 如果此时线程池中的线程数量
⼩于corePoolSize
,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。 - 如果此时线程池中的线程数量
等于corePoolSize
,但是缓冲队列workQueue未满,那么任务被放⼊缓冲队列。 - 如果此时线程池中的线程数量
⼤于等于corePoolSize
,缓冲队列workQueue满,并且线程池中的数量⼩于maximumPoolSize
,建新的线程来处理被添加的任务。 - 如果此时线程池中的线程数量
⼤于corePoolSize
,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。 - 当线程池中的线程数量
⼤于 corePoolSize
时,如果某线程空闲时间超过keepAliveTime,线程将被终⽌。这样,线程池可以动态的调整池中的线程数
7. 线程池为什么是先添加列队⽽不是先创建最⼤线程?
当线程池中的核⼼线程都在忙时,如果继续往线程池中添加任务,那么任务会先放⼊队列,队列满了之后,才会新开线程。
这就相当于,⼀个公司本来有10个程序员,本来这10个程序员能正常的处理各种需求,但是随着公司的发展,需求在慢慢的增加,但是⼀开始这些需求只会增加在待开发列表中,然后这10个程序员加班加点的从待开发列表中获取需求并进⾏处理,但是某⼀天待开发列表满了,公司发现现有的10个程序员是真的处理不过来了,所以就开始新招员⼯了。
8. ReentrantLock中的公平锁和⾮公平锁的底层实现
⾸先不管是公平锁和⾮公平锁,它们的底层实现都会使⽤AQS
来进⾏排队,它们的区别在于:线程在使⽤lock()⽅法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队
,如果有线程在排队,则当前线程也进⾏排队,如果是⾮公平锁,则不会去检查是否有线程在排队
,⽽是直接竞争锁。
不管是公平锁还是⾮公平锁,⼀旦没竞争到锁,都会进⾏排队,当锁释放时,都是唤醒排在最前⾯的线程,所以⾮公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段
。
另外,ReentrantLock是可重⼊锁,不管是公平锁还是⾮公平锁都是可重⼊的。
9. ReentrantLock中tryLock()和lock()⽅法的区别
- tryLock()表示尝试加锁,可能加到,也可能加不到,该⽅法
不会阻塞线程
,如果加到锁则返回true,没有加到则返回false - lock()表示阻塞加锁,线程会
阻塞直到加到锁
,⽅法也没有返回值
10. CountDownLatch和Semaphore的区别和底层原理
CountDownLatch表示计数器
,可以给CountDownLatch设置⼀个数字,⼀个线程调⽤CountDownLatch的await()将会阻塞,其他线程可以调⽤CountDownLatch的countDown()⽅法来对CountDownLatch中的数字减⼀,当数字被减成0后,所有await的线程都将被唤醒。对应的底层原理就是,调⽤await()⽅法的线程会利⽤AQS排队,⼀旦数字被减为0,则会将AQS中排队的线程依次唤醒
。
Semaphore表示信号量
,可以设置许可的个数,表示同时允许最多多少个线程使⽤该信号量,通过acquire()来获取许可,如果没有许可可⽤则线程阻塞,并通过AQS来排队,可以通过release()⽅法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第⼀个线程开始依次唤醒,直到没有空闲许可
。
11. Sychronized的偏向锁、轻量级锁、重量级锁
- 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该
线程下次如果⼜来获取该锁就可以直接获取
到了 - 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果
有第⼆个线程来竞争锁,偏向锁就会升级为轻量级锁
,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过⾃旋来实现的,并不会阻塞线程
- 如果
⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁
,重量级锁会导致线程阻塞
- ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,
不会去阻塞线程
,也就⽆所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS
获取预期的⼀个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量
。
12. Sychronized和ReentrantLock的区别
- sychronized是⼀个
关键字
,ReentrantLock是⼀个类
- 自动加锁。sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
- 底层。sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁
- 公平性。sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
- 锁的方式。 sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
- sychronized底层有⼀个
锁升级
的过程
13. 谈谈你对AQS的理解,AQS如何实现可重⼊锁?
- AQS是⼀个
JAVA线程同步的框架
。是JDK中很多锁⼯具的核⼼实现框架。 - 在AQS中,维护了⼀个
信号量state
和⼀个线程组成的双向链表队列
。其中,这个线程队列,就是⽤来给线程排队的,⽽state就像是⼀个红绿灯,⽤来控制线程排队或者放⾏的。 在不同的场景下,有不⽤的意义。 - 在可重⼊锁这个场景下,state就⽤来表示加锁的次数。0标识⽆锁,每加⼀次锁,state就加1。释放锁state就减1。