又到了复习八股的时间,本文根据一些常见问题进行了一下总结。
线程池主要⽤来管理⼦线程,它的优势在于:
线程池中是以⽣产者消费者模式,通过⼀个阻塞队列来实现的,阻塞队列缓存任务,⼯作线程从阻塞队列中获取任务。
核心线程数量固定,非核心线程数量最大为 Integer.MAX_VALUE,当非核⼼线程闲置超过10s会被回收,主要⽤于执行定时任务和具有固定周期的重复任务。
线程安全是指在多线程环境下,当多个线程访问某个类的实例时,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要额外的同步或协调操作,这个类都能表现出正确的行为。具体来说:
悲观锁是一种在多线程环境下常用的同步策略,与乐观锁相对。悲观锁的核心思想是假设最坏的情况,即总是认为多个线程同时修改同一数据的冲突是很常见的,因此在访问任何共享资源之前都需要先加锁,这样可以确保同时只有一个线程能够访问到共享资源。悲观锁的特点是先加锁再访问数据。
使用方法:
//同步方法
//在方法声明上添加 synchronized 关键字,使得整个方法成为同步方法。
public synchronized void syncMethod() {
// 同步操作
}
//同步代码块
//使用 synchronized 关键字同步一个对象实例。
public void method() {
synchronized(this) {
// 同步操作
}
}
//或者同步一个类的类对象来实现类级别的锁定。
public void staticMethod() {
synchronized(MyClass.class) {
// 同步操作
}
}
使用方法
import java.util.concurrent.locks.ReentrantLock;
public class MyObject {
private final ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock(); // 获取锁
try {
// 同步操作
} finally {
lock.unlock(); // 释放锁
}
}
}
//ReentrantLock 可以设置为公平锁,使等待时间最长的线程优先获得锁。
private final ReentrantLock fairLock = new ReentrantLock(true);
public void fairMethod() {
fairLock.lock();
try {
// 同步操作
} finally {
fairLock.unlock();
}
}
乐观锁是一种在多线程环境下进行同步的机制,与悲观锁相对。它基于这样一种假设:在大多数时间里,数据通常不会发生冲突,因此不需要加锁。相反,它会在数据更新时进行冲突检测,从而提高系统的并发能力。Java 中的乐观锁主要通过以下两种方式实现:
自旋锁是一种锁机制,它在等待获取锁的过程中保持线程在“忙等”(busy-waiting)状态。这意味着线程不会立即进入阻塞状态,而是在一个循环中反复检查锁是否可用。
Callable 是一个接口,类似于 Runnable 接口。与 Runnable 不同的是,Callable 允许任务返回值,并且可以抛出异常。 返回值:Callable 使用泛型来指定任务执行后的返回类型。 异常处理:Callable 允许在任务中抛出受检查的异常。
Callable<Integer> task = () -> {
// 执行任务,返回结果
return 123;
};
Future 用于表示异步计算的结果。它提供了检查计算是否完成的方法,以及等待计算完成并检索其结果的方法。 **结果检索:**可以通过 get 方法获取 Callable 返回的结果,此方法会阻塞直到结果可用。 **状态检查:**提供了方法来检查任务是否完成(如 isDone)或取消(如 cancel)。
// 使用 Callable 和 Future
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Integer> future = executor.submit(() -> {
// 一些耗时的操作
return 123;
});
Integer result = future.get(); // 获取结果
FutureTask 是 Future 的一个具体实现,它同时实现了 Future 和 Runnable 接口。这意味着它可以直接提交给 Thread 对象执行。
// 使用 FutureTask
FutureTask<Integer> futureTask = new FutureTask<>(() -> 123);
Thread thread = new Thread(futureTask);
thread.start();
Integer result = futureTask.get(); // 获取结果
Callable是Runnable封装的异步运算任务。 Future用来保存Callable异步运算的结果 FutureTask封装Future的实体类。
当一个变量被多个线程共享,并且这个变量用于指示某种状态(如线程是否运行),可以将其声明为 volatile。例如,一个线程控制开关:
volatile boolean keepRunning = true;
public void run() {
while (keepRunning) {
// 执行任务
}
}
在双重检查锁定(Double-Checked Locking)单例模式中,volatile 可以防止实例化对象时的指令重排序,确保其他线程看到的对象是完全初始化过的:
public class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
用于定期更新的变量,这些变量的最新值需要被其他线程可见。比如,在一个线程中定期更新数据,在其他线程中读取这些数据。
public class EnvironmentMonitor {
public volatile int currentTemperature;
public void updateTemperature() {
// 更新温度值
this.currentTemperature = readFromSensor();
}
// 其他线程可以安全读取 currentTemperature
}
volatile bean" 模式是一种在 Java 并发编程中使用 volatile 关键字的模式,用于确保 JavaBean 中的属性在多线程环境下能够安全地进行读写操作。
public class Person {
private volatile String first;
private volatile String last;
private volatile int age;
public String getFirst() { return first; }
public String getLast() { return last; }
public int getAge() { return age; }
public void setFirst(String first) {
this.first = first;
}
public void setLast(String last) {
this.last = last;
}
public void setAge(int age) {
this.age = age;
}
}
这个模式是针对那些读操作远远多于写操作的场景。它通过结合使用内部锁(synchronized)和 volatile 变量来减少公共代码路径的开销。
public class SafeCounter {
private volatile int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
//假设有两个线程(线程 A 和线程 B)和两个资源(资源 1 和资源 2):
//线程 A 持有资源 1,并等待资源 2。
//同时,线程 B 持有资源 2,并等待资源 1。
public class DeadlockDemo {
public static void main(String[] args) {
final Object resource1 = new Object();
final Object resource2 = new Object();
// 线程1尝试锁定资源1然后资源2
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("线程1锁定了资源1");
try {
Thread.sleep(100); // 确保线程2有时间锁定资源2
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1尝试锁定资源2");
synchronized (resource2) {
System.out.println("线程1锁定了资源2");
}
}
});
// 线程2尝试锁定资源2然后资源1
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("线程2锁定了资源2");
System.out.println("线程2尝试锁定资源1");
synchronized (resource1) {
System.out.println("线程2锁定了资源1");
}
}
});
thread1.start();
thread2.start();
}
}
//结果
线程1锁定了资源1
线程2锁定了资源2
线程2尝试锁定资源1
线程1尝试锁定资源2
public class DeadlockDemo {
public static void main(String[] args) {
final Object resource1 = new Object();
final Object resource2 = new Object();
// 线程1和线程2都按照相同的顺序获取锁(先resource1,后resource2)
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("线程1锁定了资源1");
try {
Thread.sleep(100); // 确保线程2有时间运行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1尝试锁定资源2");
synchronized (resource2) {
System.out.println("线程1锁定了资源2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (resource1) {
System.out.println("线程2锁定了资源1");
System.out.println("线程2尝试锁定资源2");
synchronized (resource2) {
System.out.println("线程2锁定了资源2");
}
}
});
thread1.start();
thread2.start();
}
}
//结果
线程1锁定了资源1
线程1尝试锁定资源2
线程1锁定了资源2
线程2锁定了资源1
线程2尝试锁定资源2
线程2锁定了资源2
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000, true);
用途:适用于需要固定大小的队列场景。
CAS(Compare-And-Swap,比较并交换)是一种用于实现多线程同步的技术,被广泛应用于编写无锁的并发算法。CAS 是一种乐观锁机制,它假设多个线程之间不会发生冲突,先进行操作,如果发现有冲突再进行相应的处理,这与悲观锁机制(如使用 synchronized)形成对比。
CAS 是一种典型的乐观锁技术。乐观锁总是假设最好的情况,它不会去锁定任何资源,而是先进行操作,如果失败了再重试或者回滚。
在使用 CAS 时,如果操作未能成功(可能由于其他线程的干扰),通常会采取自旋的方式重试,即不断重复比较并交换的过程,直到成功为止。这种自旋策略避免了线程阻塞和唤醒的开销,但如果长时间无法成功,也可能导致CPU资源的浪费。
在 Java 中,CAS 操作是通过 sun.misc.Unsafe 类中的方法实现的,而在高级API中,java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)就是使用了 CAS 操作,提供了无锁的线程安全编程方式。
假设存在两个线程:线程 one 和线程 two:
尽管线程 one的 CAS 操作成功,但是在整个过程中,位置 V 的值确实发生了变化,这可能会导致线程 one 基于错误的假设进行操作。
ABA 问题的一个常见解决方案是引入版本号机制,这是一种乐观锁的实现方式:
在 Java 中,当一个线程对象的 start() 方法被调用两次时,会抛出 IllegalThreadStateException 异常。这是因为一旦线程开始执行,其状态就发生了改变,根据线程的生命周期,它不能重新回到新建(New)状态,而 start() 方法只能在线程处于新建状态时被调用一次。
public class Example {
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
threadLocal.set(1);
System.out.println("Thread1: " + threadLocal.get());
});
Thread thread2 = new Thread(() -> {
threadLocal.set(2);
System.out.println("Thread2: " + threadLocal.get());
});
thread1.start();
thread2.start();
}
}
再深一点就是FutureTask的实现原理可以自己去看看
Java线程池实现原理及其在美团业务中的实践
线程池面试题多如牛毛,我只记了一些常见的,希望能帮到大家。