Java多线程
什么是多线程和线程安全?
多线程
多线程(Multithreading)是指在一个程序中同时运行多个线程的技术。线程是操作系统能够独立管理的最小执行单位,一个程序可以包含一个或多个线程。多线程的好处是可以充分利用多核处理器的性能,提高程序的执行效率,尤其是在处理 I/O 密集型任务时,多线程可以减少等待时间。
多线程的特点:
- 并发执行:多个线程可以同时执行,提升性能。
- 共享内存:多个线程可以共享同一进程的内存空间,包括全局变量、静态变量等。
- 上下文切换:线程之间会进行上下文切换,操作系统负责分配 CPU 资源。
线程安全
线程安全(Thread Safety)是指多个线程同时访问共享资源时,不会因为竞态条件(Race Condition)导致程序出现不正确的行为。在多线程环境中,线程之间可能会同时访问或修改共享数据,如果不进行适当的同步控制,可能导致数据不一致或程序异常。
线程安全的实现方式:
-
同步机制:通过同步关键字(如
synchronized
)来保证同一时间只能有一个线程访问共享资源。例如:synchronized (this) { // 临界区代码,只有一个线程可以执行 }
-
锁(Lock)机制:使用
java.util.concurrent
包中的ReentrantLock
等类,提供更灵活的锁定机制。Lock lock = new ReentrantLock(); lock.lock(); try { // 临界区代码 } finally { lock.unlock(); }
-
原子操作:使用
AtomicInteger
、AtomicReference
等类来保证对变量的操作是原子的。 -
并发容器:使用线程安全的容器,如
ConcurrentHashMap
、CopyOnWriteArrayList
等,这些类已经为并发访问做了适当的控制。
举例说明:
如果多个线程同时对一个共享变量进行写操作,而没有采取同步机制,可能会出现数据覆盖或读取错误。例如:
public class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
在多线程环境下,如果没有 synchronized
来保护 increment()
方法,多个线程可能同时读取和修改 count
,导致最终结果不准确。
Java实现线程安全的几种方式
在Java中,实现线程安全的关键是确保多个线程在并发访问共享资源时不会引发数据不一致或冲突问题。实现线程安全的方式主要有以下几种:
1. 使用同步块(synchronized
)
-
方法同步: 可以在方法声明上添加
synchronized
关键字,表示该方法只能由一个线程访问。public synchronized void method() { // 同步代码 }
-
同步块: 可以在方法内部对某个共享资源加锁,使用
synchronized
块来限定同步范围,避免整个方法都被锁住,提升效率。public void method() { synchronized (this) { // 同步代码 } }
2. 使用显式锁(Lock
接口)
-
java.util.concurrent.locks.Lock
接口提供了更灵活的锁机制,相对于synchronized
块,Lock
允许在不同位置加锁和解锁。Lock lock = new ReentrantLock(); public void method() { lock.lock(); try { // 同步代码 } finally { lock.unlock(); // 确保锁一定会释放 } }
3. 使用并发容器
Java的java.util.concurrent
包提供了多个线程安全的容器类,内部使用了更高效的同步机制,如:
ConcurrentHashMap
CopyOnWriteArrayList
ConcurrentLinkedQueue
这些类已经是线程安全的,适用于高并发场景,避免自己手动加锁。
4. 原子类(Atomic
包)
java.util.concurrent.atomic
包提供了一系列原子操作类,如AtomicInteger
、AtomicLong
、AtomicReference
等,这些类通过CAS(Compare-And-Swap)操作实现了非阻塞的线程安全操作。
AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
5. 线程本地变量(ThreadLocal
)
ThreadLocal
为每个线程提供独立的变量副本,使得每个线程访问到的变量是各自独立的,不会相互干扰。
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void method() {
threadLocal.set(threadLocal.get() + 1);
}
6. 使用volatile
关键字
volatile
用于修饰共享变量,确保线程对该变量的修改会立刻被其他线程可见。但它仅能保证可见性,不能保证原子性,适合用于状态标志等简单场景。
private volatile boolean flag = true;
public void stop() {
flag = false;
}
7. Thread.join()
和 wait()
/notify()
join()
:可以确保一个线程执行完毕后,其他线程再继续执行。wait()
和notify()
:配合synchronized
使用,可以实现线程之间的协调通信。
综合以上方法,具体使用哪种方式取决于应用场景和对性能的要求。常用的方式包括 synchronized
、Lock
接口和并发容器等。
Java常见的并发工具类
Java中提供了丰富的并发工具类,主要集中在java.util.concurrent
包中,涵盖了线程池、并发数据结构、同步控制工具等,用来简化并发编程并提升性能。以下是常见的并发工具类:
1. 线程池工具类
-
ExecutorService
: 一个用于管理线程的接口,可以提交任务并让线程池执行。 -
Executors
: 提供创建常见线程池的方法,如newFixedThreadPool()
、newCachedThreadPool()
、newSingleThreadExecutor()
等。ExecutorService executor = Executors.newFixedThreadPool(5); executor.submit(() -> { // 任务代码 });
2. 并发集合类
ConcurrentHashMap
: 线程安全的哈希表,支持并发读写操作,内部通过分段锁机制来提高并发性能。CopyOnWriteArrayList
: 适用于读操作远多于写操作的场景,写操作时会复制整个数组,读操作无锁。ConcurrentLinkedQueue
: 无界的、基于链接节点的线程安全队列,适合高并发场景。
3. 同步控制工具类
-
CountDownLatch
: 一个同步辅助工具,允许一个或多个线程等待,直到其他线程执行完操作(通过倒计数为0)。CountDownLatch latch = new CountDownLatch(3); latch.await(); // 等待,直到计数器为0 latch.countDown(); // 计数器减1
-
CyclicBarrier
: 类似于CountDownLatch
,但它可以重复使用。多个线程相互等待,直到都到达屏障点,才继续执行。CyclicBarrier barrier = new CyclicBarrier(5); barrier.await(); // 所有线程到达屏障时同时继续
-
Semaphore
: 信号量,用于控制同时访问某一资源的线程数量。可用于实现限流等场景。Semaphore semaphore = new Semaphore(2); // 允许2个线程同时访问 semaphore.acquire(); // 获取许可 semaphore.release(); // 释放许可
-
Exchanger
: 用于两个线程之间交换数据,两个线程必须都调用exchange()
方法后才能完成交换。Exchanger<String> exchanger = new Exchanger<>(); String data = exchanger.exchange("Thread1 Data");
4. 原子类
java.util.concurrent.atomic
包提供了线程安全的原子操作类,适用于需要高效进行简单计数或更新的场景:
AtomicInteger
、AtomicLong
: 原子更新基本数据类型的值。AtomicReference
: 原子更新引用类型。AtomicStampedReference
: 可以解决ABA问题的原子引用类,通过版本号来确保操作的原子性。
5. 并发锁
-
ReentrantLock
: 可重入锁,相对于synchronized
提供了更灵活的锁机制,例如可以设置超时时间、获取锁状态等。ReentrantLock lock = new ReentrantLock(); lock.lock(); try { // 同步代码 } finally { lock.unlock(); }
-
ReadWriteLock
: 读写锁,允许多个读线程并发访问,但写线程是独占的,适用于读操作远多于写操作的场景。ReadWriteLock rwLock = new ReentrantReadWriteLock(); rwLock.readLock().lock(); // 获取读锁 rwLock.writeLock().lock(); // 获取写锁
6. Fork/Join框架
-
ForkJoinPool
: 支持分治(Divide and Conquer)任务模型的线程池,特别适用于处理递归任务。 -
ForkJoinTask
: 包含两种类型的任务:RecursiveTask
(有返回值)和RecursiveAction
(无返回值),用于并行处理任务。ForkJoinPool pool = new ForkJoinPool(); pool.invoke(new RecursiveTask<>() { @Override protected Integer compute() { // 递归任务 } });
7. ScheduledExecutorService
-
ScheduledExecutorService
: 线程池接口,可以调度任务在给定的延迟后执行,或定期执行。ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(() -> { // 定期执行任务 }, 1, 5, TimeUnit.SECONDS);
8. CompletionService
-
ExecutorCompletionService
: 用于管理异步任务的执行和结果收集,它包装了线程池,允许提交任务并在完成时检索结果。ExecutorCompletionService<Integer> service = new ExecutorCompletionService<>(executor); Future<Integer> result = service.submit(() -> { // 任务 return 1; });
9. BlockingQueue
ArrayBlockingQueue
、LinkedBlockingQueue
: 阻塞队列,支持线程安全的生产者-消费者模型。PriorityBlockingQueue
: 基于优先级的阻塞队列,元素按优先级排序。
10. Phaser
-
Phaser
: 类似于CyclicBarrier
,但更加灵活,支持动态调整参与线程的数量,并且可以分阶段执行任务。Phaser phaser = new Phaser(1); // 注册一个线程 phaser.arriveAndAwaitAdvance(); // 到达并等待其他线程
这些工具类为Java开发者提供了丰富的并发编程手段,能够简化复杂的同步控制,并提高多线程程序的性能和稳定性。
Java创建线程池的几种方式
在 Java 中,线程池(Thread Pool)是用来管理和复用线程的机制。通过线程池,程序可以避免频繁地创建和销毁线程,提升性能,并有效控制线程的数量。Java 提供了多种方式来创建线程池,主要通过 java.util.concurrent.Executors
类和 ThreadPoolExecutor
类。
1. 使用 Executors
类创建线程池
Executors
提供了多种工厂方法用于创建不同类型的线程池,常用的有以下几种:
1.1 newFixedThreadPool(int nThreads)
创建一个固定大小的线程池。线程池中的线程数量固定,如果所有线程都在执行任务,新的任务将被放入队列中等待执行。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
- 特点:线程数量固定,不会动态增加或减少。
1.2 newCachedThreadPool()
创建一个可缓存的线程池。如果线程池中有空闲线程会被重用,如果没有空闲线程则创建新的线程。当线程长时间空闲时,会自动回收。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
- 特点:适合执行很多短期的异步任务,线程数量可以动态调整。
1.3 newSingleThreadExecutor()
创建一个只有一个线程的线程池。所有任务会按照提交的顺序依次执行。
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
- 特点:保证任务按顺序执行,适合需要串行执行任务的场景。
1.4 newScheduledThreadPool(int corePoolSize)
创建一个支持定时任务和周期性任务的线程池。适合需要按时间计划执行任务的场景。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
- 特点:可以延迟执行或周期性执行任务。
2. 使用 ThreadPoolExecutor
类自定义线程池
相比 Executors
提供的简单工厂方法,ThreadPoolExecutor
类可以精细化控制线程池的行为,通常用于更复杂的线程池配置。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
corePoolSize, // 核心线程数
maximumPoolSize, // 最大线程数
keepAliveTime, // 线程空闲时间
TimeUnit.SECONDS, // 空闲时间的时间单位
new ArrayBlockingQueue<>(100), // 任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
ThreadPoolExecutor
构造参数:
- corePoolSize:核心线程数,线程池中保持的最小线程数,即使线程处于空闲状态也不会被回收。
- maximumPoolSize:最大线程数,线程池中允许的最大线程数。
- keepAliveTime:非核心线程空闲的最大时间,超过该时间线程将被回收。
- unit:
keepAliveTime
的时间单位,如TimeUnit.SECONDS
。 - workQueue:任务队列,用于存储等待执行的任务。
- handler:拒绝策略,当任务过多且线程池已经饱和时,如何处理新任务。常用的策略有:
- AbortPolicy:抛出异常(默认)。
- CallerRunsPolicy:由提交任务的线程来执行任务。
- DiscardPolicy:直接丢弃任务。
- DiscardOldestPolicy:丢弃最旧的任务,然后尝试执行新的任务。
3. ForkJoinPool (Java 7 引入)
ForkJoinPool
是一个特殊的线程池,主要用于执行分而治之的任务(Divide and Conquer)。ForkJoinPool
支持任务的并行拆分,适合处理递归任务。
ForkJoinPool forkJoinPool = new ForkJoinPool();
- 特点:适合 CPU 密集型任务,将任务拆分为多个子任务并行执行。
4. 自定义线程工厂
有时需要对线程池中的线程进行自定义配置,可以使用 ThreadFactory
自定义线程的创建过程。
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("CustomThread-" + thread.getId());
return thread;
}
};
ExecutorService customThreadPool = new ThreadPoolExecutor(
5, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), threadFactory
);
5. 选择线程池类型的建议
- 如果任务数量较少且需要按顺序执行,使用
newSingleThreadExecutor
。 - 如果任务数量不确定且任务执行时间较短,使用
newCachedThreadPool
。 - 如果需要定时或周期性任务,使用
newScheduledThreadPool
。 - 如果任务量大且需要精细控制线程池行为,使用
ThreadPoolExecutor
。