Java面试八股文-并发篇 1. 线程基础 1.1 什么是线程?线程和进程的区别? 线程 (Thread)是进程中的一个执行单元,是操作系统进行CPU调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源,但各自拥有独立的执行栈和程序计数器。
进程 (Process)是操作系统进行资源分配的基本单位,每个进程拥有独立的内存空间、文件描述符等资源。
线程和进程的区别 :
特性
进程
线程
资源分配
独立的内存空间、文件描述符等
共享进程的资源
调度单位
不是CPU调度的基本单位
CPU调度的基本单位
创建开销
较大,需要分配资源
较小,共享进程资源
切换开销
较大,需要保存和恢复整个进程的状态
较小,只需要保存和恢复线程的状态
通信方式
复杂,需要使用IPC机制(如管道、共享内存等)
简单,可以直接共享进程内的变量
安全性
一个进程崩溃不会影响其他进程
一个线程崩溃可能会影响整个进程
数量限制
系统能运行的进程数量较少
系统能运行的线程数量较多
示例 :
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 public class ProcessExample { public static void main (String[] args) { System.out.println("This is a Java process" ); try { Process process = Runtime.getRuntime().exec("notepad.exe" ); process.waitFor(); } catch (Exception e) { e.printStackTrace(); } } } public class ThreadExample { public static void main (String[] args) { Thread thread1 = new Thread (() -> { for (int i = 0 ; i < 5 ; i++) { System.out.println("Thread 1: " + i); try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread thread2 = new Thread (() -> { for (int i = 0 ; i < 5 ; i++) { System.out.println("Thread 2: " + i); try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread1.start(); thread2.start(); } }
1.2 如何创建线程? Java中创建线程的方式有三种:
1.2.1 继承Thread类 步骤 :
继承Thread类
重写run()方法
创建线程对象并调用start()方法启动线程
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class MyThread extends Thread { @Override public void run () { for (int i = 0 ; i < 5 ; i++) { System.out.println(Thread.currentThread().getName() + ": " + i); try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main (String[] args) { MyThread thread1 = new MyThread (); thread1.setName("Thread-1" ); thread1.start(); MyThread thread2 = new MyThread (); thread2.setName("Thread-2" ); thread2.start(); } }
1.2.2 实现Runnable接口 步骤 :
实现Runnable接口
实现run()方法
创建Runnable对象
将Runnable对象作为参数传递给Thread构造函数
调用start()方法启动线程
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class MyRunnable implements Runnable { @Override public void run () { for (int i = 0 ; i < 5 ; i++) { System.out.println(Thread.currentThread().getName() + ": " + i); try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main (String[] args) { MyRunnable runnable = new MyRunnable (); Thread thread1 = new Thread (runnable, "Thread-1" ); thread1.start(); Thread thread2 = new Thread (runnable, "Thread-2" ); thread2.start(); } }
1.2.3 实现Callable接口 步骤 :
实现Callable接口
实现call()方法(可以返回结果)
创建Callable对象
将Callable对象包装成FutureTask
将FutureTask作为参数传递给Thread构造函数
调用start()方法启动线程
调用FutureTask的get()方法获取结果
示例 :
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 import java.util.concurrent.Callable;import java.util.concurrent.FutureTask;public class MyCallable implements Callable <Integer> { @Override public Integer call () throws Exception { int sum = 0 ; for (int i = 1 ; i <= 100 ; i++) { sum += i; } return sum; } public static void main (String[] args) { MyCallable callable = new MyCallable (); FutureTask<Integer> futureTask = new FutureTask <>(callable); Thread thread = new Thread (futureTask, "Callable-Thread" ); thread.start(); try { Integer result = futureTask.get(); System.out.println("Sum of 1-100: " + result); } catch (Exception e) { e.printStackTrace(); } } }
三种方式的对比 :
方式
优点
缺点
继承Thread类
实现简单,直接使用this获取当前线程
不能继承其他类,单继承限制
实现Runnable接口
可以继承其他类,更灵活
无法直接获取线程执行结果
实现Callable接口
可以返回执行结果,可以抛出异常
实现较复杂,需要FutureTask包装
1.3 线程的生命周期? 线程的生命周期包括以下五个状态:
1.3.1 新建(New) 状态说明 :线程被创建但尚未启动,此时线程对象已经创建,但还没有调用start()方法。
示例 :
1 2 3 4 Thread thread = new Thread (() -> { System.out.println("Thread running" ); });
1.3.2 就绪(Runnable) 状态说明 :线程调用了start()方法后进入就绪状态,此时线程可以运行,但尚未获取CPU时间片。
示例 :
1 2 3 4 Thread thread = new Thread (() -> { System.out.println("Thread running" ); }); thread.start();
1.3.3 运行(Running) 状态说明 :线程获取了CPU时间片,正在执行run()方法中的代码。
示例 :
1 2 3 4 5 6 7 8 9 Thread thread = new Thread (() -> { System.out.println("Thread running" ); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } }); thread.start();
1.3.4 阻塞(Blocked) 状态说明 :线程因某些原因暂停执行,放弃CPU时间片,直到阻塞原因消除后重新进入就绪状态。
阻塞的原因 :
等待获取锁:线程试图进入synchronized块或方法,但锁被其他线程持有
等待IO操作:线程等待IO操作完成
等待其他线程:线程调用了wait()方法,等待其他线程的通知
睡眠:线程调用了sleep()方法
示例 :
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 Object lock = new Object ();Thread thread1 = new Thread (() -> { synchronized (lock) { System.out.println("Thread1 got the lock" ); try { Thread.sleep(2000 ); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread thread2 = new Thread (() -> { System.out.println("Thread2 waiting for lock" ); synchronized (lock) { System.out.println("Thread2 got the lock" ); } }); thread1.start(); try { Thread.sleep(500 ); } catch (InterruptedException e) { e.printStackTrace(); } thread2.start();
1.3.5 终止(Terminated) 状态说明 :线程执行完毕或被终止,线程的生命周期结束。
终止的原因 :
正常终止:run()方法执行完毕
异常终止:run()方法抛出未捕获的异常
强制终止:调用了stop()方法(已废弃)或interrupt()方法
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Thread thread = new Thread (() -> { for (int i = 0 ; i < 5 ; i++) { System.out.println("Thread running: " + i); try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); break ; } } System.out.println("Thread finished" ); }); thread.start(); try { Thread.sleep(300 ); thread.interrupt(); } catch (InterruptedException e) { e.printStackTrace(); }
线程状态转换图 :
1 2 3 新建(New) → 就绪(Runnable) → 运行(Running) → 终止(Terminated) ↑ ↓ ← 阻塞(Blocked)
2. 线程同步 2.1 什么是线程同步?为什么需要线程同步? 线程同步是指多个线程在访问共享资源时,按照一定的顺序执行,避免数据竞争和不一致的问题。需要线程同步的原因是:当多个线程同时访问共享资源时,可能会导致数据不一致或其他并发问题。
2.2 Java中的线程同步方式有哪些? Java中的线程同步方式包括:
synchronized关键字:修饰方法或代码块
ReentrantLock:可重入锁
volatile关键字:保证变量的可见性
Atomic类:原子操作类
线程安全的集合类:如ConcurrentHashMap
信号量(Semaphore):控制并发访问的数量
倒计时门闩(CountDownLatch):等待多个线程完成
循环屏障(CyclicBarrier):等待所有线程到达某个点
读写锁(ReadWriteLock):允许多个读操作同时进行
2.3 synchronized和ReentrantLock的区别? synchronized和ReentrantLock 是Java中用于实现线程同步的两种主要方式,它们的区别如下:
特性
synchronized
ReentrantLock
实现方式
关键字,由JVM实现
类,由Java代码实现
锁类型
只能是非公平锁
可以是公平锁或非公平锁
锁状态
无法获取锁的状态
可以通过isLocked()等方法获取锁的状态
响应中断
不支持
支持,可通过lockInterruptibly()方法
尝试获取锁
不支持
支持,可通过tryLock()方法
释放锁
自动释放
必须手动释放(在finally块中)
条件变量
不支持
支持,可通过newCondition()方法
锁的范围
方法或代码块
代码块
示例 :
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 public class SynchronizedExample { private int count = 0 ; public synchronized void increment () { count++; } public void decrement () { synchronized (this ) { count--; } } public int getCount () { return count; } } import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample { private int count = 0 ; private ReentrantLock lock = new ReentrantLock (); public void increment () { lock.lock(); try { count++; } finally { lock.unlock(); } } public void decrement () { lock.lock(); try { count--; } finally { lock.unlock(); } } public boolean tryIncrement () { if (lock.tryLock()) { try { count++; return true ; } finally { lock.unlock(); } } return false ; } public int getCount () { return count; } } public class Test { public static void main (String[] args) throws InterruptedException { SynchronizedExample syncExample = new SynchronizedExample (); Thread thread1 = new Thread (() -> { for (int i = 0 ; i < 1000 ; i++) { syncExample.increment(); } }); Thread thread2 = new Thread (() -> { for (int i = 0 ; i < 1000 ; i++) { syncExample.decrement(); } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("Synchronized count: " + syncExample.getCount()); ReentrantLockExample lockExample = new ReentrantLockExample (); Thread thread3 = new Thread (() -> { for (int i = 0 ; i < 1000 ; i++) { lockExample.increment(); } }); Thread thread4 = new Thread (() -> { for (int i = 0 ; i < 1000 ; i++) { lockExample.decrement(); } }); thread3.start(); thread4.start(); thread3.join(); thread4.join(); System.out.println("ReentrantLock count: " + lockExample.getCount()); } }
使用场景 :
synchronized :适用于简单的同步场景,代码简洁,易于使用
ReentrantLock :适用于复杂的同步场景,需要更多的控制(如公平锁、尝试获取锁、响应中断等)
2.4 什么是死锁?如何避免死锁? 死锁 是指两个或多个线程互相等待对方释放资源,导致所有线程都无法继续执行的情况。
死锁的四个必要条件 :
互斥条件 :资源不能被多个线程同时使用
持有并等待条件 :线程持有资源的同时等待其他资源
不可剥夺条件 :资源不能被强制剥夺
循环等待条件 :线程之间形成循环等待关系
示例 :
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 public class DeadlockExample { private static final Object lock1 = new Object (); private static final Object lock2 = new Object (); public static void main (String[] args) { Thread thread1 = new Thread (() -> { synchronized (lock1) { System.out.println("Thread 1: Holding lock1" ); try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread 1: Waiting for lock2" ); synchronized (lock2) { System.out.println("Thread 1: Holding lock1 and lock2" ); } } }); Thread thread2 = new Thread (() -> { synchronized (lock2) { System.out.println("Thread 2: Holding lock2" ); try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread 2: Waiting for lock1" ); synchronized (lock1) { System.out.println("Thread 2: Holding lock2 and lock1" ); } } }); thread1.start(); thread2.start(); } }
避免死锁的方法 :
按顺序获取锁 :所有线程按照相同的顺序获取锁,避免循环等待
1 2 3 4 5 6 7 8 public void method () { synchronized (lock1) { synchronized (lock2) { } } }
使用超时机制 :使用tryLock()方法尝试获取锁,并设置超时时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ReentrantLock lock1 = new ReentrantLock ();ReentrantLock lock2 = new ReentrantLock ();public boolean method () { try { if (lock1.tryLock(1 , TimeUnit.SECONDS)) { try { if (lock2.tryLock(1 , TimeUnit.SECONDS)) { try { return true ; } finally { lock2.unlock(); } } } finally { lock1.unlock(); } } } catch (InterruptedException e) { e.printStackTrace(); } return false ; }
减少锁的范围 :只在必要的代码块上加锁,减少持有锁的时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public synchronized void method () { } public void method () { synchronized (this ) { } }
使用无锁数据结构 :如ConcurrentHashMap、Atomic类等
1 2 3 4 5 6 private AtomicInteger count = new AtomicInteger (0 );public void increment () { count.incrementAndGet(); }
使用Lock的tryLock()方法 :尝试获取锁,如果获取不到就放弃
1 2 3 4 5 6 7 8 9 10 11 12 13 ReentrantLock lock = new ReentrantLock ();public void method () { if (lock.tryLock()) { try { } finally { lock.unlock(); } } else { } }
使用线程池 :通过线程池管理线程,避免创建过多线程
使用信号量 :控制对资源的访问数量
死锁检测 :
使用jstack命令查看线程堆栈信息
使用jconsole或VisualVM等工具监控线程状态
3. 线程池 3.1 什么是线程池?为什么使用线程池? 线程池 是一种线程管理机制,它预先创建一组线程,用于执行提交的任务。线程池可以有效地管理线程资源,提高系统性能和稳定性。
1. 线程池的概念 :
线程池是由多个线程组成的集合,这些线程可以被重复使用
线程池维护着一个任务队列,用于存储待执行的任务
线程池负责管理线程的创建、调度和销毁
2. 使用线程池的原因 :
(1) 减少线程创建和销毁的开销 :
线程的创建和销毁是比较昂贵的操作,需要分配和释放系统资源
线程池预先创建线程,避免了频繁创建和销毁线程的开销
(2) 控制并发线程的数量 :
过多的线程会导致系统资源竞争,降低系统性能
线程池可以限制并发线程的数量,避免资源耗尽
(3) 提高线程的复用性 :
线程池中的线程可以重复执行多个任务,提高了线程的利用率
避免了为每个任务创建新线程的开销
(4) 便于管理和监控线程 :
线程池提供了统一的线程管理机制
可以监控线程池的状态,如线程数量、任务执行情况等
可以设置线程池的参数,如核心线程数、最大线程数等
(5) 提高系统的响应速度 :
线程池中的线程是预先创建的,当有任务提交时,可以立即执行
避免了线程创建的延迟,提高了系统的响应速度
3. 线程池的应用场景 :
Web服务器 :处理大量的HTTP请求
数据库连接池 :管理数据库连接
消息队列 :处理消息的消费
批量任务处理 :处理大量的计算任务
实时系统 :需要快速响应的系统
4. 示例 :
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;public class ThreadPoolExample { public static void main (String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(5 ); for (int i = 0 ; i < 10 ; i++) { final int taskId = i; executorService.submit(() -> { System.out.println("Task " + taskId + " is running on thread: " + Thread.currentThread().getName()); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Task " + taskId + " is completed" ); }); } executorService.shutdown(); } }
5. 线程池的优点总结 :
性能优化 :减少线程创建和销毁的开销
资源管理 :控制并发线程的数量,避免资源耗尽
提高响应速度 :线程预先创建,立即执行任务
可管理性 :统一管理线程,便于监控和调优
稳定性 :避免因线程过多导致的系统不稳定
6. 注意事项 :
线程池的大小应该根据系统资源和任务特性来设置
线程池需要及时关闭,避免资源泄漏
任务执行时间不宜过长,否则会阻塞线程池
应该合理处理任务执行过程中的异常
3.2 Java中的线程池有哪些? Java中提供了多种类型的线程池,以满足不同的应用场景。这些线程池都是通过Executors类创建的,主要包括以下几种:
1. FixedThreadPool(固定大小线程池) :
特点 :
线程池大小固定,核心线程数和最大线程数相等
当线程池中的线程都在工作时,新提交的任务会进入队列等待
队列使用无界队列(LinkedBlockingQueue)
适用场景 :
适用于处理稳定的并发任务,如Web服务器处理HTTP请求
适用于需要控制并发线程数量的场景
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class FixedThreadPoolExample { public static void main (String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(5 ); for (int i = 0 ; i < 10 ; i++) { final int taskId = i; executorService.submit(() -> { System.out.println("Task " + taskId + " is running on thread: " + Thread.currentThread().getName()); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } }); } executorService.shutdown(); } }
2. CachedThreadPool(可缓存线程池) :
特点 :
线程池大小不固定,核心线程数为0,最大线程数为Integer.MAX_VALUE
当有新任务提交时,如果没有空闲线程,会创建新线程
线程空闲时间超过60秒会被回收
队列使用SynchronousQueue,不存储任务
适用场景 :
适用于处理短期、突发的任务
适用于任务执行时间短、任务数量波动大的场景
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class CachedThreadPoolExample { public static void main (String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0 ; i < 10 ; i++) { final int taskId = i; executorService.submit(() -> { System.out.println("Task " + taskId + " is running on thread: " + Thread.currentThread().getName()); try { Thread.sleep(500 ); } catch (InterruptedException e) { e.printStackTrace(); } }); } executorService.shutdown(); } }
3. ScheduledThreadPool(定时任务线程池) :
特点 :
用于执行定时任务或周期性任务
核心线程数固定,最大线程数为Integer.MAX_VALUE
队列使用DelayedWorkQueue
适用场景 :
适用于需要定期执行任务的场景,如定时备份、定时清理等
适用于需要延迟执行任务的场景
示例 :
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 import java.util.concurrent.Executors;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.TimeUnit;public class ScheduledThreadPoolExample { public static void main (String[] args) { ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3 ); scheduledExecutorService.schedule(() -> { System.out.println("Delayed task executed at: " + System.currentTimeMillis()); }, 1 , TimeUnit.SECONDS); scheduledExecutorService.scheduleAtFixedRate(() -> { System.out.println("Periodic task executed at: " + System.currentTimeMillis()); }, 2 , 3 , TimeUnit.SECONDS); scheduledExecutorService.scheduleWithFixedDelay(() -> { System.out.println("Delayed periodic task executed at: " + System.currentTimeMillis()); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } }, 2 , 3 , TimeUnit.SECONDS); } }
4. SingleThreadPool(单线程线程池) :
特点 :
线程池只有一个线程
所有任务按照提交顺序执行
队列使用无界队列(LinkedBlockingQueue)
适用场景 :
适用于需要保证任务顺序执行的场景
适用于需要串行处理任务的场景
适用于需要线程安全的单线程操作
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class SingleThreadPoolExample { public static void main (String[] args) { ExecutorService executorService = Executors.newSingleThreadExecutor(); for (int i = 0 ; i < 10 ; i++) { final int taskId = i; executorService.submit(() -> { System.out.println("Task " + taskId + " is running on thread: " + Thread.currentThread().getName()); try { Thread.sleep(500 ); } catch (InterruptedException e) { e.printStackTrace(); } }); } executorService.shutdown(); } }
5. ForkJoinPool(分叉/合并线程池) :
特点 :
适用于处理可以分解为子任务的大规模任务
采用工作窃取算法,提高线程利用率
核心线程数默认为CPU核心数
适用场景 :
适用于处理大规模的计算任务,如排序、搜索等
适用于需要递归分解任务的场景
示例 :
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 51 import java.util.concurrent.ForkJoinPool;import java.util.concurrent.RecursiveTask;public class ForkJoinPoolExample { public static void main (String[] args) { ForkJoinPool forkJoinPool = new ForkJoinPool (); SumTask task = new SumTask (1 , 10000 ); Integer result = forkJoinPool.invoke(task); System.out.println("Sum: " + result); forkJoinPool.shutdown(); } static class SumTask extends RecursiveTask <Integer> { private static final int THRESHOLD = 1000 ; private int start; private int end; public SumTask (int start, int end) { this .start = start; this .end = end; } @Override protected Integer compute () { if (end - start <= THRESHOLD) { int sum = 0 ; for (int i = start; i <= end; i++) { sum += i; } return sum; } else { int mid = (start + end) / 2 ; SumTask leftTask = new SumTask (start, mid); SumTask rightTask = new SumTask (mid + 1 , end); leftTask.fork(); rightTask.fork(); return leftTask.join() + rightTask.join(); } } } }
6. 线程池的选择建议 :
线程池类型
核心线程数
最大线程数
队列类型
适用场景
FixedThreadPool
固定
与核心线程数相同
LinkedBlockingQueue
稳定的并发任务
CachedThreadPool
0
Integer.MAX_VALUE
SynchronousQueue
短期突发任务
ScheduledThreadPool
固定
Integer.MAX_VALUE
DelayedWorkQueue
定时或周期性任务
SingleThreadPool
1
1
LinkedBlockingQueue
顺序执行任务
ForkJoinPool
CPU核心数
CPU核心数
WorkQueue
大规模计算任务
7. 注意事项 :
Executors创建的线程池存在一些问题,如无界队列可能导致内存溢出
对于生产环境,建议使用ThreadPoolExecutor手动创建线程池,以更好地控制线程池参数
线程池的大小应该根据系统资源和任务特性来设置
线程池需要及时关闭,避免资源泄漏
3.3 线程池的核心参数有哪些? 线程池的核心参数是通过ThreadPoolExecutor类的构造函数来设置的,这些参数决定了线程池的行为和性能。以下是线程池的核心参数:
1. corePoolSize(核心线程数) :
作用 :
线程池中的核心线程数量,这些线程会一直存在,即使处于空闲状态
当提交任务时,如果当前线程数小于核心线程数,会创建新的核心线程来执行任务
取值建议 :
对于CPU密集型任务(如计算),核心线程数建议设置为CPU核心数
对于IO密集型任务(如网络IO、磁盘IO),核心线程数可以设置为CPU核心数的2-4倍
2. maximumPoolSize(最大线程数) :
作用 :
线程池可以创建的最大线程数量
当核心线程数已满,且任务队列也已满时,会创建非核心线程来执行任务
非核心线程在空闲时间超过keepAliveTime后会被回收
取值建议 :
对于CPU密集型任务,最大线程数建议设置为CPU核心数
对于IO密集型任务,最大线程数可以设置为CPU核心数的2-4倍
最大线程数应该大于或等于核心线程数
3. keepAliveTime(线程空闲时间) :
作用 :
非核心线程的空闲时间阈值
当非核心线程的空闲时间超过这个值时,会被回收
可以通过allowCoreThreadTimeOut(true)使核心线程也遵循这个规则
取值建议 :
对于执行时间短的任务,keepAliveTime可以设置得短一些
对于执行时间长的任务,keepAliveTime可以设置得长一些
通常设置为60秒
4. workQueue(任务队列) :
作用 :
用于存储待执行的任务
当核心线程数已满时,新提交的任务会进入任务队列
常见的队列类型 :
LinkedBlockingQueue :无界队列,可存储无限个任务,可能导致内存溢出
ArrayBlockingQueue :有界队列,可限制任务数量
SynchronousQueue :同步队列,不存储任务,直接传递给线程
PriorityBlockingQueue :优先级队列,根据任务优先级执行
取值建议 :
对于固定大小的线程池,建议使用有界队列,避免任务堆积导致内存溢出
对于可缓存的线程池,建议使用SynchronousQueue
5. threadFactory(线程工厂) :
作用 :
用于创建线程的工厂
可以自定义线程的名称、优先级、 daemon状态等
默认实现 :
Executors.defaultThreadFactory():创建的线程具有默认的名称和优先级
自定义线程工厂 :
可以通过实现ThreadFactory接口来自定义线程工厂
6. handler(拒绝策略) :
作用 :
常见的拒绝策略 :
AbortPolicy :默认策略,抛出RejectedExecutionException异常
CallerRunsPolicy :由提交任务的线程执行任务
DiscardPolicy :直接丢弃任务
DiscardOldestPolicy :丢弃队列中最旧的任务,然后尝试提交新任务
取值建议 :
对于关键任务,建议使用AbortPolicy,及时发现问题
对于非关键任务,建议使用DiscardPolicy或DiscardOldestPolicy
对于需要保证任务执行的场景,建议使用CallerRunsPolicy
7. 示例:手动创建线程池 :
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 import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.Executors;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;public class ThreadPoolParametersExample { public static void main (String[] args) { ThreadPoolExecutor executor = new ThreadPoolExecutor ( 5 , 10 , 60 , TimeUnit.SECONDS, new ArrayBlockingQueue <>(100 ), Executors.defaultThreadFactory(), new ThreadPoolExecutor .AbortPolicy() ); for (int i = 0 ; i < 150 ; i++) { final int taskId = i; executor.submit(() -> { System.out.println("Task " + taskId + " is running on thread: " + Thread.currentThread().getName()); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } }); } executor.shutdown(); } }
8. 线程池参数的调优建议 :
(1) 核心线程数和最大线程数 :
CPU密集型任务 :核心线程数 = CPU核心数
IO密集型任务 :核心线程数 = CPU核心数 * 2
(2) 任务队列 :
建议使用有界队列,避免任务堆积导致内存溢出
队列大小应该根据任务执行时间和系统资源来设置
(3) 拒绝策略 :
根据任务的重要性选择合适的拒绝策略
对于关键任务,应该使用能够及时发现问题的策略
(4) 线程工厂 :
自定义线程工厂,设置有意义的线程名称,便于调试和监控
(5) keepAliveTime :
根据任务的执行时间和频率来设置
对于短期任务,可以设置较短的keepAliveTime
9. 注意事项 :
线程池的参数设置应该根据具体的应用场景来调整
应该监控线程池的状态,如线程数量、任务队列大小等
线程池需要及时关闭,避免资源泄漏
避免使用Executors创建的线程池,因为它们可能存在资源耗尽的风险
3.4 线程池的工作原理? 线程池的工作原理是指线程池如何处理提交的任务,以及如何管理线程的生命周期。以下是线程池的详细工作原理:
1. 线程池的工作流程 :
当提交一个任务到线程池时,线程池会按照以下步骤处理:
步骤1:检查核心线程数
如果当前线程数小于核心线程数(corePoolSize),创建新的核心线程来执行任务
核心线程会一直存在,即使处于空闲状态(除非设置了allowCoreThreadTimeOut(true))
步骤2:检查任务队列
如果当前线程数达到核心线程数,将任务加入任务队列(workQueue)
任务队列用于存储待执行的任务
步骤3:检查最大线程数
如果任务队列已满,且当前线程数小于最大线程数(maximumPoolSize),创建非核心线程来执行任务
非核心线程在空闲时间超过keepAliveTime后会被回收
步骤4:执行拒绝策略
如果任务队列已满,且当前线程数达到最大线程数,执行拒绝策略(handler)
拒绝策略决定如何处理无法执行的任务
2. 线程池的状态 :
线程池有以下几种状态:
RUNNING :线程池处于运行状态,接受新任务并处理队列中的任务
SHUTDOWN :线程池处于关闭状态,不接受新任务,但处理队列中的任务
STOP :线程池处于停止状态,不接受新任务,不处理队列中的任务,中断正在执行的任务
TIDYING :所有任务已完成,线程数为0,准备进入TERMINATED状态
TERMINATED :线程池已终止,所有任务已完成,所有线程已退出
3. 线程池的状态转换 :
RUNNING → SHUTDOWN :调用shutdown()方法
RUNNING → STOP :调用shutdownNow()方法
SHUTDOWN → TIDYING :队列和线程池都为空
STOP → TIDYING :线程池为空
TIDYING → TERMINATED :调用terminated()钩子方法完成后
4. 线程池的执行机制 :
(1) 任务提交 :
通过execute()方法提交任务
通过submit()方法提交任务(返回Future对象)
(2) 任务执行 :
线程池中的线程从任务队列中获取任务执行
执行完成后,线程会继续从队列中获取任务,直到队列为空
(3) 线程管理 :
核心线程:一直存在,除非设置了allowCoreThreadTimeOut(true)
非核心线程:空闲时间超过keepAliveTime后被回收
5. 示例 :
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 import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;public class ThreadPoolWorkingPrincipleExample { public static void main (String[] args) { ThreadPoolExecutor executor = new ThreadPoolExecutor ( 2 , 4 , 60 , TimeUnit.SECONDS, new ArrayBlockingQueue <>(3 ) ); for (int i = 1 ; i <= 8 ; i++) { final int taskId = i; executor.execute(() -> { System.out.println("Task " + taskId + " is running on thread: " + Thread.currentThread().getName()); try { Thread.sleep(2000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Task " + taskId + " is completed" ); }); System.out.println("Task " + taskId + " is submitted" ); } executor.shutdown(); } }
执行过程分析 :
任务1、2:创建核心线程执行
任务3、4、5:进入任务队列
任务6、7:创建非核心线程执行(因为队列已满,且未达到最大线程数)
任务8:执行拒绝策略(因为队列已满,且已达到最大线程数)
6. 线程池的工作原理总结 :
任务提交 :通过execute()或submit()方法提交任务
核心线程处理 :如果核心线程未满,创建核心线程执行任务
队列存储 :如果核心线程已满,将任务加入队列
非核心线程处理 :如果队列已满,创建非核心线程执行任务
拒绝策略 :如果线程数达到最大值,执行拒绝策略
线程回收 :非核心线程空闲时间超过keepAliveTime后被回收
线程池关闭 :调用shutdown()或shutdownNow()方法关闭线程池
7. 线程池的性能优化 :
(1) 合理设置线程池参数 :
根据任务类型和系统资源设置核心线程数和最大线程数
使用有界队列,避免任务堆积
选择合适的拒绝策略
(2) 监控线程池状态 :
监控线程池的线程数量、任务队列大小、任务执行情况等
根据监控结果调整线程池参数
(3) 避免任务执行时间过长 :
任务执行时间过长会阻塞线程池
对于长时间运行的任务,考虑使用单独的线程池
(4) 合理处理异常 :
任务执行过程中的异常应该被捕获并处理
避免异常导致线程池中的线程意外终止
8. 注意事项 :
线程池的参数设置应该根据具体的应用场景来调整
应该监控线程池的状态,及时发现和解决问题
线程池需要及时关闭,避免资源泄漏
避免使用Executors创建的线程池,因为它们可能存在资源耗尽的风险
4. 并发工具类 4.1 什么是volatile关键字?它的作用是什么? volatile 是Java中的一个关键字,用于修饰变量。它的主要作用是保证变量的可见性和禁止指令重排序,从而确保多线程环境下的正确执行。
1. volatile的作用 :
(1) 保证变量的可见性 :
当一个线程修改了volatile变量的值,其他线程能够立即看到这个修改
这是因为volatile变量的修改会直接写入主内存,而不是缓存在线程的本地内存中
其他线程读取volatile变量时,会直接从主内存读取,而不是从本地内存读取
(2) 禁止指令重排序 :
编译器和处理器为了提高性能,会对指令进行重排序
volatile变量会禁止指令重排序,保证代码的执行顺序与程序的逻辑顺序一致
这对于依赖于执行顺序的代码(如双重检查锁定单例模式)非常重要
2. volatile的实现原理 :
(1) 内存屏障 :
volatile变量的读写操作会插入内存屏障
内存屏障会禁止指令重排序,确保内存操作的顺序
内存屏障会强制将本地内存中的修改刷新到主内存,以及从主内存读取最新值
(2) happens-before关系 :
对volatile变量的写操作 happens-before 后续对该变量的读操作
这保证了volatile变量的修改对其他线程的可见性
3. volatile的使用场景 :
(1) 状态标志 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class VolatileFlagExample { private volatile boolean stopFlag = false ; public void run () { while (!stopFlag) { System.out.println("Running..." ); } System.out.println("Stopped" ); } public void stop () { stopFlag = true ; } }
(2) 双重检查锁定单例模式 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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; } }
(3) 独立观察 :
1 2 3 4 5 6 7 8 9 10 11 public class VolatileObserverExample { private volatile int count = 0 ; public void increment () { count++; } public int getCount () { return count; } }
4. volatile与synchronized的区别 :
特性
volatile
synchronized
作用
保证可见性和禁止指令重排序
保证原子性、可见性和有序性
适用范围
变量
代码块或方法
性能
开销小
开销大
原子性
不保证原子性
保证原子性
阻塞
非阻塞
阻塞
5. volatile的局限性 :
(1) 不保证原子性 :
volatile变量的读写操作是原子的,但复合操作(如i++)不是原子的
示例:
1 2 3 4 5 6 7 8 9 10 11 12 public class VolatileAtomicityExample { private volatile int count = 0 ; public void increment () { count++; } public int getCount () { return count; } }
(2) 不适合复杂的同步场景 :
对于需要原子性的复合操作,应该使用synchronized或原子类
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import java.util.concurrent.atomic.AtomicInteger;public class AtomicExample { private AtomicInteger count = new AtomicInteger (0 ); public void increment () { count.incrementAndGet(); } public int getCount () { return count.get(); } }
6. 示例:volatile的可见性 :
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 public class VolatileVisibilityExample { private static volatile boolean flag = false ; public static void main (String[] args) { new Thread (() -> { System.out.println("Thread 1: Waiting for flag to be true" ); while (!flag) { } System.out.println("Thread 1: Flag is now true" ); }).start(); new Thread (() -> { try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread 2: Setting flag to true" ); flag = true ; }).start(); } }
7. 示例:volatile禁止指令重排序 :
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 public class VolatileReorderingExample { private static int a = 0 , b = 0 ; private static int x = 0 , y = 0 ; public static void main (String[] args) throws InterruptedException { for (int i = 0 ; i < 100000 ; i++) { a = 0 ; b = 0 ; x = 0 ; y = 0 ; Thread t1 = new Thread (() -> { a = 1 ; x = b; }); Thread t2 = new Thread (() -> { b = 1 ; y = a; }); t1.start(); t2.start(); t1.join(); t2.join(); if (x == 0 && y == 0 ) { System.out.println("Reordering detected: x = " + x + ", y = " + y); break ; } } } }
8. 注意事项 :
volatile适用于简单的状态标志和独立观察场景
对于需要原子性的复合操作,应该使用synchronized或原子类
volatile不能替代synchronized,它们有不同的适用场景
合理使用volatile可以提高程序的性能,避免不必要的同步开销
4.2 什么是原子类?Java中的原子类有哪些? 原子类 是Java中提供的线程安全的类,用于执行原子操作。原子操作是指不可被中断的操作,要么全部执行成功,要么全部不执行,不会出现中间状态。
1. 原子类的概念 :
原子类基于CAS(Compare And Swap)操作实现
原子类提供了线程安全的操作,无需使用synchronized关键字
原子类的性能通常比synchronized高
2. Java中的原子类 :
Java中的原子类主要位于java.util.concurrent.atomic包中,包括以下几类:
(1) 基本类型原子类 :
AtomicInteger :原子整数
AtomicLong :原子长整数
AtomicBoolean :原子布尔值
(2) 引用类型原子类 :
AtomicReference :原子引用
AtomicStampedReference :带版本号的原子引用,解决ABA问题
AtomicMarkableReference :带标记的原子引用
(3) 数组类型原子类 :
AtomicIntegerArray :原子整数数组
AtomicLongArray :原子长整数数组
AtomicReferenceArray :原子引用数组
(4) 字段更新器 :
AtomicIntegerFieldUpdater :原子更新整数字段
AtomicLongFieldUpdater :原子更新长整数字段
AtomicReferenceFieldUpdater :原子更新引用字段
3. 原子类的使用示例 :
(1) AtomicInteger示例 :
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.atomic.AtomicInteger;public class AtomicIntegerExample { private static AtomicInteger count = new AtomicInteger (0 ); public static void main (String[] args) { int result1 = count.incrementAndGet(); System.out.println("After incrementAndGet: " + result1); int result2 = count.decrementAndGet(); System.out.println("After decrementAndGet: " + result2); int result3 = count.getAndIncrement(); System.out.println("After getAndIncrement: " + result3); System.out.println("Current value: " + count.get()); int result4 = count.getAndDecrement(); System.out.println("After getAndDecrement: " + result4); System.out.println("Current value: " + count.get()); count.set(10 ); System.out.println("After set: " + count.get()); int result5 = count.getAndSet(20 ); System.out.println("After getAndSet: " + result5); System.out.println("Current value: " + count.get()); boolean success = count.compareAndSet(20 , 30 ); System.out.println("CompareAndSet success: " + success); System.out.println("Current value: " + count.get()); success = count.compareAndSet(20 , 40 ); System.out.println("CompareAndSet success: " + success); System.out.println("Current value: " + count.get()); } }
(2) AtomicReference示例 :
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 import java.util.concurrent.atomic.AtomicReference;public class AtomicReferenceExample { static class User { private String name; private int age; public User (String name, int age) { this .name = name; this .age = age; } @Override public String toString () { return "User{name='" + name + "', age=" + age + "}" ; } } public static void main (String[] args) { AtomicReference<User> atomicReference = new AtomicReference <>(); User user1 = new User ("Alice" , 25 ); atomicReference.set(user1); System.out.println("Current user: " + atomicReference.get()); User user2 = new User ("Bob" , 30 ); boolean success = atomicReference.compareAndSet(user1, user2); System.out.println("CompareAndSet success: " + success); System.out.println("Current user: " + atomicReference.get()); User user3 = new User ("Charlie" , 35 ); success = atomicReference.compareAndSet(user1, user3); System.out.println("CompareAndSet success: " + success); System.out.println("Current user: " + atomicReference.get()); } }
(3) AtomicStampedReference示例 :
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 import java.util.concurrent.atomic.AtomicStampedReference;public class AtomicStampedReferenceExample { public static void main (String[] args) { AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference <>(100 , 0 ); int currentValue = atomicStampedReference.getReference(); int currentStamp = atomicStampedReference.getStamp(); System.out.println("Current value: " + currentValue + ", stamp: " + currentStamp); boolean success = atomicStampedReference.compareAndSet(100 , 200 , 0 , 1 ); System.out.println("CompareAndSet success: " + success); System.out.println("Current value: " + atomicStampedReference.getReference() + ", stamp: " + atomicStampedReference.getStamp()); success = atomicStampedReference.compareAndSet(200 , 300 , 0 , 2 ); System.out.println("CompareAndSet success: " + success); System.out.println("Current value: " + atomicStampedReference.getReference() + ", stamp: " + atomicStampedReference.getStamp()); success = atomicStampedReference.compareAndSet(200 , 300 , 1 , 2 ); System.out.println("CompareAndSet success: " + success); System.out.println("Current value: " + atomicStampedReference.getReference() + ", stamp: " + atomicStampedReference.getStamp()); } }
4. 原子类的实现原理 :
(1) CAS操作 :
CAS(Compare And Swap)是一种原子操作,它比较内存中的值与预期值,如果相等则更新为新值
CAS操作由硬件支持,是原子的
CAS操作的三个参数:内存地址、预期值、新值
(2) ABA问题 :
ABA问题是指一个值从A变为B,再变回A,导致CAS操作误判
解决方法:使用AtomicStampedReference,通过版本号来避免ABA问题
5. 原子类的优缺点 :
优点 :
线程安全,无需使用synchronized
性能高,避免了线程上下文切换的开销
提供了丰富的原子操作
缺点 :
只能保证单个变量的原子性
对于复杂的复合操作,需要使用其他同步机制
可能会导致ABA问题(使用AtomicStampedReference可以解决)
6. 原子类的适用场景 :
计数器 :如统计网站访问量、请求次数等
状态标志 :如线程的运行状态
并发安全的单例模式 :使用AtomicReference实现
无锁数据结构 :如无锁队列、无锁栈等
7. 注意事项 :
原子类适用于简单的并发场景
对于复杂的并发操作,可能需要使用其他同步机制
合理使用原子类可以提高程序的性能
注意ABA问题,必要时使用AtomicStampedReference
4.3 什么是ThreadLocal?它的作用是什么? ThreadLocal 是Java中提供的一个线程本地变量工具类,它允许每个线程拥有自己独立的变量副本,从而避免线程安全问题和简化线程间的数据传递。
1. ThreadLocal的概念 :
ThreadLocal为每个线程创建一个独立的变量副本
线程可以通过ThreadLocal获取和设置自己的变量副本
不同线程之间的变量副本互不影响
2. ThreadLocal的作用 :
(1) 为每个线程提供独立的变量副本 :
每个线程都有自己的变量副本,线程间不会互相干扰
避免了线程安全问题
(2) 简化线程间的数据传递 :
可以在不同方法之间传递数据,而不需要通过方法参数
适用于在同一个线程内的不同组件之间共享数据
(3) 避免参数传递的繁琐 :
当多个方法需要使用同一个变量时,可以使用ThreadLocal来存储,避免参数传递
3. ThreadLocal的使用示例 :
(1) 基本使用 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class ThreadLocalExample { private static ThreadLocal<String> threadLocal = new ThreadLocal <>(); public static void main (String[] args) { new Thread (() -> { threadLocal.set("Thread 1 value" ); System.out.println("Thread 1: " + threadLocal.get()); threadLocal.remove(); }).start(); new Thread (() -> { threadLocal.set("Thread 2 value" ); System.out.println("Thread 2: " + threadLocal.get()); threadLocal.remove(); }).start(); } }
(2) 在Web应用中的使用 :
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 public class UserContext { private static ThreadLocal<User> userThreadLocal = new ThreadLocal <>(); public static void setUser (User user) { userThreadLocal.set(user); } public static User getUser () { return userThreadLocal.get(); } public static void removeUser () { userThreadLocal.remove(); } } public class UserFilter implements Filter { @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { User user = getUserFromRequest(request); UserContext.setUser(user); try { chain.doFilter(request, response); } finally { UserContext.removeUser(); } } } @Service public class UserService { public void process () { User user = UserContext.getUser(); System.out.println("Processing for user: " + user.getUsername()); } }
4. ThreadLocal的实现原理 :
(1) 内部结构 :
ThreadLocal内部维护了一个ThreadLocalMap,每个线程都有自己的ThreadLocalMap
ThreadLocalMap是一个特殊的Map,使用ThreadLocal作为key,存储线程的变量副本
(2) 核心方法 :
**set(T value)**:设置当前线程的变量副本
**get()**:获取当前线程的变量副本
**remove()**:移除当前线程的变量副本
(3) 工作原理 :
当调用set()方法时,ThreadLocal会获取当前线程的ThreadLocalMap,并将变量存储到其中
当调用get()方法时,ThreadLocal会获取当前线程的ThreadLocalMap,并从中获取变量
当调用remove()方法时,ThreadLocal会从当前线程的ThreadLocalMap中移除变量
5. ThreadLocal的内存泄漏问题 :
(1) 内存泄漏的原因 :
ThreadLocalMap中的Entry使用ThreadLocal作为key,并且是弱引用(WeakReference)
当ThreadLocal对象被回收后,Entry的key会变为null
如果线程没有结束,ThreadLocalMap中的Entry会一直存在,导致内存泄漏
(2) 如何避免内存泄漏 :
使用完毕后调用remove()方法清理资源
在使用ThreadLocal的地方,确保在finally块中调用remove()方法
6. ThreadLocal的优缺点 :
优点 :
线程安全,无需使用synchronized
简化线程间的数据传递
避免参数传递的繁琐
缺点 :
可能导致内存泄漏(需要手动调用remove()方法)
不适合在多线程间共享数据
可能会导致代码可读性下降
7. ThreadLocal的适用场景 :
Web应用 :存储用户会话信息
数据库连接 :为每个线程提供独立的数据库连接
事务管理 :存储事务上下文
日期格式化 :为每个线程提供独立的SimpleDateFormat实例
8. 注意事项 :
使用完毕后一定要调用remove()方法清理资源,避免内存泄漏
不要在ThreadLocal中存储大对象,以免占用过多内存
注意ThreadLocal的作用域,避免在不需要的地方使用
理解ThreadLocal的实现原理,避免滥用
4.4 什么是CountDownLatch和CyclicBarrier?它们的区别是什么? CountDownLatch 和CyclicBarrier 都是Java中提供的并发工具类,用于协调多个线程的执行。它们的主要作用是让线程等待其他线程完成或到达某个点。
1. CountDownLatch :
(1) 概念 :
CountDownLatch是一个倒计时门闩,用于等待多个线程完成
它通过一个计数器来实现,当计数器减到0时,等待的线程被唤醒
(2) 核心方法 :
**CountDownLatch(int count)**:构造方法,设置计数器的初始值
**await()**:等待计数器减到0
**countDown()**:计数器减1
(3) 使用场景 :
等待多个线程完成后再继续执行
主线程等待所有子线程完成后再结束
(4) 示例 :
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 import java.util.concurrent.CountDownLatch;public class CountDownLatchExample { public static void main (String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch (3 ); for (int i = 1 ; i <= 3 ; i++) { final int taskId = i; new Thread (() -> { System.out.println("Task " + taskId + " is running" ); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Task " + taskId + " is completed" ); latch.countDown(); }).start(); } System.out.println("Main thread is waiting for all tasks to complete" ); latch.await(); System.out.println("All tasks are completed, main thread continues" ); } }
2. CyclicBarrier :
(1) 概念 :
CyclicBarrier是一个循环屏障,用于等待所有线程到达某个点
当所有线程到达后,屏障被打开,所有线程继续执行
它的计数器可以重置,因此可以重复使用
(2) 核心方法 :
**CyclicBarrier(int parties)**:构造方法,设置参与的线程数
**CyclicBarrier(int parties, Runnable barrierAction)**:构造方法,设置参与的线程数和屏障打开时执行的任务
**await()**:等待所有线程到达
**reset()**:重置计数器
(3) 使用场景 :
多个线程需要在某个点同步后再继续执行
多个线程需要分阶段执行任务,每个阶段完成后再进入下一阶段
(4) 示例 :
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 import java.util.concurrent.CyclicBarrier;public class CyclicBarrierExample { public static void main (String[] args) { CyclicBarrier barrier = new CyclicBarrier (3 , () -> { System.out.println("All threads have arrived at the barrier" ); }); for (int i = 1 ; i <= 3 ; i++) { final int threadId = i; new Thread (() -> { try { System.out.println("Thread " + threadId + " is running" ); Thread.sleep(1000 ); System.out.println("Thread " + threadId + " has arrived at the barrier" ); barrier.await(); System.out.println("Thread " + threadId + " continues after the barrier" ); } catch (Exception e) { e.printStackTrace(); } }).start(); } } }
3. CountDownLatch和CyclicBarrier的区别 :
特性
CountDownLatch
CyclicBarrier
计数器使用
计数器只能使用一次
计数器可以重置,重复使用
等待方式
等待其他线程完成
等待所有线程到达某个点
实现原理
基于AQS(AbstractQueuedSynchronizer)
基于ReentrantLock和Condition
使用场景
主线程等待所有子线程完成
多个线程相互等待,同步后继续执行
线程角色
有等待线程和计数线程之分
所有线程都是等待线程
异常处理
计数线程异常不影响其他线程
一个线程异常会导致所有线程异常
屏障动作
无屏障动作
可以设置屏障打开时执行的任务
4. 适用场景对比 :
CountDownLatch适用场景 :
主线程等待多个子线程完成初始化
多个线程等待某个资源准备就绪
实现多个线程的协调执行
CyclicBarrier适用场景 :
多个线程分阶段执行任务,每个阶段完成后同步
多线程计算,最后合并结果
模拟并发测试
5. 注意事项 :
CountDownLatch :
计数器必须减到0,否则等待的线程会一直阻塞
计数器不能重置,只能使用一次
适用于一次性的同步场景
CyclicBarrier :
所有线程必须调用await()方法,否则其他线程会一直阻塞
可以通过reset()方法重置计数器,重复使用
适用于需要多次同步的场景
一个线程异常会导致所有线程异常,需要注意异常处理
5. 并发编程的其他问题 5.1 什么是乐观锁和悲观锁? 乐观锁 和悲观锁 是并发编程中两种不同的锁策略,用于解决多线程并发访问共享资源时的冲突问题。
1. 乐观锁 :
(1) 概念 :
乐观锁假设不会发生冲突,因此在访问资源时不会立即获取锁
只有在更新资源时才检查是否发生冲突
如果发生冲突,则重试或采取其他策略
(2) 实现方式 :
版本号机制 :为数据添加版本号,每次更新时版本号加1,更新前检查版本号是否一致
CAS操作 :使用Compare And Swap操作,比较内存中的值与预期值,如果相等则更新
(3) 适用场景 :
读操作远多于写操作的场景
冲突概率较低的场景
对性能要求较高的场景
(4) 示例 :
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 public class OptimisticLockExample { private int value; private int version; public synchronized boolean update (int newValue, int expectedVersion) { if (version == expectedVersion) { value = newValue; version++; return true ; } return false ; } public synchronized int getValue () { return value; } public synchronized int getVersion () { return version; } } import java.util.concurrent.atomic.AtomicInteger;public class CASExample { private AtomicInteger value = new AtomicInteger (0 ); public void increment () { int current; do { current = value.get(); } while (!value.compareAndSet(current, current + 1 )); } public int getValue () { return value.get(); } }
2. 悲观锁 :
(1) 概念 :
悲观锁假设会发生冲突,因此在访问资源前先获取锁
确保同一时间只有一个线程能访问资源
其他线程需要等待锁释放后才能访问
(2) 实现方式 :
synchronized关键字 :Java内置的锁机制
ReentrantLock :可重入锁,提供更多的锁操作
读写锁 :ReadWriteLock,支持读写分离
(3) 适用场景 :
写操作远多于读操作的场景
冲突概率较高的场景
对数据一致性要求较高的场景
(4) 示例 :
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 public class PessimisticLockExample { private int value; public synchronized void increment () { value++; } public synchronized int getValue () { return value; } } import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample { private int value; private ReentrantLock lock = new ReentrantLock (); public void increment () { lock.lock(); try { value++; } finally { lock.unlock(); } } public int getValue () { lock.lock(); try { return value; } finally { lock.unlock(); } } }
3. 乐观锁和悲观锁的区别 :
特性
乐观锁
悲观锁
锁策略
假设不会发生冲突
假设会发生冲突
实现方式
版本号机制、CAS操作
synchronized、ReentrantLock
适用场景
读多写少,冲突概率低
写多读少,冲突概率高
性能
无锁操作,性能高
有锁操作,性能较低
复杂度
实现复杂,需要处理冲突
实现简单,直接获取锁
死锁
不会发生死锁
可能发生死锁
4. 乐观锁和悲观锁的选择 :
选择乐观锁的情况 :
读操作远多于写操作
冲突概率较低
对性能要求较高
数据一致性要求不是特别严格
选择悲观锁的情况 :
写操作远多于读操作
冲突概率较高
对数据一致性要求较高
实现简单,易于理解
5. 注意事项 :
乐观锁需要处理冲突,可能导致重试次数过多,影响性能
悲观锁可能导致线程阻塞,影响并发性能
在高并发场景下,应根据具体情况选择合适的锁策略
可以结合使用两种锁策略,例如:读操作使用乐观锁,写操作使用悲观锁
5.2 什么是CAS操作?它的优缺点是什么? CAS(Compare And Swap) 是一种原子操作,它比较内存中的值与预期值,如果相等则更新为新值。CAS操作是实现乐观锁的核心技术,也是许多并发工具类的基础。
1. CAS操作的工作原理 :
操作过程 :CAS操作包含三个参数:内存地址(V)、预期值(A)和新值(B)。当且仅当内存地址V的值等于预期值A时,才将内存地址V的值更新为B,否则不做任何操作。
原子性 :CAS操作是由硬件支持的原子操作,确保了操作的原子性。
返回值 :CAS操作返回一个布尔值,表示操作是否成功。
2. CAS操作的实现 :
在Java中,CAS操作主要通过Unsafe类实现,该类提供了底层的CAS操作方法。例如,AtomicInteger类就是使用CAS操作来实现原子更新的。
3. CAS操作的示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import java.util.concurrent.atomic.AtomicInteger;public class CASExample { private static AtomicInteger count = new AtomicInteger (0 ); public static void main (String[] args) { boolean success = count.compareAndSet(0 , 1 ); System.out.println("First CAS: " + success + ", count = " + count.get()); success = count.compareAndSet(0 , 2 ); System.out.println("Second CAS: " + success + ", count = " + count.get()); success = count.compareAndSet(1 , 2 ); System.out.println("Third CAS: " + success + ", count = " + count.get()); } }
4. CAS操作的优点 :
无锁操作 :CAS操作不需要获取锁,避免了线程上下文切换的开销,性能更高。
非阻塞 :CAS操作不会阻塞线程,即使操作失败也会立即返回,不会导致线程挂起。
原子性 :CAS操作是原子的,确保了操作的正确性。
适用范围广 :CAS操作是许多并发工具类的基础,如原子类、ConcurrentHashMap等。
5. CAS操作的缺点 :
ABA问题 :
概念:当一个值从A变为B,再变回A时,CAS操作会误判为没有发生变化。
解决方法:使用版本号或时间戳,如AtomicStampedReference。
自旋时间过长 :
概念:当CAS操作失败时,线程会不断重试,导致CPU资源浪费。
解决方法:设置重试次数上限,或使用Backoff策略。
只能保证单个变量的原子性 :
概念:CAS操作只能保证单个变量的原子性,无法保证多个变量的原子性。
解决方法:使用锁或其他同步机制。
开销 :
概念:CAS操作需要不断比较和尝试,可能会导致缓存一致性流量增加。
解决方法:合理使用CAS操作,避免频繁的CAS操作。
6. ABA问题的解决 :
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 import java.util.concurrent.atomic.AtomicStampedReference;public class ABASolutionExample { public static void main (String[] args) { AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference <>(100 , 0 ); new Thread (() -> { int [] stampHolder = new int [1 ]; int currentValue = atomicStampedReference.get(stampHolder); int currentStamp = stampHolder[0 ]; System.out.println("Thread 1: Current value = " + currentValue + ", stamp = " + currentStamp); atomicStampedReference.compareAndSet(100 , 200 , currentStamp, currentStamp + 1 ); System.out.println("Thread 1: After first update - value = " + atomicStampedReference.getReference() + ", stamp = " + atomicStampedReference.getStamp()); int [] stampHolder2 = new int [1 ]; currentValue = atomicStampedReference.get(stampHolder2); currentStamp = stampHolder2[0 ]; atomicStampedReference.compareAndSet(200 , 100 , currentStamp, currentStamp + 1 ); System.out.println("Thread 1: After second update - value = " + atomicStampedReference.getReference() + ", stamp = " + atomicStampedReference.getStamp()); }).start(); new Thread (() -> { try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } int [] stampHolder = new int [1 ]; int currentValue = atomicStampedReference.get(stampHolder); int currentStamp = stampHolder[0 ]; System.out.println("Thread 2: Current value = " + currentValue + ", stamp = " + currentStamp); boolean success = atomicStampedReference.compareAndSet(100 , 300 , 0 , 1 ); System.out.println("Thread 2: CAS success = " + success + ", value = " + atomicStampedReference.getReference() + ", stamp = " + atomicStampedReference.getStamp()); }).start(); } }
7. CAS操作的适用场景 :
计数器 :如AtomicInteger、AtomicLong等。
并发安全的单例模式 :使用AtomicReference实现。
无锁数据结构 :如ConcurrentLinkedQueue、ConcurrentHashMap等。
乐观锁 :实现版本号机制。
8. 注意事项 :
合理使用CAS操作,避免频繁的CAS操作导致性能下降。
注意ABA问题,必要时使用AtomicStampedReference。
对于复杂的并发操作,可能需要使用锁或其他同步机制。
理解CAS操作的原理,避免滥用。
5.3 什么是线程安全?如何实现线程安全? 线程安全 是指多个线程同时访问共享资源时,不会导致数据不一致、数据丢失或其他问题,保证程序的正确性和可靠性。
1. 线程安全的概念 :
数据一致性 :多个线程访问共享资源时,数据的状态是一致的,不会出现部分更新的情况。
原子性 :操作要么全部执行成功,要么全部不执行,不会出现中间状态。
可见性 :一个线程对共享资源的修改,其他线程能够立即看到。
有序性 :程序的执行顺序与代码的逻辑顺序一致,不会出现指令重排序。
2. 实现线程安全的方法 :
(1) 使用synchronized关键字 :
概念 :synchronized是Java内置的同步机制,用于保证方法或代码块的原子性。
原理 :synchronized通过获取对象的锁来实现同步,同一时间只有一个线程能够获取锁并执行代码。
使用方式 :
修饰方法:public synchronized void method() { ... }
修饰代码块:synchronized (object) { ... }
优点 :简单易用,无需手动释放锁。
缺点 :性能较低,不支持超时和中断。
(2) 使用ReentrantLock :
概念 :ReentrantLock是Java提供的可重入锁,实现了Lock接口。
原理 :ReentrantLock通过AQS(AbstractQueuedSynchronizer)实现,提供了更灵活的锁操作。
使用方式 :1 2 3 4 5 6 7 ReentrantLock lock = new ReentrantLock ();lock.lock(); try { } finally { lock.unlock(); }
优点 :支持超时、中断,提供了公平锁和非公平锁的选择。
缺点 :需要手动释放锁,代码复杂度较高。
(3) 使用volatile关键字 :
概念 :volatile是Java中的关键字,用于保证变量的可见性和禁止指令重排序。
原理 :volatile变量的修改会直接写入主内存,其他线程读取时会直接从主内存读取。
使用方式 :private volatile int count = 0;
优点 :性能高,无需获取锁。
缺点 :不保证原子性,只适用于简单的状态标志。
(4) 使用原子类 :
概念 :原子类是Java提供的线程安全的类,用于执行原子操作。
原理 :原子类基于CAS(Compare And Swap)操作实现,保证操作的原子性。
使用方式 :AtomicInteger count = new AtomicInteger(0);
优点 :线程安全,性能高。
缺点 :只能保证单个变量的原子性。
(5) 使用线程安全的集合类 :
概念 :Java提供了线程安全的集合类,如ConcurrentHashMap、CopyOnWriteArrayList等。
原理 :线程安全的集合类通过内部的同步机制保证线程安全。
使用方式 :ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
优点 :使用方便,无需手动同步。
缺点 :性能可能比非线程安全的集合类低。
(6) 使用线程本地变量 :
概念 :ThreadLocal是Java提供的线程本地变量工具类,每个线程都有自己的变量副本。
原理 :ThreadLocal为每个线程创建一个独立的变量副本,线程间互不影响。
使用方式 :private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
优点 :线程安全,无需同步。
缺点 :可能导致内存泄漏,需要手动清理。
3. 线程安全实现方法的对比 :
方法
优点
缺点
适用场景
synchronized
简单易用,无需手动释放锁
性能较低,不支持超时和中断
简单的同步场景
ReentrantLock
支持超时、中断,提供公平锁选择
需要手动释放锁,代码复杂度高
复杂的同步场景
volatile
性能高,无需获取锁
不保证原子性
简单的状态标志
原子类
线程安全,性能高
只能保证单个变量的原子性
计数器、状态标志
线程安全的集合类
使用方便,无需手动同步
性能可能较低
多线程环境下的集合操作
线程本地变量
线程安全,无需同步
可能导致内存泄漏
线程间数据隔离
4. 线程安全的最佳实践 :
最小化同步范围 :只同步必要的代码块,减少锁的竞争。
选择合适的同步机制 :根据具体场景选择合适的线程安全实现方法。
使用不可变对象 :不可变对象天然是线程安全的。
避免共享可变状态 :尽量减少共享可变状态的使用。
使用线程安全的集合类 :在多线程环境下使用线程安全的集合类。
合理使用锁 :避免死锁和活锁,合理设置锁的粒度。
5. 示例 :
(1) 使用synchronized实现线程安全 :
1 2 3 4 5 6 7 8 9 10 11 public class SynchronizedExample { private int count = 0 ; public synchronized void increment () { count++; } public synchronized int getCount () { return count; } }
(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 import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample { private int count = 0 ; private ReentrantLock lock = new ReentrantLock (); public void increment () { lock.lock(); try { count++; } finally { lock.unlock(); } } public int getCount () { lock.lock(); try { return count; } finally { lock.unlock(); } } }
(3) 使用原子类实现线程安全 :
1 2 3 4 5 6 7 8 9 10 11 12 13 import java.util.concurrent.atomic.AtomicInteger;public class AtomicExample { private AtomicInteger count = new AtomicInteger (0 ); public void increment () { count.incrementAndGet(); } public int getCount () { return count.get(); } }
6. 注意事项 :
线程安全的实现需要根据具体场景选择合适的方法。
避免过度同步,以免影响性能。
注意锁的粒度,避免锁竞争。
注意线程安全的集合类的使用场景。
合理使用线程本地变量,避免内存泄漏。
5.4 什么是生产者-消费者模式?如何实现? 生产者-消费者模式 是一种设计模式,它将生产和消费分离,通过缓冲区进行通信,从而实现解耦和提高系统的并发性能。
1. 生产者-消费者模式的概念 :
生产者 :负责生成数据的线程或进程
消费者 :负责处理数据的线程或进程
缓冲区 :用于存储生产者生成的数据,供消费者使用
解耦 :生产者和消费者之间通过缓冲区进行通信,彼此不直接依赖
平衡 :缓冲区可以平衡生产者和消费者的速度差异
2. 生产者-消费者模式的实现方式 :
(1) 使用wait()和notify()方法 :
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 51 52 53 public class ProducerConsumerExample { private final int MAX_SIZE = 10 ; private final List<Integer> buffer = new ArrayList <>(); public synchronized void produce (int value) throws InterruptedException { while (buffer.size() == MAX_SIZE) { System.out.println("Buffer is full, producer is waiting" ); wait(); } buffer.add(value); System.out.println("Produced: " + value); notify(); } public synchronized int consume () throws InterruptedException { while (buffer.isEmpty()) { System.out.println("Buffer is empty, consumer is waiting" ); wait(); } int value = buffer.remove(0 ); System.out.println("Consumed: " + value); notify(); return value; } public static void main (String[] args) { ProducerConsumerExample example = new ProducerConsumerExample (); new Thread (() -> { for (int i = 1 ; i <= 20 ; i++) { try { example.produce(i); Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread (() -> { for (int i = 1 ; i <= 20 ; i++) { try { example.consume(); Thread.sleep(200 ); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } }
(2) 使用BlockingQueue :
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 import java.util.concurrent.BlockingQueue;import java.util.concurrent.LinkedBlockingQueue;public class BlockingQueueExample { private final int MAX_SIZE = 10 ; private final BlockingQueue<Integer> queue = new LinkedBlockingQueue <>(MAX_SIZE); public void produce (int value) throws InterruptedException { queue.put(value); System.out.println("Produced: " + value); } public int consume () throws InterruptedException { int value = queue.take(); System.out.println("Consumed: " + value); return value; } public static void main (String[] args) { BlockingQueueExample example = new BlockingQueueExample (); new Thread (() -> { for (int i = 1 ; i <= 20 ; i++) { try { example.produce(i); Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread (() -> { for (int i = 1 ; i <= 20 ; i++) { try { example.consume(); Thread.sleep(200 ); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } }
(3) 使用信号量 :
原理 :使用Semaphore来控制对缓冲区的访问
实现步骤 :
使用一个信号量控制空闲空间的数量
使用另一个信号量控制已使用空间的数量
生产者获取空闲空间信号量,生产数据后释放已使用空间信号量
消费者获取已使用空间信号量,消费数据后释放空闲空间信号量
示例 :
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 51 52 53 54 55 56 import java.util.concurrent.Semaphore;import java.util.concurrent.atomic.AtomicInteger;public class SemaphoreExample { private final int MAX_SIZE = 10 ; private final int [] buffer = new int [MAX_SIZE]; private final AtomicInteger in = new AtomicInteger (0 ); private final AtomicInteger out = new AtomicInteger (0 ); private final Semaphore empty = new Semaphore (MAX_SIZE); private final Semaphore full = new Semaphore (0 ); public void produce (int value) throws InterruptedException { empty.acquire(); buffer[in.get() % MAX_SIZE] = value; System.out.println("Produced: " + value + " at position " + (in.get() % MAX_SIZE)); in.incrementAndGet(); full.release(); } public int consume () throws InterruptedException { full.acquire(); int value = buffer[out.get() % MAX_SIZE]; System.out.println("Consumed: " + value + " from position " + (out.get() % MAX_SIZE)); out.incrementAndGet(); empty.release(); return value; } public static void main (String[] args) { SemaphoreExample example = new SemaphoreExample (); new Thread (() -> { for (int i = 1 ; i <= 20 ; i++) { try { example.produce(i); Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread (() -> { for (int i = 1 ; i <= 20 ; i++) { try { example.consume(); Thread.sleep(200 ); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } }
3. 生产者-消费者模式的优缺点 :
优点 :
解耦 :生产者和消费者之间通过缓冲区进行通信,彼此不直接依赖
平衡 :缓冲区可以平衡生产者和消费者的速度差异
并发 :生产者和消费者可以并行执行,提高系统的并发性能
可扩展性 :可以根据需要增加生产者或消费者的数量
缺点 :
缓冲区溢出 :如果生产者的速度远快于消费者,可能导致缓冲区溢出
死锁 :如果实现不当,可能导致死锁
资源消耗 :缓冲区需要占用一定的内存资源
4. 生产者-消费者模式的适用场景 :
消息队列 :如Kafka、RabbitMQ等
线程池 :线程池中的任务队列
GUI应用 :UI线程和工作线程之间的通信
网络编程 :服务器处理客户端请求
批处理系统 :数据处理 pipeline
5. 注意事项 :
缓冲区大小 :缓冲区的大小应该根据实际情况设置,避免过大或过小
线程安全 :确保缓冲区的操作是线程安全的
异常处理 :处理可能的异常,避免程序崩溃
资源管理 :及时释放资源,避免资源泄漏
性能优化 :根据实际情况选择合适的实现方式
6. 生产者-消费者模式的最佳实践 :
使用BlockingQueue :在Java中,推荐使用BlockingQueue来实现生产者-消费者模式,它内置了线程安全的操作,使用方便
合理设置缓冲区大小 :根据生产和消费的速度差异,设置合适的缓冲区大小
使用线程池 :使用线程池来管理生产者和消费者线程,提高线程的利用率
监控和调优 :监控系统的运行状态,根据实际情况进行调优