多线程面试题

1. 线程池中提交一个任务的流程是怎样的

文字

  1. 在使用execute()方法提交一个Runnable对象时
  2. 会先判断当前线程池中的线程数是否小于corePoolSize
  3. 如果小于,则创建新线程并执行Runnable
  4. 如果大于等于,则尝试将Runnable加入到workQueue中
  5. 如果workQueue没满,则将Runnable正常入队,等待执行
  6. 如果workQueue满了,则会入队失败,那么会尝试继续增加线程
  7. 如果当前线程池中的线程数是否小于maximumPoolSize
  8. 如果小于,则创建新线程并执行任务
  9. 如果大于等于,则执行拒绝策略,拒绝次Runnable

流程图

image-20250208211616064

代码

创建线程执行

1
2
3
4
5
6
7
8
9
10
@Test
void testRunnable() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 100, 200, TimeUnit.MINUTES, new LinkedBlockingDeque<>());
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("aaa");
}
});
}

进入execute方法中观察,如果传入的方法为null,则返回空指针异常,然后查看线程池中的线程数量是否小于核心线程数,如果小于核心线程数则调用addWorker方法将任务放到线程池中,如果大于核心线程数,则将其加入到工作队列中,如果加入成功等待执行,若没有入队成功,则使用addWorker加入线程,addWorker中会判断是否大于最大线程数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}

addWorker有两个参数,第一个是需要执行的任务,第二个是判断需要跟核心线程池大小比较还是最大线程池大小比较

2.线程池有几种状态?分别是如何变化的?

线程池一共有五种状态

状态 描述
RUNNING 会接受新任务并且会处理队列中的任务
SHUTDOWN 不会接收新任务但是会处理队列中的任务,任务执行完毕之后会中断所有的线程
STOP 不会接受新任务,并且不会执行队列中的任务
TIDYING 所有线程都停止之后,线程池状态就会转变成TIDYING,一旦达到此状态,就会调用线程池的terminated
TERMINATED terminated()执行完之后就会转变成TERMINATED

状态转换

RUNNING -> SHUTDOWN 执行shutdown方法

在状态转变的时候,先修改状态,执行队列中剩余的信息,防止在执行的时候又有新的任务进来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
executor.shutdown();
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 修改线程状态为SHUTDOWN
advanceRunState(SHUTDOWN);
// 将队列中的任务执行完毕
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
// 调用尝试关闭方法
tryTerminate();
}

RUNNING -> STOP 执行shutdownNow方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
executor.shutdownNow();
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 修改进程状态
advanceRunState(STOP);
// 中断执行操作
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}

SHOWDOWN -> TIDYING 当线程池中的剩余任务都执行完毕之后自动转变

在上面的shutdown方法执行过程中,有个tryTerminate方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
final void tryTerminate() {
for (;;) {
int c = ctl.get();
// isRunning(c):判断线程池是否正在运行
// runStateAtLeast(c, TIDYING):判断线程池状态是否至少为 TIDYING
// runStateLessThan(c, STOP) && ! workQueue.isEmpty():判断线程池状态小于 STOP 且工作队列不为空
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateLessThan(c, STOP) && ! workQueue.isEmpty()))
return;
// 如果工作线程数量不为 0
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}

final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 将线程池状态改为TIDYING状态
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
// 执行终结操作
terminated();
} finally {
// 将线程池状态转换成TERMINATED状态
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}

3. 如何停止一个线程

Thread线程拥有两个方法

  1. start()开启一个线程
  2. stop()关闭一个线程

但是stop方法比较粗暴,当调用stop方法的时候,会直接停掉线程,这样会导致我们不知道任务执行到了哪一步了,该释放的锁释放了没有

stop会自动释放synchronized锁,而不会自动释放ReentrantLock锁

synchronized 锁测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private static final Object lock = new Object();

@Test
void testStopThread() throws InterruptedException {
Thread thread = new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});

// 启动线程
thread.start();

