目录
4. ReentrantLock与synchronized有什么区别?
9. ExecutorService的submit()与invokeAll()方法有什么区别?
13. ConcurrentHashMap 和 Hashtable 的区别?
可以先看思考题,然后思考,然后再看看一些知道点,若是有补充的地方,欢迎👏指正🎉
具体如下:
❓思考题
题目描述:
现有函数
printNumber可以用一个整数参数调用,并输出该整数到控制台。
- 例如,调用
printNumber(7)将会输出7到控制台。给你类
ZeroEvenOdd的一个实例,该类中有三个函数:zero、even和odd。ZeroEvenOdd的相同实例将会传递给三个不同线程:
- 线程 A:调用
zero(),只输出0- 线程 B:调用
even(),只输出偶数- 线程 C:调用
odd(),只输出奇数修改给出的类,以输出序列
"010203040506...",其中序列的长度必须为2n。实现
ZeroEvenOdd类:
ZeroEvenOdd(int n)用数字n初始化对象,表示需要输出的数。void zero(printNumber)调用printNumber以输出一个 0 。void even(printNumber)调用printNumber以输出偶数。void odd(printNumber)调用printNumber以输出奇数。示例 1:
输入:n = 2
输出:"0102"
解释:三条线程异步执行,其中一个调用 zero(),另一个线程调用 even(),最后一个线程调用odd()。正确的输出为 "0102"。示例 2:
输入:n = 5
输出:"0102030405"提示:
1 <= n <= 1000
其他相关思考题目:多线程题目
1️⃣基础知识点
1. Java并发基础概念
-
并发(Concurrency):多个任务在同一时间段内交替执行,并不一定是同时执行。例如,操作系统可能通过时间片轮转来让多个线程“同时”执行。
-
并行(Parallelism):多个任务在同一时间真正并行执行,通常需要多核处理器支持。
2. 线程与进程
-
进程(Process):操作系统资源分配的基本单位,每个进程有自己的地址空间。
-
线程(Thread):是进程中的一个执行单元,线程间共享进程的资源(如内存)。线程是操作系统调度的基本单位。
3. 线程的生命周期
线程的生命周期包括:
-
新建(New):线程对象已经创建,但尚未启动。
-
就绪(Runnable):线程已准备好执行,但等待操作系统调度。
-
运行(Running):线程被操作系统调度并开始执行。
-
阻塞(Blocked):线程在等待资源(如I/O、锁)时被挂起。
-
死亡(Dead):线程执行完毕或被中止。
4. 创建线程的方式
Java提供了最常见两种的创建线程的方式:
-
继承Thread类:重写
run()方法并调用start()方法启动线程。 -
实现Runnable接口:实现
run()方法并传递给Thread对象。
其他内容:https://blog.csdn.net/qq_43544074/article/details/134988612
5. 线程池(Executor)
线程池是提高多线程应用性能的关键工具,避免频繁的线程创建和销毁。
-
Executor框架:Java提供了
Executor接口及其实现,如ThreadPoolExecutor。-
Executors.newFixedThreadPool(int nThreads):创建一个固定大小的线程池。 -
Executors.newCachedThreadPool():创建一个根据需要创建新线程的(缓存)线程池。 -
Executors.newSingleThreadExecutor():创建一个单线程的线程池。 Executors.newScheduledThreadPool(int corePoolSize):支持延迟任务的线程池。
-
线程池管理通过线程池大小、任务队列、线程工厂等参数进行调节。
6. 线程同步
线程同步是解决多个线程并发访问共享资源时数据不一致的问题。
-
synchronized关键字:Java提供了
synchronized关键字,用于修饰方法或代码块,确保同一时间只有一个线程可以访问同步代码块。public synchronized void synchronizedMethod() { // critical section }-
类锁和实例锁:
synchronized修饰实例方法时是实例锁,修饰静态方法时是类锁。
-
-
ReentrantLock:
java.util.concurrent.locks.ReentrantLock提供了比synchronized更灵活的锁机制,支持显式锁和解锁操作。Lock lock = new ReentrantLock(); try { lock.lock(); // critical section } finally { lock.unlock(); }
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 语法 | 内置关键字 | 需要显式导入和实例化 |
| 灵活性 | 不能中断等待 | 可以中断等待 |
| 超时支持 | 不支持 | 支持尝试获取锁并设置超时 |
| 公平性 | 非公平锁 | 支持公平锁和非公平锁 |
-
同步块:更细粒度的同步控制
Object lock = new Object();
public void syncBlock() {
synchronized (lock) {
// 关键代码段
}
}
同步工具
1. CountDownLatch
允许一个或多个线程等待其他线程完成操作:
CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> { latch.countDown(); }).start();
latch.await(); // 主线程等待
2. CyclicBarrier
多个线程到达屏障后一起执行:
CyclicBarrier barrier = new CyclicBarrier(3);
new Thread(() -> { barrier.await(); }).start();
barrier.await();
3. Semaphore
控制同时访问某一资源的线程数量:
Semaphore semaphore = new Semaphore(3);
semaphore.acquire();
try {
// 访问资源
} finally {
semaphore.release();
}
7. 死锁(Deadlock)
死锁是指两个或多个线程相互等待对方释放资源,导致程序无法继续执行。避免死锁的策略包括:
-
避免嵌套锁(锁的顺序应该一致)。
-
使用定时锁尝试(
tryLock()方法)。 -
限制锁的持有时间。
8. 并发集合
Java并发包(java.util.concurrent)提供了线程安全的集合类,这些集合类比传统的集合类(如Vector、Hashtable)具有更高的并发性能。
-
CopyOnWriteArrayList:写时复制,适用于读多写少的场景。
List<String> list = new CopyOnWriteArrayList<>();
list.add("item");
-
ConcurrentHashMap:高效的并发哈希表,支持分段锁定。
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("key", "value");
-
BlockingQueue:线程安全的队列,常用于生产者消费者问题。
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.put("item"); // 生产者
String item = queue.take(); // 消费者
9. 原子变量
原子变量是对基本数据类型进行线程安全操作的工具。Java提供了java.util.concurrent.atomic包中的一些原子类:
-
AtomicInteger、AtomicLong、AtomicReference等。
这些类提供了原子操作,确保不会发生线程干扰。
10. 并发工具类
Java并发工具类(位于java.util.concurrent包中)帮助开发者更方便地实现并发操作。
-
CountDownLatch:使一个或多个线程等待其他线程完成操作。
CountDownLatch latch = new CountDownLatch(1); latch.await(); // 等待 latch.countDown(); // 减少计数 -
CyclicBarrier:使一组线程互相等待,直到所有线程都到达某个公共屏障点。
-
Semaphore:控制访问资源的线程数目,类似于信号量。
-
Exchanger:用于交换数据的线程同步点。
11. 并发编程设计模式
-
生产者-消费者模式:通过队列实现生产者与消费者之间的协调。
-
读写锁模式(Read-Write Lock):允许多个线程并发读取,但只有一个线程可以写入。
-
ReentrantReadWriteLock支持读写锁。
-
12. 并发调度与线程优先级
-
线程优先级:通过
Thread.setPriority(int priority)设置线程优先级,但并非所有操作系统都支持。 -
Thread.sleep():使线程暂停执行指定的时间。
-
yield():让当前线程主动让出CPU使用权,通常是为了增强程序的响应性。
13. 并发测试与性能调优
-
线程泄漏:线程没有被正确关闭或结束,导致系统资源浪费。
-
ThreadLocal:为每个线程提供独立的变量副本,避免了线程间的数据竞争。
14. Java 8中的并发新特性
-
CompletableFuture:提供了更灵活的异步编程支持,支持链式调用和异常处理。
-
Stream API:Stream的并行处理(
parallelStream())可以让数据流在多个线程中并行处理,适用于大量数据的处理。
15. 并发性能优化
-
减少锁竞争:通过锁分段(例如
ConcurrentHashMap)或使用无锁编程(如CAS)来减少竞争。 -
减少上下文切换:避免线程频繁切换,尽量让线程在获得锁后保持较长时间的计算。
-
合适的线程池配置:合理配置线程池大小,避免创建过多的线程导致过高的上下文切换。
16. 线程安全的设计原则
- 尽量减少锁的范围:仅锁定必要的代码块。
- 使用无锁算法:如
ConcurrentHashMap和Atomic类。 - 避免死锁:确保资源获取顺序一致。
- 优先使用并发集合:如
ConcurrentHashMap替代手动同步。
2️⃣面试相关
1. 什么是死锁?如何避免死锁?
参考回答:
死锁是指两个或多个线程因争夺资源而互相等待,导致程序无法继续执行。死锁发生的四个必要条件是:
-
互斥:每个资源要么是被一个线程占用,要么是可用的。
-
持有并等待:一个线程持有一个资源并等待获取其他资源。
-
不剥夺(不可抢占):线程已经获得的资源在没有使用完之前,不能被其他线程剥夺。
-
循环等待:线程之间形成一种循环等待资源的关系。
如何避免死锁:
-
避免嵌套锁:所有线程按相同顺序请求锁,以避免循环等待。
-
使用
tryLock()方法:通过ReentrantLock的tryLock()方法尝试获取锁,如果获取不到锁可以选择放弃或者尝试其他措施。 -
设定锁的超时:设置锁的最大等待时间,如果超时未能获得锁,线程放弃并重新尝试。
-
减少锁的粒度:避免对大范围的资源进行锁定,尽量缩小锁的范围。
2. 什么是volatile关键字?它的作用是什么?
参考回答:
volatile是Java中的一个关键字,它用于保证变量在不同线程之间的可见性。当一个变量被声明为volatile时,确保所有线程都能看到这个变量的最新值,而不会读取到线程的本地缓存中的值。
作用:
-
保证可见性:当一个线程修改了
volatile变量的值,其他线程能立即看到修改后的值。 -
禁止指令重排:
volatile保证了在多线程环境下的“有序性”。即volatile变量的读取和写入操作不能与其他操作交换顺序。
注意:
-
volatile并不能保证复合操作(如i++)的原子性,如果需要复合操作的原子性,应该使用AtomicInteger等类。 -
volatile只是保证可见性,不能保证原子性。
3. 什么是synchronized关键字?它如何工作?
参考回答:
synchronized是Java中一个用来实现线程同步的关键字。它保证在同一时刻只有一个线程能够执行被synchronized修饰的代码块或方法,从而避免多个线程并发访问共享资源时发生数据不一致。
如何工作:
-
同步方法:在方法声明时使用
synchronized关键字,表示方法中的代码块是同步的。public synchronized void syncMethod() { // synchronized code }这个同步方法的锁是当前实例对象(对于实例方法)或者类对象(对于静态方法)。
-
同步代码块:使用
synchronized关键字修饰代码块,可以指定一个锁对象进行同步。synchronized (lockObject) { // synchronized code }
原理:
当一个线程访问synchronized修饰的代码时,首先需要获取该方法或代码块的锁对象。当该锁被其他线程持有时,其他线程无法访问该代码块,直到锁被释放。
4. ReentrantLock与synchronized有什么区别?
参考回答:
-
可重入性:
ReentrantLock是可重入的,意味着同一个线程可以多次获得同一把锁;synchronized也是可重入的。 -
锁的灵活性:
ReentrantLock比synchronized提供了更多的功能,如定时锁(tryLock())、公平锁、可以通过lockInterruptibly()响应中断等。 -
性能:
ReentrantLock的性能在高并发时通常优于synchronized,因为它提供了更多的调节控制方式(例如公平锁)。 -
可中断性:
ReentrantLock支持中断锁的等待(lockInterruptibly()),而synchronized在等待锁时无法中断。 -
可实现公平性:
ReentrantLock支持公平锁(通过构造函数传入true),确保线程按请求的顺序获取锁,而synchronized没有公平性保证。
5. 什么是CountDownLatch,如何使用它?
参考回答:
CountDownLatch是一个同步工具类,用于在一个或多个线程中等待其他线程完成操作后再继续执行。它维护一个计数器,每次调用countDown()方法会减少计数器的值,直到计数器的值为零时,调用await()方法的线程才会继续执行。
使用场景:
通常用于线程间协调,确保某些操作完成后才进行下一步工作。比如,可以等待多个线程任务完成后再进行合并操作。
示例代码:
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("Thread finished");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
}).start();
}
latch.await(); // 主线程等待
System.out.println("All threads finished");
}
}
ConcurrentHashMap 和 Hashtable 的区别?
| 特性 | ConcurrentHashMap | Hashtable |
|---|---|---|
| 线程安全 | 分段锁机制 | 整体锁 |
| 性能 | 高效并发 | 性能较差 |
| 空值支持 | 支持空值 | 不支持空值 |
| 迭代器 | 弱一致迭代器 | 强一致迭代器 |
6. 什么是线程池,Java如何实现线程池管理?
参考回答:
线程池是一个通过预先创建一定数量的线程来处理任务的机制,可以减少频繁创建和销毁线程的开销,提高程序的性能和资源利用率。Java通过java.util.concurrent包中的ExecutorService接口及其实现来提供线程池管理。
常用线程池实现:
-
FixedThreadPool:创建一个固定大小的线程池,适用于任务数已知且固定的场景。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10); -
CachedThreadPool:创建一个可以根据需要创建新线程的线程池,适用于任务数不确定且可能较多的场景。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); -
SingleThreadExecutor:创建一个单线程的线程池,适用于顺序执行的任务。
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); -
ScheduledThreadPoolExecutor:支持任务定时执行的线程池。
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5);
线程池的好处:
-
降低了线程创建和销毁的开销。
-
可以通过合理配置线程池的大小来平衡性能和资源消耗。
-
通过队列管理任务的执行顺序。
7. Java中如何处理并发中的线程安全问题?
参考回答:
Java提供了几种方式来处理线程安全问题:
-
使用
volatile:确保变量的可见性,适用于单一变量的同步。 -
使用
synchronized:保证同一时刻只有一个线程能够访问同步块或方法。 -
使用
ReentrantLock:比synchronized提供更灵活的锁控制,包括定时锁、可中断锁等。 -
使用并发集合:例如
ConcurrentHashMap、CopyOnWriteArrayList等,适用于高并发环境下的数据结构。 -
使用原子变量:通过
java.util.concurrent.atomic包中的类(如AtomicInteger)来保证操作的原子性。
8. 解释一下什么是“ABA问题”,并提供解决方案。
参考回答:
ABA问题:这是指一个线程在执行操作时检查变量的值是否为A,假设值没有变化,然后修改该值为B,再修改为A。此时其他线程可能也修改了该值,但并不会检测到这个变化。由于变量值最终回到A,线程并不知道该值已经被改变过,从而导致错误的结果。
解决方案:
-
使用版本号:通过引入版本号来标记变量的变化。每次修改时都更新版本号,从而避免ABA问题。
-
AtomicStampedReference:java.util.concurrent.atomic.AtomicStampedReference类结合了原子操作和版本控制,避免了ABA问题。AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0); int[] stamp = new int[1]; Integer currentValue = ref.get(stamp); // Do something with currentValue ref.compareAndSet(currentValue, 200, stamp[0], stamp[0] + 1);
9. ExecutorService的submit()与invokeAll()方法有什么区别?
参考回答:
-
submit():-
提交一个单独的任务给线程池,并返回一个
Future对象,表示任务的执行结果。 -
可以提交
Runnable或Callable任务。 -
适用于你只关心一个任务的执行结果的情况。
-
ExecutorService executor = Executors.newFixedThreadPool(10);
Future<Integer> future = executor.submit(() -> {
return 1 + 1;
});
// 阻塞直到任务完成
Integer result = future.get();
-
invokeAll():-
提交一个
Collection类型的任务给线程池,并返回一个List<Future>对象,表示每个任务的执行结果。 -
所有任务会并发执行,并且
invokeAll()会等到所有任务完成后返回。 -
适用于你需要同时处理多个任务并等待所有任务完成的场景。
-
ExecutorService executor = Executors.newFixedThreadPool(10);
List<Callable<Integer>> tasks = Arrays.asList(
() -> 1 + 1,
() -> 2 + 2,
() -> 3 + 3
);
List<Future<Integer>> futures = executor.invokeAll(tasks);
for (Future<Integer> future : futures) {
// 输出每个任务的结果
System.out.println(future.get());
}
10. 什么是ThreadLocal,它是如何工作的?
参考回答:
ThreadLocal是Java中的一个类,用于为每个线程提供一个独立的变量副本。每个线程都会持有该变量的一个副本,因此不同线程之间不会相互干扰,避免了线程安全问题。
工作原理:
-
ThreadLocal变量在每个线程中都有一个独立的副本,因此多个线程同时访问该变量时,互不影响。 -
每个线程通过
ThreadLocal.get()获取当前线程的变量副本,使用ThreadLocal.set()设置当前线程的变量副本。
使用场景:
适用于每个线程需要独立数据的情况,如数据库连接、会话信息等。
示例代码:
public class ThreadLocalExample {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
public static void main(String[] args) {
System.out.println("Main thread value: " + threadLocal.get());
Thread thread1 = new Thread(() -> {
threadLocal.set(10);
System.out.println("Thread 1 value: " + threadLocal.get());
});
Thread thread2 = new Thread(() -> {
threadLocal.set(20);
System.out.println("Thread 2 value: " + threadLocal.get());
});
thread1.start();
thread2.start();
}
}
注意事项:
-
ThreadLocal变量通常会在线程结束时自动清理,但在一些高并发的场景中,如果使用不当(例如没有及时清除),可能会导致内存泄漏。
11. Java中如何实现线程间的通信?
参考回答:
Java中可以通过以下方式实现线程间的通信:
-
使用
wait()、notify()和notifyAll()方法:这些方法是Object类的一部分,通过在同步代码块中使用wait()来让线程等待,notify()或者notifyAll()用来唤醒等待的线程。示例代码:
class SharedResource { private int count = 0; public synchronized void increment() throws InterruptedException { while (count >= 5) { wait(); // 等待条件满足 } count++; System.out.println("Count: " + count); notify(); // 通知其他线程 } public synchronized void decrement() throws InterruptedException { while (count <= 0) { wait(); // 等待条件满足 } count--; System.out.println("Count: " + count); notify(); // 通知其他线程 } } public class ThreadCommunicationExample { public static void main(String[] args) { SharedResource resource = new SharedResource(); new Thread(() -> { try { for (int i = 0; i < 6; i++) { resource.increment(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); new Thread(() -> { try { for (int i = 0; i < 6; i++) { resource.decrement(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); } } -
使用
BlockingQueue:java.util.concurrent包中的BlockingQueue是一个线程安全的队列,提供了阻塞的插入和删除操作,可以有效地解决线程间的通信问题。示例代码:
class Producer implements Runnable { private BlockingQueue<Integer> queue; public Producer(BlockingQueue<Integer> queue) { this.queue = queue; } @Override public void run() { try { for (int i = 0; i < 5; i++) { queue.put(i); // 放入数据 System.out.println("Produced: " + i); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } class Consumer implements Runnable { private BlockingQueue<Integer> queue; public Consumer(BlockingQueue<Integer> queue) { this.queue = queue; } @Override public void run() { try { for (int i = 0; i < 5; i++) { Integer value = queue.take(); // 获取数据 System.out.println("Consumed: " + value); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public class BlockingQueueExample { public static void main(String[] args) { BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); new Thread(new Producer(queue)).start(); new Thread(new Consumer(queue)).start(); } }
12. Future与Callable的区别是什么?
参考回答:
-
Callable:是一个函数式接口,类似于Runnable,但是它的call()方法可以返回一个值,并且能够抛出异常。适用于需要计算结果的任务。 -
Future:表示一个异步计算的结果。submit()方法会返回一个Future对象,允许你查询任务的执行状态,或者获取任务的结果。通过Future.get()方法可以阻塞当前线程直到任务完成,并返回任务的结果。
区别:
-
Callable可以返回计算结果,Runnable不能。 -
Future用于获取异步计算的结果,而Callable是定义计算逻辑的任务。
13. ConcurrentHashMap 和 Hashtable 的区别?
参考回答:
| 特性 | ConcurrentHashMap | Hashtable |
|---|---|---|
| 线程安全 | 分段锁机制 | 整体锁 |
| 性能 | 高效并发 | 性能较差 |
| 空值支持 | 支持空值 | 不支持空值 |
| 迭代器 | 弱一致迭代器 | 强一致迭代器 |
14. Java 中的线程池有哪些实现?
参考回答:
Java 提供了以下几种线程池实现:
Executors.newFixedThreadPool(int nThreads):固定大小线程池。Executors.newSingleThreadExecutor():单线程池。Executors.newCachedThreadPool():缓存线程池。Executors.newScheduledThreadPool(int corePoolSize):支持延迟任务的线程池。
15. 什么是线程池的工作原理?
参考回答:
线程池的工作原理如下:
- 任务提交:任务被提交到队列中。
- 线程复用:线程池中的线程从队列中获取任务并执行。
- 线程回收:线程空闲时会被回收以节省资源。
- 动态调整:根据负载动态调整线程数量。
16. Java 中的线程安全集合有哪些?
参考回答:
Java 提供了以下线程安全集合:
ConcurrentHashMapCopyOnWriteArrayListCopyOnWriteArraySetConcurrentLinkedQueueBlockingQueue(如LinkedBlockingQueue)
💡思考题参考解答
为了实现多线程之间的同步和协作,确保输出顺序符合要求,我们会使用 Java 的 synchronized 关键字来保证线程安全,利用 wait() 和 notifyAll() 方法来控制线程的执行顺序。同时,借助一个布尔类型的标记变量 isZero 来决定当前应该打印 0 还是奇数 / 偶数。
class ZeroEvenOdd {
private final int n;
private int x = 1;
// true: 打印0 false: 不打印0
private boolean isZero = true;
public ZeroEvenOdd(int n) {
this.n = n;
}
public void zero(IntConsumer printNumber) throws InterruptedException {
while (x <= n) {
synchronized (this) {
if (isZero) {
printNumber.accept(0);
isZero = false;
this.notifyAll();
} else {
this.wait();
}
}
}
}
public void even(IntConsumer printNumber) throws InterruptedException {
while (x <= n) {
synchronized (this) {
if (x % 2 == 0 && !isZero) {
printNumber.accept(x);
isZero = true;
x++;
this.notifyAll();
} else {
this.wait();
}
}
}
}
public void odd(IntConsumer printNumber) throws InterruptedException {
while (x <= n) {
synchronized (this) {
if (x % 2 != 0 && !isZero) {
printNumber.accept(x);
isZero = true;
x++;
this.notifyAll();
} else {
this.wait();
}
}
}
}
}
解答内容:
作者:晨晨
链接:https://leetcode.cn/problems/print-zero-even-odd/solutions/3070573/shi-yong-synchronizedwaitfang-fa-notifya-vatc/
来源:力扣(LeetCode)著作权归作者所有。
至此,还有补充的,欢迎👏评论留言🎉
644

被折叠的 条评论
为什么被折叠?



