多线程 面试题
多线程 面试题
线程有那些状态
Java 多线程状态分为六种
操作系统层面有五种状态
线程池的核心参数
ThreadPoolExecutor
构造函数参数
corePoolSize
: 核心线程数目- 最多保留的线程数
maximumPoolSize
: 最大线程数目- 核心线程 + 救急线程
keepAliveTime
: 生存时间- 针对救急线程
unit
:keepAliveTime
参数的时间单位。workQueue
: 一个阻塞队列,用于存储等待执行的任务。unit
:时间单位- 针对救急线程
workQueue
- 阻塞队列
threadFactory
:线程工厂- 为线程在创建时,创建名称
handler
: 拒绝策略- 四种
sleep 和 wait 方法区别
- 来源:
sleep
方法定义在java.lang.Thread
类中。wait
方法定义在java.lang.Object
类中,因此它是所有 Java 对象的成员方法。
- 目的:
sleep
用于使当前正在执行的线程暂停执行指定的时间,让出 CPU 给其他线程,但不释放对象锁。wait
用于在其他线程调用同一个对象的notify()
或notifyAll()
方法之前,使当前线程暂停执行,并且释放对象锁。
- 中断响应:
sleep
方法在指定时间结束后,线程继续执行,不会抛出InterruptedException
。wait
方法可以在等待过程中被中断,如果当前线程在wait
期间被中断,它会抛出InterruptedException
。
- 锁:
- 在调用
sleep
方法期间,当前线程不会释放任何锁。 - 在调用
wait
方法时,当前线程必须拥有对象的锁,并且会释放这个锁,进入等待状态,直到其他线程调用notify()
或notifyAll()
。
- 在调用
- 使用场景:
sleep
通常用于控制程序的执行时间间隔,例如在循环中暂停执行一段时间。wait
通常用于线程间的协调,特别是在生产者-消费者模型中,消费者可能需要等待生产者生产出产品。
- 返回值:
sleep
方法没有返回值,它接受一个表示时间的参数。wait
方法没有参数,但它可以在调用时指定一个超时时间,并且可以在等待过程中被中断。
总结:sleep
主要用于简单的时间延迟,而 wait
是用于线程间的同步,它涉及到更复杂的线程通信和锁的释放与获取。在使用 wait
方法时,通常需要与 synchronized
块一起使用,以确保线程安全。
Lock 和 synchronronized
Lock
和synchronized
都是 Java 并发编程中用于线程同步的机制,但它们在设计和使用上存在一些显著的区别
- 语法方面
synchronronized
是关键字,源码在 JVM 中,用 C++ 语言实现Lock
是接口,源码由 JDK 提供,同 Java 语法实现- 使用
synchronronized
时,退出同步代码块锁会自动释放,而使用Lock
时,需要手动调用unlock()
方法释放锁- 功能方面
- 二者均属于悲观锁,都具备基本的互斥、同步、锁重入功能
Lock
提供了许多synchronronized
不具备的功能,例:获取等待状态、公平锁、可打断、可超时、多条件变量Lock
适合不不同场景实现、如:ReentrantLock
、ReentrantReadWriteLock
- 性能方面
- 在没有竞争时,
synchronronized
做了很多优化,如:偏向锁、轻量级锁、性能不赖- 在竞争激烈,
Lock
的实现通常会提供更好的性能
以下是 Lock
和 synchronized
的详细对比:
- 使用方式
- synchronized:
- 可以用于修饰方法或代码块。
- 使用简单,只需在方法或代码块前加上
synchronized
关键字。
- Lock:
- 是
java.util.concurrent.locks
包中的一个接口。 - 使用时需要先实例化一个锁对象,然后调用
lock()
方法获取锁,unlock()
方法释放锁。
- 是
- 锁的获取方式
- synchronized:
- 进入同步代码块或方法时,自动获取锁。
- 离开同步代码块或方法时,自动释放锁。
- Lock:
- 需要显式调用
lock()
方法获取锁。 - 需要显式调用
unlock()
方法释放锁。
- 需要显式调用
- 响应中断
- synchronized:
- 锁的获取过程中,如果线程被中断,不会抛出
InterruptedException
。
- 锁的获取过程中,如果线程被中断,不会抛出
- Lock:
- 可以使用
lockInterruptibly()
方法,该方法可以在线程被中断时立即响应,并抛出InterruptedException
。
- 可以使用
- 尝试非阻塞获取锁
- synchronized:
- 不支持尝试非阻塞获取锁。
- Lock:
- 支持通过
tryLock()
方法尝试非阻塞获取锁。
- 支持通过
- 超时获取锁
- synchronized:
- 不支持超时获取锁。
- Lock:
- 支持通过
tryLock(long timeout, TimeUnit unit)
方法在指定时间内尝试获取锁。
- 支持通过
- 可重入性
- synchronized 和 Lock 都支持可重入性,即同一个线程可以多次获取同一个锁。
- 公平性(Fairness)
- synchronized:
- 不支持设置公平性,锁的获取顺序不保证公平。
- Lock:
- 例如
ReentrantLock
可以设置公平性(true
或false
),公平性锁可以按照线程等待的顺序来分配锁。
- 例如
- 条件变量
- synchronized:
- 使用
wait()
,notify()
,notifyAll()
方法实现条件变量。
- 使用
- Lock:
- 提供了更丰富的条件变量支持,通过
newCondition()
方法创建Condition
对象,可以有更复杂的线程间协调。
- 提供了更丰富的条件变量支持,通过
- 锁状态检查
- synchronized:
- 没有提供检查锁状态的方法。
- Lock:
- 提供了
isLocked()
,isHeldByCurrentThread()
,hasQueuedThreads()
等方法,可以检查锁的状态。
- 提供了
- 示例代码
- synchronized 示例:
1 2 3 4 5 6 7 8 9
public class Counter { private int count = 0; public void increment() { synchronized (this) { count++; } } }
- Lock 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Counter { private final Lock lock = new ReentrantLock(); private int count = 0; public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } }
总结
Lock
提供了比 synchronized
更丰富的控制能力和灵活性,特别是在处理复杂同步场景时。然而,synchronized
在简单场景下更加简洁和方便。选择使用哪种机制取决于具体的应用场景和性能要求。
公平锁和非公平锁
公平锁(Fair Lock)和非公平锁(Unfair Lock)是指锁的获取方式是否考虑了获取锁的顺序。在 Java 的
ReentrantLock
类中,可以通过构造函数设置锁的公平性。
以下是公平锁和非公平锁的详细对比:
公平锁(Fair Lock)
- 定义:公平锁是指多个线程按照请求锁的顺序去获取锁。线程获取锁的顺序是公平的,即先到的线程先获得锁。
- 优点:
- 避免饥饿现象,即线程长时间无法获取到锁。
- 适用于需要确保任务按顺序执行的场景。
- 缺点:
- 吞吐量可能较低,因为需要维护一个队列来保证公平性。
- 可能导致线程频繁的上下文切换,影响性能。
- 实现:在
ReentrantLock
的构造函数中设置true
来创建公平锁。1
Lock fairLock = new ReentrantLock(true);
非公平锁(Unfair Lock)
- 定义:非公平锁是指在获取锁时不考虑线程请求的顺序。线程可能随时抢占锁,不论其他线程等待的时间长短。
- 优点:
- 吞吐量可能较高,因为省去了维护等待队列的开销。
- 性能可能更好,因为减少了线程调度和上下文切换。
- 缺点:
- 可能导致线程饥饿,即某些线程长时间无法获取到锁。
- 可能导致线程饥饿现象,尤其是在高负载的情况下。
- 实现:在
ReentrantLock
的构造函数中设置false
或者不设置(默认为false
)来创建非公平锁。1 2 3
Lock unfairLock = new ReentrantLock(); // 默认非公平锁 // 或者 Lock unfairLock = new ReentrantLock(false);
选择公平锁还是非公平锁:
- 如果你的应用程序中线程需要按照请求顺序公平地访问资源,那么公平锁是一个好选择。
- 如果性能是关键考虑因素,并且锁竞争不激烈,或者你可以接受某些线程可能会饿死的风险,那么非公平锁可能更合适。
示例代码
以下是使用 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
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
public static void main(String[] args) {
// 创建公平锁
Lock fairLock = new ReentrantLock(true);
// 创建非公平锁
Lock unfairLock = new ReentrantLock();
// 使用公平锁
fairLock.lock();
try {
// 访问共享资源
} finally {
fairLock.unlock();
}
// 使用非公平锁
unfairLock.lock();
try {
// 访问共享资源
} finally {
unfairLock.unlock();
}
}
}
在实际应用中,选择哪种类型的锁取决于具体的应用场景和性能要求。公平锁提供了更好的公平性保证,而非公平锁则可能提供更高的吞吐量。
Lock条件变量
在 Java 中,条件变量允许线程在某些条件不满足时挂起(等待),并在条件变为满足时被唤醒。
Lock
接口提供了条件变量的支持,通过Condition
接口实现
以下是使用 Lock
和条件变量的详解:
条件变量(Condition)
- 定义:条件变量是一种同步辅助工具,用于线程间的协调,允许一个或多个线程等待某个条件变为真,而其他线程在适当的时候发出信号通知等待的线程。
使用条件变量的步骤:
- 获取锁:在操作条件变量之前,必须先获取关联的
Lock
。 - 等待条件:使用
Condition
的await()
方法使当前线程等待,直到它被其他线程通过signal()
或signalAll()
唤醒。 - 检查条件:在
await()
方法返回后,再次检查条件是否满足,因为await()
方法可能因为InterruptedException
或其他原因而返回。 - 释放锁:在等待条件变量之前,确保释放锁,以便其他线程可以进入并可能改变条件状态。在
await()
方法调用后,当前线程会自动重新获取锁。
示例:
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
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionVariableExample {
private int signalCount = 0;
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void waitForSignal() throws InterruptedException {
lock.lock();
try {
while (signalCount < 1) { // 等待条件满足
condition.await();
}
// 处理信号
} finally {
lock.unlock();
}
}
public void sendSignal() {
lock.lock();
try {
signalCount++; // 更改条件
condition.signalAll(); // 唤醒所有等待的线程
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ConditionVariableExample example = new ConditionVariableExample();
Thread waitingThread = new Thread(() -> example.waitForSignal());
waitingThread.start();
// 给等待线程发送信号
Thread.sleep(1000); // 模拟一些工作
example.sendSignal();
// 等待等待线程完成
waitingThread.join();
}
}
条件变量与 wait()
和 notify()
wait()
和notify()
方法是Object
类的成员,它们也用于线程间的协调。但是,它们与synchronized
关键字一起使用,并且没有提供Lock
那样的灵活性。- 使用
wait()
时,线程必须先获得对象的synchronized
锁,然后调用wait()
来等待。线程会在释放锁后进入等待状态,并在接收到notify()
或notifyAll()
调用时被唤醒。 Condition
接口提供了比wait()
和notify()
更丰富的功能,例如能够创建多个条件变量,并对它们分别进行await()
和signal()
操作。
注意事项:
- 使用条件变量时,要避免进入无限等待状态。在
await()
返回之后,应该总是重新检查条件是否满足。 - 确保在
await()
之前释放锁,并在await()
之后重新获取锁,以避免死锁。 - 使用
signal()
唤醒单个等待线程,而signalAll()
唤醒所有等待的线程。
条件变量提供了一种强大的方式来同步线程,使得线程可以根据特定的条件进行等待和唤醒,从而实现复杂的同步逻辑。
volatile 能否保证线程安全
线程安全要考虑三个方面:可见性、有序性、原子性
可见性(Visibility):
可见性是指当多个线程访问同一个变量时,一个线程对变量的修改对其他线程是可见的。
volatile
关键字可以确保变量的可见性。当一个线程修改了一个volatile
变量时,新值会立即同步到主内存中,其他线程再次读取该变量时会从主内存中读取新值。有序性(Ordering):
有序性是指程序执行的顺序按照代码的先后顺序进行。在多线程环境中,由于编译器优化和处理器优化,指令重排可能导致代码执行顺序与编写顺序不同。
volatile
变量的写操作在读取操作之前不会发生指令重排,从而确保了有序性。原子性(Atomicity):
原子性是指一个操作或者一系列操作要么全部执行,要么全部不执行,中间不会穿插其他线程的操作。
基本数据类型的访问和操作(如 int、long、boolean 等)通常具有原子性,但是复合操作(如递增 i++ 或 ++i)不是原子的。Java 提供了 synchronized 和 java.util.concurrent 包中的原子类(如 AtomicInteger)来确保复合操作的原子性。
线程安全的实现方法:
- 使用
volatile
:适用于只读操作或对单个变量的写入操作,确保变量的可见性和有序性,但不保证复合操作的原子性。 - 使用
synchronized
:确保同一时刻只有一个线程可以访问被synchronized
修饰的代码块或方法,从而保证原子性、可见性和有序性。 - 使用
Lock
接口:提供了比synchronized
更丰富的锁操作,可以设置尝试非阻塞获取锁、超时获取锁等,同样可以保证原子性、可见性和有序性。 - 使用原子类:如
AtomicInteger
、AtomicLong
等,它们利用 CAS(Compare-And-Swap)操作来保证复合操作的原子性。 - 使用线程局部变量:每个线程有自己的变量副本,不需要与其他线程共享,从而避免线程安全问题。
- 使用不可变对象:不可变对象的状态在创建后不能被修改,因此天然是线程安全的。