Thread.sleep(3000);

// 终止线程
thread.stop();

// 判断线程中的锁是否释放
synchronized (lock) {
System.out.println("该锁已经解开");
}

}

输出结果是:

1
2
3
4
0
1
2
该锁已经解开

ReentrantLock测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Test
void testStopThread() throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
Thread thread = new Thread(() -> {
reentrantLock.lock();
for (int i = 0; i < 100; i++) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
reentrantLock.unlock();
});

// 启动线程
thread.start();

Thread.sleep(3000);

// 终止线程
thread.stop();

// 判断线程中的锁是否释放
reentrantLock.lock();
System.out.println("该锁已经解开");
reentrantLock.unlock();

}

结果:ReentrantLock一直没有释放

1
2
3
0
1
2

interrupt方法

通过interrupt方法告知线程,现在外部需要次线程进行停止工作,请结束工作并进行结束后的一些操作,然后终止线程

这种方式的优点是可以让线程内部拥有更加灵活的停用策略,可以有时间反应并做一些后续操作,并且将是否终止线程的决定权交给线程本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
void testStopThread() throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
if (Thread.currentThread().isInterrupted()) {
// 判断如果外部需要本线程停止的话,线程在做下一步操作
if (i > 300000) break; // 如果输出的i大于300000则可以终止,则直接终止操作
}
System.out.println(i);
}
});

// 启动线程
thread.start();

Thread.sleep(1000);

// 终止线程
thread.interrupt();

System.out.println("结束操作");

}

输出结果:可以得出当使用interrupt操作终止线程之后,线程不会立刻终止,而是等到输出到300000之后再进行终止

1
2
3
4
5
6
7
8
...
287223
287224
结束操作
287225
287226
...
300000

4.如何理解JAVA并发中的可见性

由于CPU和内存中的速度差异性,当线程A读取内存中的变量i的时候 读取到的是1,然后将变量i放到cpu1的内存中,此刻线程A对变量i进行操作,将变量的值修改为2,但是此刻并没有将内存中的变量i也同时修改。

同时线程B读取内存中的变量i,发现i还是1,这就会导致变量读取不一致。出现了可见性问题

image-20250209223324382

解决方法:在java中使用volatile关键字来保证变量的可见性,如果对变量使用volatile关键字,那么线程读取该变量的时候会直接从内存中读取,在修改改变量的时候,会同时修改内存中该变量的值和cpu高速换成中该变量的值,保证变量的一致性。

5.如何理解JAVA并发中的原子性

并发的原子性:在多线程并发操作的时候,一段代码要么完全执行成功要么完全不执行,不出现线程A执行一半被打断或者干扰的情况。换句话说,就是对同一个变量的多个操作能够像原子操作一样,保证多线程环境下的数据一致性,避免数据竞争和脏数据等问题。

出现原因

由于CPU,内存,IO(磁盘,网络)之间的性能差距,为了能够充分的利用CPU,当线程执行IO操作的时候,线程会让出CPU,让CPU去执行其他指令,并且本身来说,为了达到线程并发执行的效果,CPU也会按照固定的时间片来切换执行不同线程

例子

当多个线程去执行i++这个操作的时候,底层对应的就是三条指令

  1. 从内存中读取值
  2. 对i+1
  3. 写回i的值到CPU高速缓存中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static int counter = 0;
@Test
void testAtom() throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter++;
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter++;
}
});

// 启动两个线程
thread1.start();
thread2.start();
// 等待两个线程执行完毕
thread1.join();
thread2.join();
// 输出最终结果
System.out.println("最终变量值: " + counter);
}

最后结果

1
最终变量值: 1509

线程A在执行i++操作的时候,在读取到i的时候,切换到线程B,线程B也读取i,然后线程A和B都对i进行加一操作,然后线程AB都进行写入,最后i的值是2,而不是3

image-20250217212137850

解决方法

