内容分类 | 详情 |
---|---|
Java高频面试题 | 汇总入口 |
JVM | JVM面试题 |
并发 | 并发面试题 |
Spring | Spring面试题 |
分布式 | 分布式面试题 |
SpringBoot | SpringBoot面试题 |
SpringCloud | SpringCloud面试题 |
Dubbo | Dubbo面试题 |
MySQL | MySQL面试题 |
Mybatis | Mybatis面试题 |
Redis | Redis面试题 |
RocketMQ | RocketMQ面试题 |
算法 | 算法面试题 |
遇到的问题 | 遇到的问题 |
面试官的其他问题 | 面试官的其他问题 |
Git | Git面试题 |
文章目录
线程的生命周期
Thread常用方法
start()
启动线程,线程状态由创建变为就绪。
String getName()
返回线程的名称
run()
线程在被调度时执行的操作
Thread currentThread()
返回当前线程 。在 Thread 子类中就是 this ,通常用于主线程和 Runnable 实现类
interrupt()
中断线程,由运行状态到死亡状态。interrupt() 方法只是改变中断状态而已,它不会中断一个正在运行的线程。
如果线程处于阻塞状态,调用interrupt方式,就会报错:
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.thread.InterruptTest$InterruptThread.run(InterruptTest.java:17)
at java.lang.Thread.run(Thread.java:748)
isAlive()
测试线程是否处于活动状态,线程调用 start 后,即处于活动状态
join(long millis)
主线程等待调用join方法的线程结束后,再继续执行主线程。
sleep(long millis)
睡眠指定时间,程序暂停运行,睡眠期间会让出 CPU 的执行权
yield()
暂停当前正在执行的线程对象,并执行其他线程。
Object 类中的常用 API
wait()
一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。而当前线程排队等候其他线程调用 notify() 或 notifyAll() 方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行.
notify()
一旦执行此方法,就会唤醒被 wait 的一个线程。如果有多个线程被 wait,就唤醒优先级高的那个。
notifyAll()
一旦执行此方法,就会唤醒所有被 wait 的线程。
锁的类型
从线程加锁时机分为:
- 乐观锁
乐观锁的思想与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过。 - 悲观锁:当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程去拿数据的时候就会阻塞。
从锁的公平性进行区分:
- 公平锁:指多个线程按照申请锁的顺序来获取锁。指多个线程按照申请锁的顺序来获取锁,简单来说 如果一个线程组里,能保证每个线程都能拿到锁
- 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
从线程根据锁是否重复获取:
- 可重入锁:是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
从多个线程能否获取同一把锁分为:
- 共享锁
- 独享锁
悲观锁
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。悲观锁的实现往往依靠数据库本身的锁功能实现。
举例:
Java 中的 Synchronized 和 ReentrantLock 排他锁也是一种悲观锁思想的实现。
mysql的select for update语句就是悲观锁,该语句是基于索引的,悲观锁的有效范围是begin和commit之间,session1执行了select for update后没有提交,session2再执行select for update时就会阻塞,mysql会返回锁表超时的提示。
乐观锁
它总认为资源和数据不会被别人所修改,所以读取不会上锁,乐观锁的实现方案一般来说有两种:版本号机制 和 CAS实现 。乐观锁多适用于多读的应用类型,这样可以提高吞吐量。
乐观锁实现机制:版本号机制和CAS机制
版本号机制:
线程A在修改时判断,当前版本号是否一致,如果一致说明从内存中获取到的数据是最新的,则线程A可以执行修改操作。
mysql中使用乐观锁,往往会在业务表中添加version字段,执行更新语句时加上version字段。
update test set num = 12,version = version + 1 where id = 1 and version = 1;
CAS机制:
在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
独享锁/共享锁
ReentrantLock,Synchronized,ReadWriteLock(其读锁是共享锁,其写锁是独享锁)
可重入锁
ReetrantLock,synchronized
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
如果synchronized不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。
公平锁/非公平锁
ReetrantLock默认是公平锁。
synchronized是非公平锁,由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
分段锁
JDK1.7的ConcurrentHashMap:数组+Segment+分段锁
Segment(分段锁):ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
JDK1.8的ConcurrentHashMap:synchronized 和 CAS 来操作
CAS的原理
由于CAS是CPU指令,我们只能通过JNI与操作系统交互,关于CAS的方法都在sun.misc包下Unsafe的类里,java.util.concurrent.atomic包下的原子类等通过CAS来实现原子操作。
全称是Compare And Swap
,即比较再交换,是实现并发应用到的一种技术
底层通过Unsafe
类实现原子性操作操作包含三个操作数 —— 内存地址(V)
、预期原值(A)
和新值(B)
。
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 ,若果在第一轮循环中,a
线程获取地址里面的值被b线程修改了,那么a
线程需要自旋,到下次循环才有可能机会执行。
CAS
这个是属于乐观锁,性能较悲观锁有很大的提高
AtomicXXX
等原子类底层就是CAS
实现,一定程度比synchonized
好,因为后者是悲观锁
底层调用C++
写的代码,直接请求CPU
调用
synchronized实现原理
JVM 是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。
具体实现是在编译之后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和异常处插入 monitor.exit 的指令。
当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(Lock Record)区域,同时将锁对象的对象头中 Mark Word 拷贝到锁记录中,再尝试使用 CAS 将 Mark Word 更新为指向锁记录的指针。
如果更新成功,当前线程就获得了锁。
如果更新失败 JVM 会先检查锁对象的 Mark Word 是否指向当前线程的锁记录。
如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。
不是则说明有其他线程抢占了锁,如果存在多个线程同时竞争一把锁,轻量锁就会膨胀为重量锁。
偏向锁->轻量级锁->重量级锁
javap命令对class文件进行反汇编,查看字节码指令如下:
synchronized锁升级
偏向锁
-
适用情况:一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
偏向锁的偏是指会偏向第一个获得锁的线程。 -
原理:当一个线程访问同步代码块并获取锁时,会通过CAS操作在Mark Word里存储锁偏向的线程ID。
-
优点:在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的锁执行操作。
轻量级锁
-
适用情况:当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,适用在多线程交替执行同步块的情况
-
原理:当前线程的栈帧中的创建LockRecod,将锁对象的MarkWord复制到LockRecod中,CAS操作尝试将对象的MarkWord更新为指向LockRecord的指针,如果这个更新动作成功了,那么这个线程就拥有了该对象的锁。
-
优点:在多线程交替执行同步块的情况下,用CAS进行加锁和解锁而不是直接用重量级锁,避免性能消耗
重量级锁
monitor锁
锁升级自己的一些理解:
偏向锁是一段同步代码一直被一个线程所访问,只需要第一次用CAS存储在Mark Word里存储锁偏向的线程ID,后续就直接判断这个线程ID在不在,不需要再使用CAS了。
轻量级锁就是多线程交替执行同步块的情况下,每次都是用CAS操作尝试将对象的MarkWord更新为指向LockRecord的指针,而不是使用重量级锁阻塞其他线程。
AQS 原理(AbstractQueuedSynchronizer)
AQS
的全称为(AbstractQueuedSynchronizer)
抽象队列同步器,这个抽象类在java.util.concurrent.locks
包下面。它是一个Java
提高的底层同步工具类,比如CountDownLatch
、ReentrantLock
,Semaphore
,ReentrantReadWriteLock
,SynchronousQueue
,FutureTask
等等皆是基于AQS
的
只要搞懂了AQS
,那么J.U.C
中绝大部分的api
都能轻松掌握
简单来说包含:
- 一个
int
类型的变量state
(用于计数器,类似gc
的回收计数器)表示同步状态,并提供了一系列的CAS
操作来管理这个同步状态对象;
AQS维护了一个变量state,使用volatile修饰保证其可见性,目的是为了让多线程可以知道此资源当前的访问状态。
/**
* The synchronization state.
*/
private volatile int state;
AQS维护了两个方法getState和setState,用来维护此状态,"1"表示已占用,"0"表示空闲。
- 一个是线程标记(当前线程是谁加锁的);
- 一个是阻塞队列(用于存放其他未拿到锁的线程);
例子:线程A
调用了lock()
方法,通过CAS
将state
赋值为1
,然后将该锁标记为线程A
加锁。如果线程A
还未释放锁时,线程B
来请求,会查询锁标记的状态,因为当前的锁标记为 线程A
,线程B
未能匹配上,所以线程B
会加入阻塞队列,直到线程A
触发了 unlock()
方法,这时线程B
才有机会去拿到锁,但是不一定肯定拿到
-
acquire(int arg)
源码讲解,好比加锁
lock
操作 -
tryAcquire()
尝试直接去获取资源,如果成功则直接返回,
AQS
里面未实现但没有定义成abstract
,因为独占模式下只用实现tryAcquire-tryRelease
,而共享模式下只用实现tryAcquireShared-tryReleaseShared
,类似设计模式里面的适配器模式 -
addWaiter()
根据不同模式将线程加入等待队列的尾部,有
Node.EXCLUSIVE
互斥模式、Node.SHARED
共享模式;如果队列不为空,则以通过compareAndSetTail
方法以CAS
将当前线程节点加入到等待队列的末尾。否则通过enq(node)
方法初始化一个等待队列
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
initializeSyncQueue();
}
}
}
-
acquireQueued()
使线程在等待队列中获取资源,一直获取到资源后才返回,如果在等待过程中被中断,则返回
true
,否则返回false
-
release(int arg)
源码讲解 好比解锁
unlock
独占模式下线程释放指定量的资源,里面是根据tryRelease()
的返回值来判断该线程是否已经完成释放掉资源了;在自义定同步器在实现时,如果已经彻底释放资源(state=0)
,要返回true
,否则返回false
-
unparkSuccessor(Node node)
方法用于唤醒等待队列中下一个线程
如果需要线程安全,且效率高的Map
,应该怎么做
多线程环境下可以用concurrent
包下的ConcurrentHashMap
, 或者使用Collections.synchronizedMap()
,
ConcurrentHashMap
虽然是线程安全,但是他的效率比Hashtable
要高很多
synchronized和ReentrantLock的区别
ReentrantLock
和synchronized
使用的场景是什么,实现机制有什么不同?
ReentrantLock
和synchronized
都是独占锁
synchronized
1、是悲观锁会引起其他线程阻塞,java
内置关键字,
2、无法判断是否获取锁的状态,锁可重入、不可中断、只能是非公平
3、加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单但显得不够灵活
4、一般并发场景使用足够、可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁
5、synchronized
操作的应该是对象头中mark word
,参考原先原理图片
ReentrantLock
1、是个Lock
接口的实现类,是悲观锁,
2、可以判断是否获取到锁,可重入、可判断、可公平可不公平
3、需要手动加锁和解锁,且 解锁的操作尽量要放在finally
代码块中,保证线程正确释放锁
4、在复杂的并发场景中使用在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。
5、创建的时候通过传进参数true
创建公平锁,如果传入的是false
或没传参数则创建的是非公平锁
6、锁机制是AQS
的state
和FIFO
队列来控制加锁
Redisson分布式锁实现原理
线程池的核心属性
-
threadFactory(线程工厂):用于创建工作线程的工厂。
-
corePoolSize(核心线程数):当线程池运行的线程少于 corePoolSize 时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态。
-
workQueue(队列):用于保留任务并移交给工作线程的阻塞队列。
-
maximumPoolSize(最大线程数):线程池允许开启的最大线程数。
-
handler(拒绝策略):往线程池添加任务时,将在下面两种情况触发拒绝策略:1)线程池运行状态不是 RUNNING;2)线程池已经达到最大线程数,并且阻塞队列已满时。
-
keepAliveTime(保持存活时间):如果线程池当前线程数超过 corePoolSize,则多余的线程空闲时间超过 keepAliveTime 时会被终止。
线程池的状态流转
RUNNING:接受新任务并处理排队的任务。
SHUTDOWN:不接受新任务,但处理排队的任务。
STOP:不接受新任务,不处理排队的任务,并中断正在进行的任务。
TIDYING:所有任务都已终止,workerCount 为零,线程转换到 TIDYING 状态将运行 terminated() 钩子方法。
TERMINATED:terminated() 已完成。
JDK中的线程池有哪些
线程池实现类 | 使用的阻塞任务队列 | 线程池特点 | 使用注意事项 |
---|---|---|---|
Executors.newFixedThreadPool(5); | LinkedBlockingQueue | 固定线程数量,任务队列容量为int最大值 | 任务队列可能导致OOM |
Executors.newCachedThreadPool(); | SynchronousQueue | 核心线程数量为int最大值,任务队列仅作转发 | 线程过多可能导致OOM |
Executors.newSingleThreadExecutor(); | LinkedBlockingQueue | 核心线程数为一,线程异常将会创建新的线程执行任务,可以保证任务执行的顺序 | 任务队列可能导致OOM |
Executors.newScheduledThreadPool(5); | DelayedWorkQueue | 指定频率执行任务,多个核心线程执行 | 任务队列可能导致OOM |
Executors.newSingleThreadScheduledExecutor(); | DelayedWorkQueue | 指定频率执行任务,单个核心线程执行 | 任务队列可能导致OOM |
为什么不用线程池的工具类创建线程
FixedThreadPool和SingleThreadExecutor => 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而引起OOM异常
CachedThreadPool => 允许创建的线程数为Integer.MAX_VALUE,可能会创建大量的线程,从而引起OOM异常
线程池有哪些队列
-
ArrayBlockingQueue:基于数组结构的有界阻塞队列,按先进先出对元素进行排序。
-
LinkedBlockingQueue:基于链表结构的有界/无界阻塞队列,按先进先出对元素进行排序,吞吐量通常高于 ArrayBlockingQueue。Executors.newFixedThreadPool 使用了该队列。
-
SynchronousQueue:不是一个真正的队列,而是一种在线程之间移交的机制。要将一个元素放入 SynchronousQueue 中,必须有另一个线程正在等待接受这个元素。如果没有线程等待,并且线程池的当前大小小于最大值,那么线程池将创建一个线程,否则根据拒绝策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是被放在队列中,然后由工作线程从队列中提取任务。只有当线程池是无界的或者可以拒绝任务时,该队列才有实际价值。Executors.newCachedThreadPool使用了该队列。
-
PriorityBlockingQueue:具有优先级的无界队列,按优先级对元素进行排序。元素的优先级是通过自然顺序或 Comparator 来定义的。