java并发高级面试题(三)
问题59:StampedLock的乐观读和悲观读有什么区别?
回答: StampedLock是java.util.concurrent包中的一种锁机制,支持三种访问模式:乐观读、悲观读和写。
-
乐观读(Optimistic Read): 乐观读是一种无锁的读操作,线程不会阻塞等待锁的释放。线程通过tryOptimisticRead()方法尝试获取乐观读锁,然后进行读操作。在读操作完成后,线程需要调用validate()方法来检查锁是否仍然有效。如果在读操作期间没有其他线程进行写操作,则读操作是有效的,否则需要转为悲观读。
-
悲观读(Read): 悲观读是一种传统的读操作,线程会获取悲观读锁,其他线程无法获取写锁。悲观读锁在写锁释放之前会一直保持,可能会导致写锁等待。
-
写(Write): 写操作是独占性的,线程获取写锁后,其他线程无法获取读锁或写锁。
StampedLock的乐观读适用于读多写少的场景,可以提高性能。但需要注意,乐观读在检查锁的有效性时可能会失败,需要重新尝试或转为悲观读。
问题60:ThreadPoolExecutor中的核心线程数和最大线程数的区别是什么?
回答: ThreadPoolExecutor是java.util.concurrent包中的一个线程池实现,它有两个参数与线程数量相关:核心线程数和最大线程数。
-
核心线程数(Core Pool Size): 核心线程数是线程池中保持的常驻线程数量。当有新的任务提交时,如果当前线程数小于核心线程数,会创建新的线程来处理任务。即使线程池中没有任务,核心线程也不会被回收。
-
最大线程数(Maximum Pool Size): 最大线程数是线程池中允许的最大线程数量。当有新的任务提交时,如果当前线程数小于核心线程数,会创建新的线程来处理任务。但如果当前线程数大于等于核心线程数,且工作队列已满,线程池会创建新的线程,直到线程数达到最大线程数。
核心线程数和最大线程数的区别在于线程的回收。核心线程数的线程不会被回收,最大线程数的线程在空闲一段时间后会被回收。这可以根据任务负载的情况来灵活调整线程池中的线程数量。
问题61:ThreadPoolExecutor中的拒绝策略有哪些?如何选择合适的拒绝策略?
回答: ThreadPoolExecutor中的拒绝策略用于处理当任务提交超过线程池容量时的情况,即线程池已满。以下是常见的拒绝策略:
-
AbortPolicy: 默认的拒绝策略,当线程池已满时,新的任务提交会抛出RejectedExecutionException异常。
-
CallerRunsPolicy: 当线程池已满时,新的任务会由提交任务的线程来执行。这样可以避免任务被抛弃,但可能会影响提交任务的线程的性能。
-
DiscardPolicy: 当线程池已满时,新的任务会被直接丢弃,不会抛出异常,也不会执行。
-
DiscardOldestPolicy: 当线程池已满时,新的任务会丢弃等待队列中最旧的任务,然后尝试将新任务添加到队列。
选择合适的拒绝策略取决于业务需求和应用场景。如果对任务丢失比较敏感,可以选择CallerRunsPolicy,保证任务不会被丢弃。如果不关心丢失一些任务,可以选择Discard
Policy或DiscardOldestPolicy。如果希望了解任务被拒绝的情况,可以选择AbortPolicy并捕获RejectedExecutionException。
问题62:ForkJoinTask的fork()和join()方法有什么作用?
回答: ForkJoinTask是java.util.concurrent包中 用于支持分治任务的基类,它有两个重要的方法:fork()和join()。
-
fork()方法: fork()方法用于将当前任务进行拆分,生成子任务并将子任务提交到ForkJoinPool中执行。子任务的执行可能会递归地进行拆分,形成任务树。
-
join()方法: join()方法用于等待子任务的执行结果。在调用join()方法时,当前线程会等待子任务的执行完成,然后获取子任务的结果。如果子任务还有未完成的子任务,join()方法也会递归等待。
fork()和join()方法的使用可以实现分治任务的并行处理,将大任务拆分为小任务,然后将子任务的结果合并。这有助于提高任务的并行性和效率。
问题63:ThreadLocal是什么?它的作用是什么?
回答: ThreadLocal是java.lang包中的一个类,用于在多线程环境中为每个线程提供独立的变量副本。每个线程可以独立地访问自己的变量副本,互不干扰。ThreadLocal通常被用来解决线程安全问题和避免线程间共享变量造成的竞争问题。
ThreadLocal的作用主要有两个方面:
-
线程隔离: 每个线程可以独立地使用自己的ThreadLocal变量,而不会受到其他线程的影响。这可以避免线程安全问题,允许每个线程在多线程环境中拥有自己的状态。
-
上下文传递: ThreadLocal可以用于在同一线程的不同方法之间传递上下文信息,而不需要显式地传递参数。这对于 一些跨方法、跨类的调用场景非常有用。
示例:
public class ThreadLocalExample {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
Runnable task = () -> {
int value = threadLocal.get();
System.out.println(Thread.currentThread().getName() + " initial value: " + value);
threadLocal.set(value + 1);
value = threadLocal.get();
System.out.println(Thread.currentThread().getName() + " updated value: " + value);
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
}
}
在上面的示例中,我们展示了ThreadLocal的用法。每个线程可以独立地使用自己的ThreadLocal变量,并且在不同线程之间互不干扰。
问题64:ThreadLocal的内存泄漏问题如何避免?
回答: 尽管ThreadLocal提供了线程隔离的能力,但在某些情况下会导致内存泄漏。当ThreadLocal变量被创建后,如果没有手动清理,它会一直保留对线程的引用,导致线程无法被回收,从而可能引发内存泄漏问题。
为了避免ThreadLocal的内存泄漏,可以考虑以下几点:
-
及时清理: 在使用完ThreadLocal变量后,应该调用remove()方法将变量从当前线程中移除,以便线程可以被回收。可以使用try-finally块来确保在任何情况下都会清理。
-
使用WeakReference: 可以使用WeakReference来
持有ThreadLocal变量,使得变量不会阻止线程的回收。但需要注意,这可能会导致变量在不需要的时候被提前回收。
- 使用InheritableThreadLocal: InheritableThreadLocal允许子线程继承父线程的ThreadLocal变量,但仍然需要注意及时清理,以避免子线程的变量引用造成泄漏。
示例:
public class ThreadLocalMemoryLeakExample {
private static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set(new Object());
// ... Some operations
// Ensure to remove the thread-local variable
threadLocal.remove();
}
}
在上面的示例中,我们在使用完ThreadLocal变量后调用了remove()方法来清理变量,避免了内存泄漏问题。
问题65:如何实现一个线程安全的单例模式?
回答: 实现线程安全的单例模式需要考虑多线程环境下的并发访问问题。以下是几种常见的线程安全的单例模式实现方式:
-
懒汉模式(Double-Check Locking): 在第一次使用时才创建实例,使用双重检查来确保只有一个线程创建实例。需要使用volatile修饰实例变量,以保证在多线程环境下的可见性。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
} -
静态内部类模式: 使用静态内部类来持有实例,实现懒加载和线程安全。由于静态内部类只会在被引用时加载,因此实现了懒加载的效果。
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
} -
枚举单例模式: 枚举类型天然地支持单例模式,而且在多线程环境下也是线程安全的。枚举类型的实例是在类加载时创建的。
public enum Singleton {
INSTANCE;
// Add methods and fields here
}
以上这些方式都可以实现线程安全的单例模式,选择哪种方式取决于项目的需求和使用场景。
问题66:Thread.sleep()和Object.wait()有什么区别?
回答: Thread.sleep()和Object.wait()都可以用于线程的等待,但它们之间有一些区别:
-
方法来源: Thread.sleep()是Thread类的静态方法,用于让当前线程休眠一段时间。Object.wait()是Object类的实例方法,用于将当前线程放入对象的等待队列中。
-
调用方式: Thread.sleep()可以直接调用,无需获取对象的锁。Object.wait()必须在同步块或同步方法中调用,需要获取对象的锁。
-
等待目标: Thread.sleep()只是让线程休眠,不释放任何锁。Object.wait()会释放调用对象的锁,进入等待状态,直到其他线程调用相同对象的notify()或notifyAll()方法。
-
使用场景: Thread.sleep()主要用于线程暂停一段时间,模拟时间等待。Object.wait()主要用于线程间的通信和同步,将线程置于等待状态,直到特定条件满足。
示例:
public class WaitSleepExample {
public static void main(String[] args) {
Object lock = new Object();
// Thread.sleep() example
new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("Thread A woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// Object.wait() example
new Thread(() -> {
synchronized (lock) {
try {
lock.wait(1000);
System.out.println("Thread B woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
在上面的示例中,我们展示了Thread.sleep()和Object.wait()的用法。Thread.sleep()是在不同线程中使用的,而Object.wait()是在同一个对象的锁范围内使用的。
问题67:volatile关键字的作用是什么?它解决了什么问题?
回答: volatile是一个关键字,用于修饰变量,它的主要作用是确保线程之间对该变量的可见性和禁止指令重排序。volatile关键字解决了多线程环境下的两个问题:
-
可见性问题: 在多线程环境下,一个线程修改了一个共享变量的值,其他线程可能无法立即看到这个变化,从而导致错误的结果。volatile关键字可以确保变量的修改对所有线程可见,即使在不同线程中使用不同的缓存。
-
指令重排序问题: 编译器和处理器为了提高性能可能会对指令进行重排序,这在单线程环境下不会产生问题,但在多线程环境下可能导致意想不到的结果。volatile关键字可以防止指令重排序,确保指令按照预期顺序执行。
示例:
public class VolatileExample {
private volatile boolean flag = false;
public void toggleFlag() {
flag = !flag;
}
public boolean isFlag() {
return flag;
}
public static void main(String[] args) {
VolatileExample example = new VolatileExample();
Thread writerThread = new Thread(() -> {
example.toggleFlag();
System.out.println("Flag set to true");
});
Thread readerThread = new Thread(() -> {
while (!example.isFlag()) {
// Busy-wait
}
System.out.println("Flag is true");
});
writerThread.start();
readerThread.start();
}
}
在上面的示例中,我们使用了volatile关键字来确保flag变量的可见性,使得在readerThread中可以正确读取到writerThread修改的值。