加锁或者使用原子变量(底层也是加锁)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private final static Object lock2 = new Object();
@Test
void testAtom() throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
synchronized (lock2) {
counter++;
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
synchronized (lock2) {
counter++;
}
}
});

// 启动两个线程
thread1.start();
thread2.start();
// 等待两个线程执行完毕
thread1.join();
thread2.join();
// 输出最终结果
System.out.println("最终变量值: " + counter);
}

最总输出结果

1
最终变量值: 2000

6.如何理解JAVA并发中的有序性

出现原因

JAVA编译的时候为了能够提高性能,会进行指令从新排列,这样就会导致程序在并发执行时出现有序性问题。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Person() {
static Person instance;

public Person getInstance() {
if (instance == null){
synchronized(Person.class) {
if (instance == null) {
instance = new Person();
}
}
}
}
}

new Person的操作在执行的时候被分为三步

  1. 申请内存空间
  2. 在内存空间中初始化Person对象相关内容
  3. 返回内存地址

编译优化之后:

  1. 申请内存空间
  2. 返回内存地址
  3. 在内存空间中初始化Person对象相关内容

线程A在调用getInstance方法的时候已经申请到内存地址并返回了,线程B在调用getInstance方法后发现instance不是null,就不会进行初始化,会直接返回,这时候instance还没有进行初始化,会导致,线程B后续操作出现问题。

解决方法

volatile关键字

volatile关键字,可以保证变量的可见性,同时一定程度上禁止执行重排序

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Person() {
static volatile Person instance;

public Person getInstance() {
if (instance == null){
synchronized(Person.class) {
if (instance == null) {
instance = new Person();
}
}
}
}
}

synchronized关键字

syncchronized关键字可以保证代码块在同一时刻只能被一个线程访问,从而保证代码块方法内执行的指令是有序的。

synchronized关键字,通过获取和释放锁来实现同步,在获取锁和释放锁的过程中,会插入内存屏障,进行指令重排序

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Person() {
static Person instance;

public synchronized Person getInstance() {
if (instance == null){
synchronized(Person.class) {
if (instance == null) {
instance = new Person();
}
}
}
}
}

Lock接口

同样是加锁,Lock相比于synchronized更加灵活,想要在哪加锁,在哪解锁都是由自己决定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Person() {
static Person instance;

private final Lock lock = new ReentrantLock();

public Person getInstance() {
lock.lock();
if (instance == null){
synchronized(Person.class) {
if (instance == null) {
instance = new Person();
}
}
}
lock.unlock();
}
}

7.如何避免死锁

出现原因

  1. 互斥:一个资源在同一时刻只能被一个进程使用
  2. 不可剥夺:一个资源在没有被线程使用完的时候,不能够被别的线程调用
  3. 请求和保持:一个线程在访问一个资源的时候,想要访问另一个资源,但是当先访问的资源并不释放
  4. 循环等待:若干线程,依次在等待资源,并且形成等待环

我们只需要将其中一个条件破坏掉,就可以打破死锁条件

但是前三个条件是锁的必要条件,所以我们可以破坏第四个条件

解决方法

顺序加锁

所有线程都按照同样的顺序加锁,例如,线程A先锁资源A,在锁资源B,线程B也应该和线程A同样的加锁顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Resource {
// 资源类
}

public class OrderLockExample {
private static final Resource resource1 = new Resource();
private static final Resource resource2 = new Resource();

public static void main(String[] args) {
// 线程 1 按顺序获取锁
Thread thread1 = new Thread(() -> {
synchronized (resource1) { // 加锁A
System.out.println("Thread 1 acquired resource1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) { // 加锁B
System.out.println("Thread 1 acquired resource2");
}
}
});

// 线程 2 按相同顺序获取锁
Thread thread2 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 2 acquired resource1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("Thread 2 acquired resource2");
}
}
});

thread1.start();
thread2.start();
}
}

限时加锁

