文章目录
- 一、线程的基础知识
- 1.线程与进程的区别
- 2.并行和并发有什么区别?
- 3.创建线程的方式有哪些?
- 3.1.Runnable 和 Callable 有什么区别?
- 3.2.run()和 start()有什么区别?
- 4.线程包括哪些状态,状态之间是如何变化的
- 4.1.线程包括哪些状态?
- 4.2.状态之间是如何变化的?
- 5.新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
- 6.notify()和 notifyAll()有什么区别?
- 7.java中wait和sleep方法的不同?
- 8.如何停止一个正在运行的线程?
- 2.线程中并发安全
- 9.synchronized关键字的底层原理
- 10.谈谈 JMM(Java内存模型)
- 11.什么是CAS?
- 12.请谈谈你对 volatile 的理解
- 12.1.保证线程间的可见性
- 12.1.禁止进行指令重排序
- 13.什么是AQS?
- 14.ReentrantLock的实现原理
- 15.synchronized和Lock有什么区别 ?
- 16.死锁产生的条件是什么?
- 17.聊一下ConcurrentHashMap
- 18.导致并发程序出现问题的根本原因是什么
- 3.线程池
- 19.说一下线程池的核心参数(线程池的执行原理知道嘛)
- 20.线程池中有哪些常见的阻塞队列
- 21.如何确定核心线程数
- 22.线程池的种类有哪些
- 23.为什么不建议用Executors创建线程池
一、线程的基础知识
1.线程与进程的区别
程序由指令
和数据
组成,这些指令
要运行,数据要读写,就必须将指令加载至 CPU
,数据
加载至内存
。
当一个程序被运行,从磁盘加载这个
程序的代码至内存
,这时就开启了一个进程。
二者对比
- 进程是正在运行程序的实例,
进程中包含了线程
,每个线程执行不同的任务
不同的进程使用不同的内存空间
,在当前进程下的所有线程
可以共享内存空间
- 线程更轻量,
线程上下文切换
成本一般上要比进程上下文切换低
(上下文切换指的是从一个线程切换到另一个线程)
2.并行和并发有什么区别?
现在都是多核CPU,在多核CPU下
并发
是同一时间
应对多件事情的能力
,多个线程
轮流使用一个或多个CPU
并行
是同一时间动手做多件事情的能力,4核CPU同时执行4个线程
3.创建线程的方式有哪些?
-
继承Thread类
-
实现runnable接口
-
实现Callable接口
-
线程池创建线程(项目常用)
3.1.Runnable 和 Callable 有什么区别?
参考回答:
- Runnable 接口run方法
没有返回值
- Callable接口
call方法有返回值
,是个泛型
,和Future、FutureTask
配合可以用来获取异步执行的结果
- Callable接口的call()方法
允许抛出异常
;而Runnable接口的run()方法的异常只能在内部消化
(try catch),不能继续上抛
3.2.run()和 start()有什么区别?
- 方法性质不同:
run 是一个普通方法
,而start 是开启新线程的方法
。 - 执行速度不同:调用
run 方法
会立即
执行任务,调用start 方法
是将线程的状态
改为就绪
状态,不会立即执行。(调用start()方法的时候线程不是立即执行,而是进入到一个等待状态,等待CPU进行调度
) - 调用次数不同:run 方法可以被重复调用,而 start 方法只能被调用一次。
- start 方法之所以
不能被重复调用的原因
是,线程的状态是不可逆的
,Thread 在 start的实现源码中做了判断,如果线程不是新建状态 NEW
,则会抛出非法线程状态异常
IllegalThreadStateException。
4.线程包括哪些状态,状态之间是如何变化的
4.1.线程包括哪些状态?
新建(NEW)、
可运行(RUNNABLE)、
阻塞(BLOCKED)、
等待( WAITING )、
时间等待(TIMED_WALTING)、
终止(TERMINATED)
4.2.状态之间是如何变化的?
- 创建线程对象是
新建状态
- 调用了start()方法转变为
可执行状态
- 线程获取到了CPU的执行权,执行结束是
终止状态
- 在
可执行状态
的过程中,如果没有
获取CPU的执行权
,可能会切换其他状态
其他状态:
如果没有获取锁
(synchronized或lock)进入阻塞状态
,获得锁再切换为可执行状态
如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后
可切换为可执行状态
如果线程调用了sleep(50)方法,进入计时等待状态
,到时间后可切换为可执行状态
5.新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
可以使用线程中的join方法解决
t.join()
阻塞调用此方法的线程进入timed_waiting
直到线程t执行完成后
,此线程再继续执行
6.notify()和 notifyAll()有什么区别?
notifyAll:唤醒所有wait的线程
notify:只随机唤醒一个
wait 线程
7.java中wait和sleep方法的不同?
共同点
wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权
,进入阻塞状态
不同点
不同点
- 方法归属不同
sleep(long) 是 Thread 的静态方法
而 wait(),wait(long) 都是 Object 的成员方法
,每个对象都有
- 醒来时机不同
执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
wait(long) 和 wait() 还可以被 notify 唤醒
,wait() 如果不唤醒
就一直等下去
它们都可以被打断唤醒
- 锁特性不同(重点)
wait 方法的调用必须先获取 wait 对象的锁
,而 sleep 则无此限制
wait 方法执行后会释放对象锁
,允许其它线程获得该对象锁
(我放弃 cpu,但你们还可以用)
也就是只要执行到
wait()方法不管wait方法有没有执行完
都会立即释放锁
,别的线程就可以拿到锁进而执行自己的业务。
而 sleep 如果在 synchronized 代码块中执行
,并不会释放对象锁
(我放弃 cpu,你们也用不了)
也就是
只有sleep()方法执行结束了
,才会去释放锁
,别的线程才能拿到锁,执行自己的业务
8.如何停止一个正在运行的线程?
有三种方式可以停止线程
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
也就是给线程配置一个退出运行状态标志,能让run方法执行完成后正常退出
-
使用stop方法强行终止(不推荐,方法已作废)
-
使用interrupt方法中断线程(原理和第一种是一样的)
打断阻塞的线程(
sleep,wait,join
)的线程,线程会抛出InterruptedException异常
打断正常的线程,可以根据打断状态
来标记是否退出线程
current.isInterrupted()默认是false
只有在线程调用interrupt();方法
才会将current.isInterrupted()设置为true
2.线程中并发安全
9.synchronized关键字的底层原理
Synchronized【对象锁】采用互斥的方式
让同一时刻至多只有一个线程能持有【对象锁】
,其它线程
再想获取这个【对象锁】时就会阻塞
住.
synchronized锁是基于monitor锁实现的
例如这一段代码加锁之后进行反编译查看class字节码信息:
synchronized锁
修饰方法
和代码块
时底层实现上是一样的,但是在修饰方法时
,不需要JVM编译出的字节码完成加锁操作
,而synchronized在修饰代码块时
,是通过编译出来的字节码生成的monitorenter和monitorexit指令来实现的。
Monitor(监视器):结构包括三部分
Monitor 被翻译为监视器,是由jvm提供,c++语言实现
- Owner:
存储当前获取锁的线程的
,只能有一个线程可以获取 - EntryList:关联
没有抢到锁
的线程,处于Blocked状态的线程
- WaitSet:关联调用了
wait方法
的线程,处于Waiting状态的线程
可参考链接【Java并发】synchronized关键字的底层原理-锁的升级
10.谈谈 JMM(Java内存模型)
JMM(Java Memory Model)Java内存模型
,定义了共享内存
中多线程程序读写操作的行为规范
,通过这些规则来规范对内存的读写操作
从而保证指令的正确性
通俗的来说,就是
保证不同的线程对共享内存的值进行改变是的透明的
(也就是当两个线程同时拿到共享内存的一个变量,其中一个线程对变量进行了更改,那么此时就会和共享内存做一个同步操作,然后共享内存会对所有拿到这个变量的线程做出一个同步操作)
总结:
- JMM了共享内存中
多线程程序读写
操作的行为规范,保证读写指令的正确性 - JMM把内存分为两块,一块是私有
线程的工作区域(工作内存
),一块是所有线程的共享区域(主内存)
- 线程跟线程之间是
相互隔离
,线程跟线程交互需要通过主内存
11.什么是CAS?
CAS的全称是: Compare And Swap(比较再交换
),它体现的一种乐观锁
的思想,在无锁情况
下保证线程操作共享数据的原子性。
举例:【Java】CAS数据交换流程
CAS 底层实现
依赖于一个 Unsafe 类
来直接调用操作系统底层的 CAS 指令
乐观锁和悲观锁
- CAS
是基于乐观锁的思想
:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,改了反正要同步到主内存的,别的线程CAS失败可以进行自旋在拷贝一份主内存的共享变量数据,再执行自己的业务。 - synchronized
是基于悲观锁的思想
:最悲观的估计,得防着其它线程来修改共享变量
,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
总结:
- CAS的全称是: Compare And Swap(比较再交换);它体现的一种乐观锁的思想,在无锁状态下保证线程操作数据的原子性。
- CAS使用到的地方很多:AQS框架、AtomicXXX类
- 在操作共享变量的时候使用的自旋锁,效率上更高一些
- CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现
CAS缺点:
ABA问题
:
如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了
。
ABA问题的解决思路就是使用版本号。
在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
循环时间长开销大
:
如果CAS不成功,则会原地自旋,如果长时间自旋会给CPU带来非常大
且没必要的开销。
可以破坏掉for死循环,当超过一定时间或者一定次数时
,return退出。
- 只能
保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时
,循环CAS就无法保证操作的原子性
,这个时候就可以用锁
,或者有一个取巧的办法,就是把多个共享变量合并
成一个共享变量来操作。
12.请谈谈你对 volatile 的理解
一旦一个共享变量
(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:.
12.1.保证线程间的可见性
用 volatile 修饰共享变量,能够防止编译器等优化发生
,让一个线程对共享变量的修改
对另一个线程可见
例如下面的代码:
解决方案一(不推荐
):在程序运行的时候加入vm参数-Xint表示禁用即时编译器,不推荐,得不偿失(其他程序还要使用)
解决方案二:在修饰stop变量的时候加上volatile
,当前告诉 jit
,不要对 volatile 修饰的变量做优化
12.1.禁止进行指令重排序
用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障
,阻止其他读写操作越过屏障
,从而达到阻止重排序的效果
例如下面的代码在并发环境中,由于存在多个线程同时访问共享变量的情况,可能会导致可见性问题和指令重排
等影响程序正确性的行为。
解决办法就是在变量上添加volatile
,禁止指令重排序
,则可以解决问题
当然也不能随便加volatile关键字,要根据实际情况去加。如果都禁用了,指定效率肯定不高。
13.什么是AQS?
全称是 AbstractQueuedSynchronizer,即抽象队列同步器
。它是构建锁或者其他同步组件的基础框架
所谓抽象,其实目的就是把具体的逻辑交给子类去实现,这样就可以实现不同的特性的锁:
例如:AQS常见的实现类
ReentrantLock阻塞式锁
Semaphore信号量
CountDownLatch倒计时锁
AQS内部维护了一个先进先出的双向队列
,队列中存储的排队的线程
在AQS内部还有一个属性state
,这个state就相当于是一个资源
,默认是0
(无锁状态),如果队列中的有一个线程修改成功了state为1
,则当前线程就相等于获取了资源
在对state修改的时候使用的cas操作
,保证多个线程修改的情况下原子性
参考链接:【Java并发】什么是AQS?
14.ReentrantLock的实现原理
ReentrantLock表示支持重新进入的锁
,调用 lock 方 法获取了锁之后,再次调用 lock,是不会再阻塞
ReentrantLock主要利用CAS+AQS队列
来实现
支持公平锁和非公平锁
,在提供的构造器的中无参默认是非公平锁
,也可以传参设置为公平锁
构造方法接受一个可选的·
公平参数(默认非公平锁
),当设置为true时
,表示公平锁
,否则为非公平锁
。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
参考链接:【Java并发】ReentrantLock的实现原理
15.synchronized和Lock有什么区别 ?
语法层面
synchronized 是关键字
,源码在jvm 中,用 c++ 语言实现
Lock 是接口,源码由 jdk 提供,用 java 语言实现
(API)
使用 synchronized 时,退出同步代码块锁会自动释放
,而使用 Lock 时,需要手动调用 unlock 方法释放锁
功能层面
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
Lock 提供了许多 synchronized 不具备的功能,例如
公平锁(参考ReentrantLock)
可打断
可超时
也就是使用tryLock()时加入时间参数,如果超过这个时间拿不到锁,就自动放弃抢锁(放弃阻塞),反之在规定时间能抢到锁,那就正常执行自己的逻辑
多条件变量
Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock(读写锁)
性能层面
在没有竞争时
,synchronized 做了很多优化
,如偏向锁、轻量级锁,性能不赖
在竞争激烈时
,Lock 的实现通常会提供更好的性能
16.死锁产生的条件是什么?
产生的四个必要条件如下:(缺一不可)
互斥条件
:一个资源同一时间能且只能被一个线程访问;不可掠夺
:当资源被一个线程占用时,其他线程不可抢夺该资源;请求与等待
:当资源被一个线程占用时,其他线程只能等待资源的释放再拥有;循环等待
:指的是若干线程形成头尾相接的情况,将所有资源都占用导致的整体死锁或局部死锁。
一个线程需要同时获取多把锁,这时就容易发生死锁
前三条其实就是作为锁的条件,第四条(循环等待)就是造成死锁的主要原因
循环等待
也就是双方的锁都锁住了对方,并且都在等待对方的解锁,造成死循环(类似springbean的循环依赖)
如何进行死锁诊断?
- 命令查看(jps+jstack)
- jconsole工具
- VisualVM:故障处理工具
参考链接:【Java并发】如何进行死锁诊断?
17.聊一下ConcurrentHashMap
ConcurrentHashMap 是一种线程安全
的高效Map集合
底层数据结构:
- JDK1.7底层采用
分段数组+链表
实现 - JDK1.8 采用的数据结构跟HashMap1.8的结构一样,
数组+链表/红黑二叉树。
JDK1.7中ConcurrentHashMap:
采用分段数组+链表
实现
Segment数组的每一个元素都存储这一个HashEntry 数组的地址值,并且segment数组定义好了就不能扩容了。
向ConcurrentHashMap添加元素的流程
底层使用了ReentrantLock锁
保证并发下的线程安全。
但是这种方式效率并不高,每一次添加元素进去都要枷锁,解锁。效率不高
JDK1.8中ConcurrentHashMap
在JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表
- 采用 CAS + Synchronized来保证并发安全进行实现
- CAS
控制数组节点的添加
(CAS操作保证一个共享变量的原子操作
) - synchronized
只锁定当前链表
或红黑二叉树的首节点
,只要hash不冲突,就不会产生并发的问题 , 效率得到提升
总结:
1. 底层数据结构:
JDK1.7
底层采用分段的数组+链表
实现
JDK1.8
采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
2. 加锁的方式
JDK1.7采用Segment分段锁
,底层使用的是ReentrantLock
JDK1.8采用CAS添加新节点
,采用synchronized锁定链表
或红黑二叉树的首节点
,相对Segment分段锁粒度更细
,性能更好
18.导致并发程序出现问题的根本原因是什么
Java并发编程三大特性
1. 原子性(加锁)
一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行
-
可见性(共享变量加volatile关键字)
内存可见性:让一个线程对共享变量的修改对另一个线程可见
-
有序性(共享变量加volatile关键字)—
会在读、写共享变量时加入不同的屏障
指令重排:指令重排虽然在单个线程内保持了语义一致性,但在多线程环境下,并发读写操作的顺序可能被改变,从而引发问题。例如,在多线程中对共享变量进行读取和写入操作时,如果指令重排改变了读取和写入的顺序,并且没有适当的同步机制来保证顺序性,就可能导致线程读取到失效的数据或产生不符合预期的结果。
3.线程池
资源管理:线程池可以有效地管理系统中的线程资源。线程创建和销毁的开销比较大,如果在每个任务执行时都手动创建和销毁线程,会产生较高的开销。而线程池可以在程序启动时预先创建一定数量的线程,并对其进行复用,避免了线程频繁的创建和销毁,从而提高了系统性能和资源利用率。
控制线程数量:线程池可以限制同时执行的线程数量,可以通过设置线程池的大小来控制并发度。这样可以避免线程数量过多导致系统负载过重,以及线程数量过少导致资源浪费。线程池会自动管理线程的调度和执行,保证线程数量在合理范围内。
提高响应速度:线程池可以提高任务的响应速度。当有新的任务到达时,线程池中的空闲线程可以立即执行任务,而不需要等待新线程的创建。
这样可以降低任务的等待时间,提高整体的响应性能。
避免资源竞争:线程池可以避免由于资源竞争而引起的性能问题。线程池可以通过适当的同步机制来管理共享资源的访问,避免多个线程同时对共享资源进行修改而导致的竞争和冲突。
统一管理和监控:线程池可以提供统一的管理和监控接口,方便对线程的状态、执行情况、异常处理等进行统一管理和监控。可以通过线程池的API来获取线程池中线程的状态或取消执行中的任务等操作。
19.说一下线程池的核心参数(线程池的执行原理知道嘛)
- corePoolSize 核心线程数目
- maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)
- keepAliveTime 生存时间 - 救急线程的生存时间,
生存时间内没有新任务,此线程资源会释放
- unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
- workQueue - 当
没有空闲核心线程
时,新来任务会加入
到此队列排队
,队列满会创建救急线程执行任务
- threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
- handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
- 拒绝策略
1.AbortPolicy:直接抛出异常,
默认策略
;
2.CallerRunsPolicy:用调用者所在的线程来执行任务;(调用主线程来完成任务)
3.DiscardOldestPolicy:丢弃阻塞队列中靠最前(待在队列最久的任务)的任务,并执行当前任务;
4.DiscardPolicy:直接丢弃任务;
线程池线程执行流程:
20.线程池中有哪些常见的阻塞队列
workQueue - 当没有空闲核心线程时
,新来任务会加入到此队列排队
,队列满会创建救急线程执行任务
阻塞队列有四种:(1,2常用,3,4了解即可)
1.ArrayBlockingQueue:基于数组结构
的有界阻塞队列,FIFO(先进先出)。
2.LinkedBlockingQueue:基于链表结构
的有界阻塞队列,FIFO。
3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的
4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
ArrayBlockingQueue的LinkedBlockingQueue区别(其实也就是数组和链表的区别以及有界无界和出入队列锁的数量不同
)
注意:
- LinkedBlockingQueue建议一般设置有界(虽然也可以无界,但不推荐)
- LinkedBlockingQueue他的出队和入队是两把不同的锁,相比ArrayBlockingQueue一把锁。灵活度高
- LinkedBlockingQueue因为是链表,只有在任务加入到队列时才会创建node任务节点。而ArrayBlockingQueue在队列创建之初就初始化长度了。
21.如何确定核心线程数
CPU核数指的是物理核心的数量,而线程数包括物理核心和虚拟核心的总数。在多核处理器中,线程数通常等于核心数,而在带有超线程技术的处理器中,线程数可以大于核心数。
① 高并发、任务执行时间短
( CPU核数+1 ),减少线程上下文的切换
② 并发不高、任务执行时间长
IO密集型的任务 (CPU核数 * 2 + 1)(java程序通常是这种)
计算密集型任务 ( CPU核数+1 )
③ 并发高、业务执行时间长
,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)
22.线程池的种类有哪些
- newFixedThreadPool(
固定线程数
线程池):创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
适用于任务量已知,相对耗时的任务
无需救急,人人有份
,相当于KFC知道今天会有多少个顾客来点餐,那KFC事先备好餐品的数量。
- newSingleThreadExecutor(
单线程化
线程池):创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行
适用于按照顺序执行的任务
KFC只有一个窗口,
排队取餐
,来一个人点完单不做,先滚后面排队去,到你了才给你做餐
- newCachedThreadPool(
可缓存
线程池):创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
适合任务数比较密集,但每个任务执行时间较短的情况
KFC有0元领鸡腿活动,鸡腿做很快,来一个单先做好这个单的鸡腿,在接下一个单。并且做鸡腿的都是临时员工
- newScheduledThreadPool(“延迟”和“周期执行”):可以执行延迟任务的线程池,支持定时及周期性任务执行
相当于提前预定,到了预定时间就去做餐,或者接到单了,但是先不做,可以摸鱼晚点做
23.为什么不建议用Executors创建线程池
参考阿里开发手册《Java开发手册-嵩山版》
其实就是,使用Executors创建的线程池要么就是阻塞队列太长了,要么就是允许创建的线程数量最大化,都会导致堆积问题,导致堆内存溢出错误(OMM)
推荐使用(根据实际业务情况,定制化线程池)
使用 ThreadPoolExecutor
类来手动创建线程池
,并根据实际需求进行配置
,以更好地控制线程池的行为。通过自定义线程池的参数,例如核心线程数、最大线程数、队列容量和拒绝策略等,可以更好地适应不同的业务场景
,并避免上述潜在问题。