对每个锁进行超时时间,如果超时还没有获取到锁,就会返回false,这样可以避免线程无限期的等待锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class TimeoutLockExample {
private static final ReentrantLock lock1 = new ReentrantLock();
private static final ReentrantLock lock2 = new ReentrantLock();

public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
if (lock1.tryLock(1, TimeUnit.SECONDS)) { //加锁A,设置超时时间 一秒
System.out.println("Thread 1 acquired lock1");
try {
if (lock2.tryLock(1, TimeUnit.SECONDS)) { // 加锁B,设置超时时间一秒
System.out.println("Thread 1 acquired lock2");
// 执行操作
lock2.unlock();
}
} finally {
lock1.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 线程2和线程1加锁的顺序一样
Thread thread2 = new Thread(() -> {
try {
if (lock2.tryLock(1, TimeUnit.SECONDS)) {
System.out.println("Thread 2 acquired lock2");
try {
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
System.out.println("Thread 2 acquired lock1");
// 执行操作
lock1.unlock();
}
} finally {
lock2.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});

thread1.start();
thread2.start();
}
}

减少锁的持有时间

减少线程持有锁的时间,可以降低其他线程等待锁的时间,从而减少死锁的可能性。可以将不需要加锁的代码放到同步快之外执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ReduceLockHoldTimeExample {
private static final Object lock = new Object();

public static void main(String[] args) {
Thread thread = new Thread(() -> {
// 不需要加锁的代码
System.out.println("Do some work without lock");

synchronized (lock) {
// 需要加锁的代码
System.out.println("Acquired lock");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Released lock");
}

// 不需要加锁的代码
System.out.println("Do some more work without lock");
});

thread.start();
}
}

检测是否含有死锁

可以通过定期检测系统中是否存在死锁,如果发现死锁,则采取相应的恢复措施,如终止某些线程或释放某些锁。Java 中可以使用 ThreadMXBean 来检测死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class DeadlockDetectionExample {
public static void main(String[] args) {
// 初始化ThreadMxBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 获取所有的锁
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
// threadMxBean通过getThreadInfo来判断是否有死锁
ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("Deadlocked thread: " + threadInfo.getThreadName());
}
}
}
}

8.HashMap扩容原理

1.7版本之前

  1. 先生成新数组
  2. 遍历老数组每个位置上的链表上的每个元素
  3. 取每个元素的的key,并基于新数组的长度计算出在新数组下的下标
  4. 将元素添加到新数组中去
  5. 将所有的元素转移完之后,将新数组赋值给HashMap对象的table属性

1.8版本之后

1.8之后HashMap的底层结构和1.7不同

  1. 先生成新数组
  2. 遍历老数组每个位置上的链表或红黑树
  3. 如果时链表则直接计算链表中的每个数组的下标,并添加到新数组中
  4. 如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置
    1. 统计每个下标位置的元素个数
    2. 如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置
    3. 如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置
  5. 将所有的元素转移完之后,将新数组赋值给HashMap对象的table属性

9.JDK1.7到JDK1.8HashMap发生了什么变化

  1. 1.7中底层是数组加链表,1.8中底层是数组+链表+红黑树,加入红黑树的目的是提高hashmap插入和查询的整体效率
  2. 1.7中链表插入使用的头插法,1.8链表插入的是尾插法,因为1.8中插入key和value的时候需要判断链表元素的个数,所以正好遍历链表,因此采用尾插法
  3. 1.7中HashMap中的哈希算法比较复杂,存在各种右移与异或运算,1.8中进行了简化,因为复杂的哈希算法目的就是提高散列性,来提拱HashMap的整体效率,而1.8中新增加了红黑树,所以可以适当简化哈希算法,节省CPU资源

10.HashMap的Put方法

流程:

  1. 根据key通过哈希算法与运算得出数组下标
  2. 如果数组下标位置为空,则将key和value封装为entity对象(jdk1.7是entity对象,jdk1.8中是node对象)并放入该位置
  3. 如果数组下标不为空,则需要分情况讨论
    1. jdk1.7,先判断是否需要扩容,如果需要扩容则进行扩容,如果不需要扩容则生成entity对象,并使用头插法插入到当前位置的链表中
    2. 如果是1.8则先会判断当前位置的node类型,看是红黑树还是链表
      1. 如果是红黑树,则将key和value封装称为一个node转入到红黑树中,在这个过程中判断红黑树是否存在当前key,如果存在则进行更新value
      2. 如果此位置上是链表,则将key和value封装成一个node对象使用尾插法插入到链表尾部,使用尾插法需要遍历链表,判断链表上是否有该key,如果有则进行更新,遍历完之后没有,则插入到尾部,插入到链表中会看当前链表的节点数,如果节点数大于等于8,则将链表转换成红黑树。
      3. 将node插入到链表或者红黑树中,在判断是否需要扩容,如果需要就扩容,不需要就结束put方法

11.ConcurrentHashMap的扩容机制

1.7 版本之前

1.7版本之前的ConcurrentHashMap是基于Segment分段实现的

1.7之前ConcurrentHashMap会将HashMap分为一个个的segment段,每一个段使用ReentrantLock锁来加锁和解锁,保证线程的安全性

每一个Segment是一个小型的HashMap

每个Segment都会进行扩容,和HashMap的扩容机制类似,都是实现二倍扩容

先生成新数组,然后转移元素到新数组中,是否扩容也是每一个Segment分段内部单独实现的,判断是否超过阈值

1.8版本之后

ConcurrentHashMap就废弃了Segment分段操作,改为正常的HashMap了,每一个HashMap的数组块都是一个桶采用 CAS(Compare-And-Swap)和 synchronized 来解决并发问题。它使用数组 + 链表 + 红黑树的数据结构,通过对每个桶节点加锁来实现并发控制。当插入的时候当前的位置没有元素,则直接插入,如果有元素的话则通过synchronized来进行加锁,每个线程访问不同的桶,可以加不同的锁,能够提高访问效率

扩容流程,当一个线程想要向concurrentHashMap中添加元素的时候,发现当前需要扩容,则当前线程暂时放弃添加元素,去帮助扩容,在扩容期间如果有新的添加元素的线程到达,则同样会放弃当前的添加操作进行扩容。

如果添加的时候没有发现需要扩容,则直接添加,添加完毕之后,在进行判断,超过了阈值则进行扩容

concurrentHashMap是支持多线程同时扩容的

扩容前也是先生成一个新数组

在转移元素之前,先将原数组分组,将每一个组分配给不同的线程来进行元素转移,每一个线程负责一组或者多组元素转移工作

12.ReentrantLock中的tryLock和lock方法的区别

  1. tryLock表示尝试加锁,可能加锁成功,也可能加锁失败,但是该方法不会阻塞当前线程,加锁成功返回true,加锁失败返回false可以通过返回值来判断是否需要进行下一步操作
  2. lock会阻塞加锁,如果加锁成功则进行下一步操作,如果加锁失败则会一直阻塞等待加锁,没有返回值

13.ReentrantLock中公平锁和非公平锁底层实现

不管是公平锁还是非公平锁,他们底层实现都是会使用AQS来进行排队,他们的区别在于:当线程使用lock()方式加锁的时候,

  • 如果是公平锁,会先检查AQS中是否有对象在排队,如果有排队的情况,则自己也会进入排队状态,如果没有排队情况则会去竞争锁
  • 如果是非公平锁,会先去竞争锁,如果竞争成功则进行下一步操作,如果没有竞争成功,则会去排队

公平锁和非公平锁区别只是在先排队还是先竞争锁,锁释放的时候都是从队列中唤醒第一个线程

14.sychronized的偏向锁,轻量级锁和重量级锁

偏向锁

在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果再次来之后可以直接获取到这个锁

轻量级锁

轻量级锁由偏向锁升级来,当一个线程获取到一个锁的时候,此时该锁是偏向锁,等到有第二个线程来竞争锁,偏向锁就会升级为轻量锁,之所以为轻量锁,是为了和重量级锁分开,轻量级锁的底层是通过让等待线程自旋等待来实现的,不会调用操作系统底层让线程停止工作阻塞。但是从用户角度看,线程都是被阻塞了

重量级锁

当一个线程自旋次数过多也没有获取到锁,则会升级为重量级锁,重量级锁会调用操作系统底层来使线程阻塞

自旋锁

自旋锁就是在线程获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒都是交到操作系统层面去做的,比较耗费时间,自旋锁是线程CAS获取预期的一个标志,如果没有获取到则继续循环获取,如果获取到了则表示获取到了锁,这个过程中线程一直在运行,相对而言没有耗费太多的操作系统资源,比较轻量

15.sychronized和ReentrantLock的区别

  1. sychronized是一个关键字而ReentrantLock是一个类
  2. sychronized会自动加锁和解锁,RenntranLock需要程序员手动的去加锁和解锁
  3. sychronized的底层是jvm层面的锁,RenntranLock是API层米的锁
  4. sychronized是非公平锁,RenntranLock可以选择公平和非公平锁
  5. sychronized锁的是对象,锁信息保存在对象头中,ReentranLock通过代码中的int类型state标识来标识锁的状态
  6. sychronized底层是一个锁升级的过程,偏向锁->轻量锁->重量锁

16.ThreadLocal底层原理

ThreadLocal是JAVA中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程的内部,该线程可以在任意时刻,任意方法中获取缓存数据。

ThreadLocal底层是通过ThreadLocalMap来实现的,每一个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值。

如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象是用完之后,应该要把设置的key,value,也就是entity对象进行回收,但是线程池中的线程不会进行回收,而线程池中的对象是通过强引用指向的ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entity对象,线程不会回收,Entity对象也不会回收,从而出现内存泄漏,解决方法,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象

ThreadLocal经典使用场景就是连接管理(一个线程持有一个链接,该链接对象可以在不同的方法中进行传递,线程之间不共享一个链接)

17.并发,并行,串行之间的区别

串行:一个任务执行完毕,在执行下一个任务,排着队依次来

并行:多个任务同时进行

并发:宏观上来看是多个任务同时进行,微观来看就是多个任务交替执行

18.对守护线程的理解

线程分为用户线程和守护线程,用户线程就是普通的线程,守护线程就是jvm的后台线程,比如说垃圾回收线程就是一个守护线程,守护线程会在其他的普通线程都停止运行之后自动关闭,我们可以通过thread.setDaemon(true)来把一个线程设置为守护线程

19.说说对线程安全的理解

线程安全指定是:我们在写某段代码的时候,再多个线程同时执行这段代码的时候,不会产生混乱,依然能够得到正确的结果,比如说i++,i初始话为0,那么两个线程来同时执行这个代码,如果代码是线程安全的,那么两个线程会一个得到1,一个得到2,如果两个线程的结果都是1,则表明这段代码是线程不安全的。

所以线程安全,主要指的是一段代码在多个线程同时执行的情况下,能够得到正确的结果

20.谈一下对AQS的理解,AQS如何实现可重入锁

AQS是一个JAVA线程同步框架,是JDK很多锁工具的核心实现框架

在AQS中维护了一个信号量state和一个线程组成的双向链表队列(先进先出),其中这个线程队列,就是用来给线程排队的

  • 同步状态:AQS 使用一个volatile int类型的变量state来表示同步状态。volatile关键字保证了该变量在多线程环境下的可见性。通过getState()setState(int newState)compareAndSetState(int expect, int update)方法可以对同步状态进行操作,其中compareAndSetState是基于 CAS(Compare-And-Swap)操作实现的,能保证原子性地更新同步状态。
  • CLH 队列:AQS 内部维护了一个 FIFO(先进先出)的双向队列,该队列基于 CLH(Craig, Landin, and Hagersten)锁队列的变体实现。当多个线程竞争同步状态时,获取失败的线程会被封装成一个Node节点加入到这个队列中进行排队等待。

可重入锁

**可重入锁:**可重入锁是指同一个线程在外层方法获取到指定锁的时候,如果该线程内部方法也需要获取该锁,则可以直接获取,不会造成阻塞。这种锁允许线程多次进入该锁保护的代码块。

特点:

  1. 线程可重复获取锁:同一线程可以多次获取同一把锁,避免了死锁的发生,例如在一个线程调用一个方法的时候已经获取了一个锁,在该方法内部有调用了另一个需要获取相同锁的方法,此时线程可以直接进入,不会因为锁已经被自己持有而阻塞
  2. 锁的计数机制:可重入锁通常有一个计数器来记录锁被获取的次数,每次线程获取锁的时候,数量加一,线程释放锁的时候数量减一,只有当计数器为0的时候锁才能够被真正的释放,其他线程才能够获取到该锁

JAVA中的实现

  1. sychronized关键字

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class SynchronizedReentrantExample {
    public synchronized void outerMethod() {
    System.out.println("进入外层方法");
    innerMethod();
    System.out.println("离开外层方法");
    }

    public synchronized void innerMethod() {
    System.out.println("进入内层方法");
    }

    public static void main(String[] args) {
    SynchronizedReentrantExample example = new SynchronizedReentrantExample();
    example.outerMethod();
    }
    }

    在上述代码中,outerMethodinnerMethod 都使用了 synchronized 关键字。当线程调用 outerMethod 时,它会获取对象的锁。在 outerMethod 内部调用 innerMethod 时,由于 innerMethod 也需要同一把锁,而该线程已经持有了这把锁,所以可以直接进入 innerMethod,不会被阻塞。

  2. ReentrantLock

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    import java.util.concurrent.locks.ReentrantLock;

    public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void outerMethod() {
    lock.lock();
    try {
    System.out.println("进入外层方法");
    innerMethod();
    System.out.println("离开外层方法");
    } finally {
    lock.unlock();
    }
    }

    public void innerMethod() {
    lock.lock();
    try {
    System.out.println("进入内层方法");
    } finally {
    lock.unlock();
    }
    }

    public static void main(String[] args) {
    ReentrantLockExample example = new ReentrantLockExample();
    example.outerMethod();
    }
    }

    在这个示例中,ReentrantLock 被用于保护代码块。线程在调用 outerMethod 时获取锁,在 outerMethod 内部调用 innerMethod 时再次获取同一把锁,由于 ReentrantLock 是可重入的,线程可以正常进入 innerMethod。在每个方法结束时,需要在 finally 块中调用 unlock 方法来释放锁,以确保锁最终会被释放。

AQS如何实现可重入锁

AQS 借助一个 volatile int 类型的 state 变量来表示同步状态。对于可重入锁,state 用于记录锁被当前线程持有的次数:

  • state 为 0 时,意味着锁未被任何线程持有。
  • 当线程首次获取锁时,state 会被设置为 1,并且记录下持有该锁的线程。
  • 若持有锁的线程再次获取锁,state 会递增,例如变为 2、3 等,这体现了锁的可重入特性。
  • 每次线程释放锁时,state 会递减。当 state 减为 0 时,表明锁已被完全释放,其他线程就有机会获取该锁。

21.线程池的底层工作原理

线程池的内部是通过队列+线程来实现的,当我们再利用线程池执行任务的时候

  1. 如果此时的线程池中的线程数量小于corePoolSize,即、即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务
  2. 如果线程池中的线程数量等于corePoolSize,但是缓冲队列没有满,那么任务放到缓冲队列中
  3. 如果线程池中的线程数量大于corePoolSize缓冲队列workQueue满并且线程池中线程个数小于maximunPoolSize,建立新的线程来处理被添加的任务
  4. 如果线程池中的线程数量大于corePoolSize缓冲队列workQueue满并且线程池中线程个数等于maximunPoolSize,执行拒绝策略来处理此任务
  5. 当线程池中的线程数量大于corePoolSize,如果某空闲时间超过keepAliveTime,则线程被终止,线程池可动态的调整线程池中的线程数

22.线程池为什么是先添加队列而不是先创建最大线程

核心思想:尽量想要通过最小的线程数解决最多的问题,让每一个线程的利用率都达到最大

当线程池中的核心线程数都在忙的时候,如果继续往线程池中添加任务,那么任务会先放入队列,队列满了之后才会新开线程。

例子:一个公司有10个程序员,本来这十个程序员都能正常的处理各种请求,但是随着公司的发展,需求在慢慢增加,但是一开始这些需求只会增加在待开发列表中,然后这10个程序员加班加点从待开发队列中获取请求并进行处理,但是某一天待开发列表满了,公司发现10个程序员真的处理不过来了,所以就需要开始招聘新的员工

23.线程之间是如何进行通讯的

两种通讯方式:

  1. 基于共享内存来进行通讯
  2. 基于网络进行通讯

共享内存通讯

**共享变量:**多个线程可以访问同一个共享变量,通过对共享变量的读写来实现数据的传递与交互,为了保证数据的一致性和可见性通常需要使用volatile关键字或者同步机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SharedVariableCommunication {
// 使用volatile关键字保证变量的可见性
private static volatile int sharedData = 0;

public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
// 对共享变量进行写操作
sharedData = 10;
});

Thread thread2 = new Thread(() -> {
// 读取共享变量的值
System.out.println("Shared data value: " + sharedData);
});

thread1.start();
thread2.start();
}
}

**协作机制:**通过java中的wait和notify和notifyAll方法,用于实现线程时间的协作,wait让当前线程等待,直到其他线程使用notify或者notifyAll来唤醒它,notify方法随机唤醒一个等待的线程,notifyAll唤醒所有线程,通常在synchronized块中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ThreadCommunication {
private static final Object lock = new Object();
private static int data;

public static void main(String[] args) {
Thread producer = new Thread(() -> {
synchronized (lock) {
data = 42;
// 通知消费者线程数据已准备好
lock.notify();
}
});

Thread consumer = new Thread(() -> {
synchronized (lock) {
while (data == 0) {
try {
// 等待生产者线程通知
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Consumed data: " + data);
}
});

producer.start();
consumer.start();
}
}

使用Future和Callable:Callable是一个泛型接口,它的call()方法返回一个结果。Future表示一个异步的操作,可以通过Future获取Callable任务的执行结果,ExecutorService可以提交Callable任务,并返回一个Future对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class FutureCommunication {
public static void main(String[] args) throws Exception {
// 创建一个线程池
ExecutorService executor = Executors.newSingleThreadExecutor();

// 创建一个Callable任务
java.util.concurrent.Callable<Integer> callable = () -> {
// 模拟耗时操作
Thread.sleep(2000);
return 42;
};

// 提交任务并获取Future对象
Future<Integer> future = executor.submit(callable);

// 获取任务的执行结果
System.out.println("Result: " + future.get());

// 关闭线程池
executor.shutdown();
}
}

24.提交任务的时候,线程队列已满,这时候会发生什么

  1. 如果使用无界队列,可以继续提交任务没有关系
  2. 如果使用有界队列,则需看线程池中的线程数量有没有达到最大线程数,如果没有达到则可以创建新线程,如果达到最大线程数则需要使用拒绝策略

25.FixedThreadPool用的阻塞队列是什么

FixedThreadPool,代表定长线程池,底层使用的LinkedBlockingQueue,表示无界的阻塞队列。

26.volatile关键字是如何保证可见性,有序性的

使用volatile修饰的关键字,会在修改的时候直接修改主内存中的数据,并且在读取的时候也是直接从主内存中读取,从而保证了可见性

volatile修饰的成员变量进行读写的时候,会插入内存屏障,而内存屏障可以达到禁止重排序的效果,从而保证有序性