赞
踩
进程和线程都是多任务处理的核心概念,它们之间有几个主要的区别:
基本概念:
资源分配:
通信成本:
独立性:
开销:
并发:
执行环境:
在现代操作系统中,多线程程序被广泛使用,它们可以提高程序的响应性能并有效利用多核处理器的计算能力。此外,不同的应用场景可能会要求不同的并发策略—一些情况下,进程间通信符合要求,而在需要频繁共享数据和通信的情况下,多线程可能是更合适的选择。
在 Java 中,线程的生命周期包含了线程从创建到结束的一系列状态,以及在这些状态之间的转换。以下是 Java 线程状态以及可能的状态转换:
1. 新建(New)
Thread
对象但还没有调用它的 start()
方法。start()
方法后,线程进入 “可运行(Runnable)” 状态。2. 可运行(Runnable)
3. 阻塞(Blocked)
4. 等待(Waiting)
Object.wait()
、Thread.join()
和 LockSupport.park()
调用。5. 超时等待(Timed Waiting)
Thread.sleep(long millis)
,Object.wait(long timeout)
,它会进入超时等待状态。6. 终止(Terminated)
线程状态转换图
新建 (New) | start()| V 可运行 (Runnable) ----------------. | ^ | | | | 获得CPU | | 释放或失去CPU | | | V | | 运行 (Running) | | | | 同步锁 | | | V | 阻塞 (Blocked) <------. | | | | | 其他线程释放锁 | | V | | 可运行 (Runnable) ---| Object.wait()| | \______|_____/ Thread.sleep(long), | Object.notify() | Object.wait(long),等 | 或 notifyAll() | | | V | | 等待 (Waiting) ------| | | V 超时或interrupt() 超时等待(Timed Waiting) -------' | | 超时或interrupt() V 可运行 (Runnable) ---. | | run()结束或异常退出 | V | 终止 (Terminated) <---'
线程的状态转换受到线程调度器、锁等待和其他线程的行为的影响。了解这些状态和它们之间的关系对于编写并发程序至关重要,因为它有助于诊断线程问题并提高应用程序的性能。
线程安全(Thread-Safe)和线程不安全(Thread-Unsafe)是并发编程中两个关键概念,它们描述了代码在多线程环境下执行时的行为特性。
线程安全(Thread-Safe)
当一个方法或类在多线程环境中,被多个线程同时访问和使用时,如果它可以正确执行、产生正确结果,并且不会导致任何不良副作用,则该方法或类被认为是线程安全的。线程安全的代码可以保护共享资源的状态,避免如数据竞争(race conditions)、死锁(deadlocks)和资源争用(contention)等问题。
实现线程安全的常用技术包括:
AtomicInteger
类)。java.util.concurrent
包中的集合类)。线程不安全(Thread-Unsafe)
相反,如果一个方法或类无法保证在多线程环境中的安全使用,可能导致错误的计算结果或应用程序状态,那么它被认为是线程不安全的。线程不安全的代码通常包含对共享资源的非原子操作,并且在未经同步的情况下,同时被多个线程访问。
线程不安全的典型例子包括:
举例
在 Java 中,StringBuilder
是线程不安全的,因为它的方法不是同步的,如果多个线程同时修改同一个 StringBuilder
对象,可能会导致不一致的状态。而其线程安全的对应物,StringBuffer
,则是同步的,可以在多线程场景中安全使用。
重要性
线程安全是并发编程中保持数据一致性和避免潜在错误的重要保障。设计线程安全的代码是提高软件质量、保证软件可靠性的关键环节。当建立并发程序时,需要特别注意访问和修改共享资源的方式,并采取适当的同步机制以避免线程安全问题。
在 Java 中创建新线程可以通过以下几种方式:
1. 继承 Thread
类
创建一个新的类继承Thread
类,然后覆写其run
方法。
public class MyThread extends Thread {
@Override
public void run() {
// 执行线程任务
System.out.println("This is a new thread running.");
}
}
// 使用新线程
MyThread thread = new MyThread();
thread.start(); // 启动新线程
调用start()
方法时,JVM 会为这个线程分配资源,并调用线程的run
方法。
2. 实现 Runnable
接口
创建一个新的类实现Runnable
接口,实现其run
方法。
public class MyRunnable implements Runnable {
@Override
public void run() {
// 执行线程任务
System.out.println("This is a new thread running.");
}
}
// 使用新线程
Thread thread = new Thread(new MyRunnable());
thread.start(); // 启动新线程
3. 使用 Executor
框架
Java 5 之后,可以使用Executor
框架来简化线程的创建和管理。
Executor executor = Executors.newCachedThreadPool();
executor.execute(new Runnable() {
@Override
public void run() {
// 执行线程任务
System.out.println("This is a new thread running.");
}
});
或者简洁地使用 Lambda 表达式:
Executor executor = Executors.newCachedThreadPool();
executor.execute(() -> System.out.println("This is a new thread running."));
4. 实现 Callable
接口
如果你的线程需要返回结果,可以实现Callable
接口,该接口的call
方法允许返回结果。
public class MyCallable implements Callable<String> { @Override public String call() throws Exception { // 执行线程任务 return "Task done!"; } } ExecutorService executorService = Executors.newFixedThreadPool(1); Future<String> future = executorService.submit(new MyCallable()); // 在需要的地方获取执行结果 try { String result = future.get(); // 阻塞直到线程执行完成,返回结果 } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } // shutdown 执行器,防止新的任务被提交 executorService.shutdown();
选择哪种方式通常取决于具体的使用场景。直接使用Thread
类或Runnable
接口比较简单直接,适合简单的应用场景。如果需要线程返回结果或管理一个线程池,通常推荐使用Executor
框架和Callable
接口。
在 Java 中,synchronized
关键字用于控制并发编程中的访问,保证在同一时刻只有一个线程可以访问特定段的代码,从而防止出现竞争条件和数据不一致的问题。
synchronized
可以用在不同的级别:
同步方法(Synchronized Method)
将一个方法声明为 synchronized
,将会锁定调用该方法的对象的锁,或者如果是 static
方法,会锁定该类的 Class 对象。
public synchronized void synchronizedMethod() {
// 在这里只有一个线程可以执行
}
public static synchronized void synchronizedStaticMethod() {
// 在这里只有一个线程可以执行静态同步方法
}
同步块(Synchronized Block)
可以更细粒度地控制同步,适用于只需要部分方法同步的情况。你可以指定一个锁对象,任何线程在进入同步块之前都必须获得那个锁对象的监视器。
public void synchronizedBlock() {
synchronized (this) {
// 只有获得当前实例锁的线程可以执行这段代码
}
}
public void synchronizedBlockWithObject() {
Object lock = new Object();
synchronized (lock) {
// 只有获得 lock 对象的监视器的线程可以执行这段代码
}
}
作用
保证互斥性:
被同步的代码块在同一时刻只能由一个线程进入执行。
保证可见性:
当线程退出同步代码块时,它对共享变量所作的所有写操作对于后续获得同一锁的线程都是可见的。
防止指令重排序:
内存屏障(Memory Barrier)是 synchronized
实现的一部分,用于防止代码执行的重排序,确保在锁释放之前对共享变量的写操作完成。
注意事项
synchronized
可能导致死锁。synchronized
块(应尽量减少同步块的长度)。synchronized
块内调用其他可能会导致锁的线程方法,以减少死锁风险。在设计并发程序时,适当地使用 synchronized
是同步共享数据的常见做法,但也需要权衡性能和复杂度。替代方案,如 java.util.concurrent.locks
包中的 ReentrantLock
,可能提供更高级的并发控制,并允许更灵活的锁定方案。
在 Java 中,volatile
是一个用在变量声明前的修饰符。当一个变量被声明为 volatile,Java 运行时环境会理解到这个变量可能会被多个线程同时访问和修改,因此必须特别注意对它的操作。volatile
关键字有两个主要的作用:
1. 确保内存可见性
在并发编程中,内存可见性是指一个线程对共享变量所做的修改,能够立即对其他线程可见。通常情况下,一个线程的变更可能首先存储在 CPU 缓存中,而不是直接写入主内存,这意味着其他线程可能看不到这个变量的最新值。这就是为什么在没有适当同步措施的情况下,线程间共享变量的值可能会出现不一致。
声明变量为 volatile
可以确保每次读取 volatile
变量时都会从主内存中读取,每次修改 volatile
变量时都会立即写入主内存。这样可以保证变量在多线程间的可见性。
2. 禁止指令重排
Java 编译器和处理器可能会对指令进行重排优化来提高性能。但是,在某些情况下,这种重排序可能破坏多线程间对共享变量的正确同步。volatile
关键字确保对变量的读写操作不会被编译器和处理器重新排序到其他内存操作之后,这有助于维持代码的“发生前”关系(happens-before relationship),从而避免了多线程间的数据竞态问题。
何时使用 volatile?
使用 volatile
适合于以下情况:
何时避免使用 volatile?
当变量的操作需要依赖当前状态,比如自增操作(i++
),或者在多个变量之间需要保持原子性时,简单地声明变量为 volatile
是不够的,因为 volatile
无法锁定变量以执行复合动作。在这种情况下,你应该使用像 synchronized
块或 java.util.concurrent
包中的原子变量和锁机制。
一个 volatile 变量的示例:
public class SharedObject {
private volatile int counter = 0;
public void setCounter(int counter) {
this.counter = counter; // 写入时,立即刷新到主内存
}
public int getCounter() {
return counter; // 读取时,直接从主内存中读取最新值
}
}
在这个例子中,任何线程对 counter
的修改都将立即对其他线程可见,且不会被编译器或处理器重排到其他内存操作之后。
总之,volatile
对提高 Java 中变量在多核处理器系统下的内存可见性非常重要,但它不是并发编程的万能钥匙。在需要复杂同步逻辑的场景下,应该考虑使用 synchronized
、ReentrantLock
或 AtomicVariable
等其他同步措施。
Java 中的 CountDownLatch
是一个同步辅助工具,它允许一个或多个线程等待在其他线程中执行的一组操作完成后再继续执行。CountDownLatch
提供了一个计数器,其初始值是你指定的操作数量。当一个线程完成了一个任务后,计数器值就会减一。使用 countDown()
方法来减少计数器的值,而 await()
方法用来阻塞当前线程直到计数器值达到零。
使用场景
CountDownLatch
常用于确保某些活动直到其他活动全部完成后才继续执行。例如,在开始一个复杂的计算之前,需要等待必要的资源全部就绪(如文件、网络连接等)。每个启动依赖项都使用 countDown()
表示已经准备完毕,而启动计算的操作在 await()
调用上阻塞,直到所有依赖项报告完成。
基本原理
当创建 CountDownLatch
的实例时,需要指定一个计数器的初始值。调用 countDown()
方法会使计数器的值减一。调用 await()
方法的线程会在计数器的值不为零时阻塞,直至其值变为零。一旦计数器值变为零,所有等待的线程都会被释放,并能够接着执行。
示例代码
这里是 CountDownLatch
的一个基本用法示例:
// 一个有3个工人的工作队列 final CountDownLatch latch = new CountDownLatch(3); // 线程1:工人1 new Thread(() -> { // do work... latch.countDown(); // 表示工人1完成工作 }).start(); // 线程2:工人2 new Thread(() -> { // do work... latch.countDown(); // 表示工人2完成工作 }).start(); // 线程3:工人3 new Thread(() -> { // do work... latch.countDown(); // 表示工人3完成工作 }).start(); // 等待所有工人完成工作 latch.await(); // 所有工人都完成了工作 System.out.println("所有的工人都完成了工作");
在本例中,三个工作线程分别执行任务,每个任务完成后都会调用 countDown()
方法。await()
在主线程中被调用,主线程会阻塞直到所有工人的任务都完成,计数器的值变为零。
注意事项
CountDownLatch
的计数器无法被重置,一旦计数器达到零就不能再次使用。如果需要能够重置计数器的功能,可以考虑使用 CyclicBarrier
。CountDownLatch
是线程安全的,可以在多线程环境中使用,而不需要额外的同步措施。CyclicBarrier
和 CountDownLatch
是 Java Concurrent 包中用于线程同步的两个类,它们虽然都能够阻塞一组线程直到某个特定状态的发生,但是它们的用途和工作方式有所不同。
CountDownLatch
CountDownLatch
主要用于一个或多个线程等待一组其他操作完成。countDown()
方法的调用将不起任何作用。int count = ...; // 需要等待的操作数
CountDownLatch latch = new CountDownLatch(count);
// 启动操作
for (int i = 0; i < count; ++i) {
new Thread(() -> {
// 执行操作
...
// 操作完成后,倒计数减一
latch.countDown();
}).start();
}
// 等待所有操作完成
latch.await();
CyclicBarrier
CyclicBarrier
允许一组线程相互等待达到一个公共屏障点(Common Barrier Point)。CyclicBarrier
是可循环使用的,一旦所有等待线程都到达屏障点,屏障打开,所有线程会释放,然后 CyclicBarrier
可以重置以便下一次使用。CyclicBarrier
支持一个可选的 Runnable,在屏障点上所有线程释放后运行,通常用于更新共享状态。int parties = ...; // 参与线程数 CyclicBarrier barrier = new CyclicBarrier(parties, () -> { // 在所有线程都到达屏障后执行的操作 ... }); // 启动线程 for (int i = 0; i < parties; ++i) { new Thread(() -> { // 执行操作 ... // 等待其他线程 barrier.await(); // 在屏障打开后执行后续动作 ... }).start(); }
简单对比
CountDownLatch
是一次性的,CyclicBarrier
可以重复使用。CyclicBarrier
有一个屏障动作,这是所有线程在屏障点释放后要执行的任务,而 CountDownLatch
没有。CountDownLatch
适合一个线程等待其他线程的场景,而 CyclicBarrier
适合多个线程互相等待至某个公共点再同时继续执行。理解这两个同步工具的使用场景以及它们特有的特点,对于编写高效且正确的并发代码非常重要。
在多线程编程中,信号量(Semaphore)是一种同步机制,用来控制对共享资源的访问。它内部维护了一组许可(permits),这些许可可以被线程获取(acquire)和释放(release),以此来限制对某个特定资源的并发访问数量。
Semaphore 通常被用于:
Semaphore 的基本使用
创建Semaphore
首先,你需要创建一个 Semaphore 的实例,并指定许可的初始数量。可选地,还可以指定是否公平(即等待时间最长的线程优先获得许可)。
import java.util.concurrent.Semaphore;
// 创建一个Semaphore实例,初始许可数量为5
Semaphore semaphore = new Semaphore(5);
获取许可
在访问共享资源之前,线程必须从 Semaphore 中获取许可。如果 Semaphore 没有可用的许可,线程将阻塞直到有许可被释放。
// 获取一个许可,如果没有可用许可,将阻塞
semaphore.acquire();
释放许可
当线程访问完共享资源后,必须释放许可,以供其他线程使用。
// 访问共享资源...
// 完成后,释放许可
semaphore.release();
使用注意事项
semaphore.acquire()
方法可能会抛出 InterruptedException
,表示获取许可时线程被中断。通常需要对该异常进行处理,避免资源泄漏。release()
始终在 finally
块中被调用,以防止许可泄漏。示例
以下是一个简单的例子,展示了如何在一个线程中使用 Semaphore 控制对共享资源的访问:
public class SemaphoredResource {
private final Semaphore semaphore = new Semaphore(3); // 允许3个线程访问资源
public void useResource() {
try {
semaphore.acquire(); // 获取一个许可
// 资源访问逻辑
System.out.println("Resource being used by " + Thread.currentThread().getName());
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 避免忽略中断
} finally {
semaphore.release(); // 总是释放许可
}
}
}
使用 Semaphore 时,务必小心操作许可的获取和释放,不正确的管理可能会导致死锁、资源耗尽或性能问题。
Java线程池的工作原理主要基于重用一组现有的线程来执行任务,避免了频繁创建和销毁线程所带来的开销。线程池由java.util.concurrent
包中的Executor
框架提供支持,其中ThreadPoolExecutor
是这一框架的核心实现类。
以下是线程池的基本工作原理:
线程池的主要组成
核心线程(Core Pool):
这是线程池创建并启动的线程的最小数量。当任务提交给线程池时,如果线程池中的活动线程数量少于核心线程数,则即使有空闲线程,线程池也可能会创建一个新线程来执行该任务。
最大线程数(Maximum Pool Size):
这是线程池允许创建的最大线程数量。只有在工作队列满了的情况下,线程池才会创建超出核心线程数量的线程。
工作队列(Work Queue):
这是一个队列,用于存放等待被线程池中的线程执行的任务。可以使用诸如LinkedBlockingQueue
、SynchronousQueue
或ArrayBlockingQueue
等多种阻塞队列实现。
线程工厂(Thread Factory):
线程工厂用于创建新线程,可以自定义线程的名称、优先级等属性。
拒绝策略(Rejected Execution Handler):
当线程池已达到其最大容量,且工作队列也已满时,提交的新任务会被拒绝。拒绝策略定义了怎样处理这些无法执行的任务。
线程池的工作流程
新任务到来:
当一个新任务被提交到线程池时,线程池会进行以下判断:
任务执行:
线程从工作队列中提取任务并执行。一旦核心线程完成任务,它们将持续在工作队列中查找新任务来执行。
线程存活时间:
如果线程池中的线程数量超过核心线程数,那么超出的线程在空闲一定时间(keepAliveTime
)后会被销毁,直到线程数减少到核心线程数。
线程池的优势
线程池的合理配置对于系统性能的影响极大,需要根据具体的系统负载、硬件能力以及要执行的任务特性来进行调整。
在 Java 线程池(ThreadPoolExecutor)中,execute()
和 submit()
是两个用来提交任务以供线程池执行的方法。它们的主要区别如下:
execute() 方法:
Executor
接口的一部分。Runnable
对象作为参数。RejectedExecutionException
。ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new Runnable() {
@Override
public void run() {
// 执行一些工作...
}
});
submit() 方法:
ExecutorService
接口的一部分,这个接口是 Executor
的子接口。Future
对象。Callable
或 Runnable
对象。当提交 Runnable
时,结果 Future
没有返回值,调用 get()
会返回 null
;当提交 Callable
时,Future
将返回计算结果。Future
对象上调用 get()
方法时被抛出。ExecutorService executorService = Executors.newFixedThreadPool(10);
Future<Integer> future = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// 执行一些计算,并返回结果
return 42;
}
});
try {
Integer result = future.get(); // 获取结果,如果必要的话这个方法会阻塞
} catch (InterruptedException | ExecutionException e) {
// 处理异常
}
关键区别:
execute()
不返回任何内容,但 submit()
返回一个 Future
对象,用于检查任务是否执行完成,或者等待任务执行结束并获取返回值。execute()
只能接受 Runnable
对象;submit()
可以接受 Runnable
或 Callable
,后者允许任务完成时返回结果。execute()
提交的任务中抛出的异常无法直接捕获,通常会在 Thread
的 UncaughtExceptionHandler
中处理;而通过 submit()
提交的任务中抛出的异常可以被 Future
对象捕获,并通过 get()
方法传递给调用者。选择 execute()
还是 submit()
取决于你是否需要任务的执行结果,以及是否要处理任务中抛出的异常。如果只是简单地运行一些独立的无返回值的任务,execute()
是合适的选择;如果你需要获得任务执行的结果或者将来可能会取消任务,那么 submit()
是更好的选择。
ThreadPoolExecutor
是 Java 并发框架中提供的一个执行器(Executor)类,用来执行被提交的Runnable
或Callable
任务。ThreadPoolExecutor
是ExecutorService
接口的一个实现,它使用一个线程池来管理工作线程,这有助于减少由于线程创建和销毁而带来的性能开销。
ThreadPoolExecutor
提供了灵活的线程池管理,允许设置核心线程池大小、最大线程池大小、线程空闲时间、任务队列等参数。以下是一些关键概念和组件:
核心概念
核心线程数(Core Pool Size):
线程池的基本大小,即使线程处于空闲状态,线程池也会保留在池中的线程数量。
最大线程数(Maximum Pool Size):
线程池允许创建的最大线程数量。
存活时间(Keep-Alive Time):
当线程池中的线程数量超过核心线程数时,这是超出的线程在终止前可以处于空闲状态的最长时间。
工作队列(Work Queue):
用于在执行任务前保持待处理任务的队列。常见的实现包括LinkedBlockingQueue
、SynchronousQueue
、ArrayBlockingQueue
等。
线程工厂(ThreadFactory):
创建新线程的工厂。你可以自定义线程工厂来改变线程的属性,比如守护状态、线程优先级等。
拒绝策略(Rejected Execution Handler):
当线程池和队列都满时,拒绝新任务的策略。内置的拒绝策略包括AbortPolicy
、CallerRunsPolicy
、DiscardPolicy
和DiscardOldestPolicy
。
创建 ThreadPoolExecutor
你可以使用ThreadPoolExecutor
的构造器直接创建一个实例:
int corePoolSize = 10; int maximumPoolSize = 20; long keepAliveTime = 30; TimeUnit timeUnit = TimeUnit.SECONDS; BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100); ThreadFactory threadFactory = Executors.defaultThreadFactory(); RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, workQueue, threadFactory, handler );
使用 ThreadPoolExecutor
你可以将实现了Runnable
或Callable
接口的任务提交给ThreadPoolExecutor
以进行异步执行。
executor.execute(new RunnableTask());
Future<String> future = executor.submit(new CallableTask());
execute
方法用于提交不需要返回值的任务,submit
方法用于提交需要返回值的任务。
关闭 ThreadPoolExecutor
当你不再需要线程池时,应当适当地关闭它,使用shutdown
或shutdownNow
方法。
executor.shutdown(); // 等待已提交的任务完成再关闭
executor.shutdownNow(); // 尝试立即停止所有活动任务,并返回尚未执行的任务列表
ThreadPoolExecutor的适用场景
ThreadPoolExecutor
特别适用于需要大量异步任务处理,并且希望能够管理并发级别以及系统资源使用的场景。线程池帮你避免了线程的创建和销毁成本,提高了系统资源的使用效率,对于提高应用程序的响应性和吞吐量非常有帮助。
在 Java 中,ScheduledExecutorService
是 ExecutorService
的一个子接口,它能够在给定的时间延迟或定期执行任务。这是一个理想的解决方案,用于那些需要多次或定期执行的任务,例如,定期地清理缓存、周期性地检查系统健康状况、定时报告生成等。
ScheduledExecutorService
提供几种方法来安排任务的执行:
schedule(Runnable command, long delay, TimeUnit unit)
:
安排指定延迟后执行的一次性任务。
schedule(Callable<V> callable, long delay, TimeUnit unit)
:
安排指定延迟后执行的一次性任务,并返回一个表示任务待处理结果的 ScheduledFuture
。
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
:
安排首次在给定初始延迟后开始执行,然后在给定周期内定期执行的任务。任务在每次执行完毕后间隔固定的时间长度再次执行。
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
:
安排首次在给定初始延迟后开始执行,然后在每次执行结束和下次执行开始之间都存在给定的延迟。
下面是一个简单的使用 ScheduledExecutorService
的例子:
import java.util.concurrent.*; public class ScheduledTaskExample { public static void main(String[] args) { ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); Runnable task = () -> System.out.println("Executing Task at " + System.nanoTime()); Callable<String> callableTask = () -> "Called at " + System.nanoTime(); // 安排一次性任务在 5 秒后执行 Future<String> result = scheduler.schedule(callableTask, 5, TimeUnit.SECONDS); // 安排任务每 10 秒执行一次 ScheduledFuture<?> fixedRateFuture = scheduler.scheduleAtFixedRate(task, 0, 10, TimeUnit.SECONDS); // 安排任务在每次执行结束后,5秒再执行一次 ScheduledFuture<?> fixedDelayFuture = scheduler.scheduleWithFixedDelay(task, 0, 5, TimeUnit.SECONDS); } }
在这个例子中,我们创建了 ScheduledExecutorService
实例,然后安排了三个不同类型的任务。一个是在指定的延迟后执行的一次性任务,其他两个分别按照固定频率和固定延迟周期性地执行。
ScheduledExecutorService
提供了强大的机制,用于精确控制任务的执行,适用于复杂的调度需求。在使用时需要注意任务的执行时长和调度间隔,确保没有产生意外的"任务堆积",并在最后适当地关闭调度器,释放资源。
Java 中的 Timer
类和 ScheduledExecutorService
接口都可以安排将来某一时刻或定期执行的任务。不过,它们在设计和能力上存在一些关键区别:
Timer
Timer
是 Java 早期提供的定时器工具,它可以用于安排定时任务。Timer
实例对应单个后台线程。Timer
线程会终止,并且已计划执行的任务也不会再继续执行。Timer
提供了安排一次性任务和重复执行任务的功能。示例:
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("任务执行了!");
}
}, 1000, 2000); // 延时 1 秒后开始执行,每隔 2 秒执行一次
ScheduledExecutorService
ScheduledExecutorService
是 Java 5 中引入的一部分 Executor 框架,提供了更灵活和广泛的定时任务调度功能。Executors
工具类方便地创建一个拥有多个线程的 ScheduledThreadPool
。scheduleAtFixedRate
(按固定频率)和 scheduleWithFixedDelay
(固定延迟)。示例:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
executor.scheduleAtFixedRate(() -> {
System.out.println("任务执行了!");
}, 1, 2, TimeUnit.SECONDS); // 初始化延时 1 秒,任务间隔 2 秒
两者的选择
Timer
。ScheduledExecutorService
是更好的选择。ScheduledExecutorService
,因为它提供了比 Timer
更多的功能和更好的错误处理能力。由于 ScheduledExecutorService
提供了更完善的错误处理机制以及更高的灵活性和可配置性,它通常是安排定时任务的首选方案。
在软件开发中,定时任务和周期性任务通常用于在特定时间或以固定频率执行预定的作业。在 Java 程序中,主要有如下几种方式来实现定时任务和周期性任务:
1. 使用 java.util.Timer
和 java.util.TimerTask
Timer
和 TimerTask
是 Java 标准库提供的一种简易方法来安排任务的执行。
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
// 执行任务
}
}, delay, period);
delay
是首次运行任务前的延迟时间(以毫秒为单位),period
是连续任务执行之间的时间间隔(也是以毫秒为单位)。
2. 使用 java.util.concurrent
包中的 ScheduledExecutorService
ScheduledExecutorService
是一个更强大且灵活的方法来安排任务的执行,它可以处理并发任务,允许多个线程的任务同时进行。
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
executorService.scheduleAtFixedRate(() -> {
// 执行周期性任务
}, initialDelay, period, TimeUnit.SECONDS);
initialDelay
是首次执行任务前的延迟时间,而 period
是连续任务执行之间的周期,可指定单位,如秒、分钟等。
3. 使用 Spring Framework 的 @Scheduled
注解
在使用 Spring 的项目中,@Scheduled
注解提供了一种声明式的方式来定义定时任务。
@Component
public class ScheduledTasks {
@Scheduled(fixedRate = 5000)
public void reportCurrentTime() {
// 每隔5000毫秒执行一次
}
@Scheduled(cron = "0 * * * * ?")
public void executeCron() {
// 根据Cron表达式执行
}
}
Spring 允许你使用 fixedRate
、 fixedDelay
或 Cron 表达式来指定任务执行的时间规则。
4. 使用 Spring Boot 和 Spring TaskScheduler
Spring Boot 进一步简化了周期性任务的配置,通过 TaskScheduler
接口和相关配置属性来管理任务的调度。
@Configuration
@EnableScheduling
public class SchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {
return new ConcurrentTaskScheduler();
}
}
在方法上使用 @Scheduled
注解来安排定时任务。
5. 使用 Quartz
Quartz 是一个功能丰富的开源作业调度库,它能够实现复杂的调度需求,例如数据依赖作业、作业持久化、事务管理等。
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); JobDetail job = JobBuilder.newJob(MyJob.class) .withIdentity("job1", "group1") .build(); Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "group1") .startNow() .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(40) .repeatForever()) .build(); scheduler.scheduleJob(job, trigger); scheduler.start();
定时任务的实现方式很多,选择适合的方式取决于具体需求和技术栈。对于简单的定时或周期性任务,Timer 或 ScheduledExecutorService 通常已经足够。如果需要更多的功能或控制,Quartz 或 Spring Task Scheduling 可能是更好的选择。对于依赖 Spring 生态系统的应用,Spring 的 @Scheduled
提供了便捷且强大的定时任务实现方式。
生产者消费者问题是一个经典的并发问题,它涉及两类进程或线程:生产者(负责生成数据)和消费者(负责处理数据)。生产者将生成的数据放入一个公共的缓冲区,而消费者则从缓冲区中取走数据进行处理。问题的核心在于要确保生产者不会向已满的缓冲区添加数据,消费者不会尝试从空的缓冲区中取出数据。
Java中实现生产者消费者问题的方法:
1. 使用 Object
类的 wait()
和 notify()
方法:
class Buffer { private Queue<Integer> queue = new LinkedList<>(); private int capacity; public Buffer(int capacity) { this.capacity = capacity; } public synchronized void put(int value) throws InterruptedException { while (queue.size() == capacity) { wait(); // 缓冲区满,等待消费者消费 } queue.add(value); notifyAll(); // 通知消费者可以消费了 } public synchronized int get() throws InterruptedException { while (queue.isEmpty()) { wait(); // 缓冲区空,等待生产者生产 } int value = queue.poll(); notifyAll(); // 通知生产者可以生产了 return value; } }
2. 使用 BlockingQueue
:
class Producer implements Runnable { private BlockingQueue<Integer> queue; Producer(BlockingQueue<Integer> queue) { this.queue = queue; } @Override public void run() { try { // 生产者的生产过程 queue.put(produce()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private int produce() { // 生产数据 } } class Consumer implements Runnable { private BlockingQueue<Integer> queue; Consumer(BlockingQueue<Integer> queue) { this.queue = queue; } @Override public void run() { try { int data = queue.take(); consume(data); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private void consume(int data) { // 处理数据 } } public static void main(String[] args) { BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(BUFFER_SIZE); Thread producerThread = new Thread(new Producer(queue)); Thread consumerThread = new Thread(new Consumer(queue)); producerThread.start(); consumerThread.start(); }
BlockingQueue
是一个线程安全的队列实现,它内部使用锁来保证生产者和消费者的操作是互斥的,不需要使用额外的同步。生产者调用put()
操作时,如果队列已满,它会阻塞直到有空间变 available。消费者调用take()
操作时,如果队列为空,它会阻塞直到有元素可取。
3. 使用 ReentrantLock
和 Condition
:
class Buffer { private Queue<Integer> queue = new LinkedList<>(); private int capacity; private final Lock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); public Buffer(int capacity) { this.capacity = capacity; } public void put(int value) throws InterruptedException { lock.lock(); try { while (queue.size() == capacity) { notFull.await(); // 缓冲区满,等待消费者消费 } queue.add(value); notEmpty.signal(); // 通知消费者可以消费了 } finally { lock.unlock(); } } public int get() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); // 缓冲区空,等待生产者生产 } int value = queue.poll(); notFull.signal(); // 通知生产者可以生产了 return value; } finally { lock.unlock(); } } }
在这个解决方案中,ReentrantLock
用于取代对象内部的监视器锁,并且使用Condition
替代了监视器方法wait()
和notify()
,提供了更灵活的线程同步手段。
选择哪种方式取决于具体的应用场景和个人偏好。使用BlockingQueue
是实现生产者消费者问题的最简单直接的方式,因为它抽象了所有低级别的锁操作。如果需要更细粒度的控制,可以选择wait/notify
或ReentrantLock/Condition
。
在 Java 中,wait()
, notify()
, 和 notifyAll()
是与对象监视器(Monitor)密切相关的低级线程同步方法,它们在 Object
类中定义,用于协调多个线程对共享资源的访问。这些方法必须在同步代码块或同步方法中调用,通常这意味着调用线程已经持有了该对象的锁。
wait() 方法
wait()
方法使当前线程暂停执行并释放对象的锁,然后进入等待状态(Waiting State)直到被通知(notify)或中断。wait()
后,它会释放对象的锁,并且在 notify()
或 notifyAll()
被调用前,或者指定的等待时间结束前,不会从 wait()
方法返回。synchronized (object) {
while (<condition does not hold>) {
object.wait();
}
// Proceed when condition holds
}
notify() 方法
notify()
方法用于唤醒在该对象上等待的单个线程。如果有多个线程在等待,其中一个会被随机挑选出来被唤醒。synchronized (object) {
// Change the condition
object.notify();
}
notifyAll() 方法
notifyAll()
方法用于唤醒在该对象上等待的所有线程。这些线程将进入锁定池(Lock Pool)并争夺对象的锁。notifyAll()
始终比 notify()
更安全,因为它确保了所有等待的线程都有机会响应条件的变更。synchronized (object) {
// Change the condition
object.notifyAll();
}
注意事项
wait()
方法,并在每次唤醒后检查等待条件是否满足。notify()
或 notifyAll()
后不会立即释放锁。当前同步区块结束后,锁才会被释放。notify()
或 notifyAll()
,否则可能会导致虚假唤醒(Spurious Wakeup)。wait()
, notify()
, 和 notifyAll()
提供了一种强大的机制,用于多线程间的通信和同步。然而,考虑到 API 使用的复杂性和潜在风险,现代 Java 开发通常推荐使用更高级的同步设施,如 java.util.concurrent
包中的 Locks
, Conditions
, Semaphores
, 和 BlockingQueues
。
在 Java 中,阻塞队列(Blocking Queue)是一种特别的队列,它支持在队列操作中进行阻塞。这意味着,如果队列为空,它会在尝试读取元素时阻塞线程直到队列不为空;如果队列已满,在尝试添加元素时,它会阻塞线程直到队列中有可用空间。阻塞队列在处理生产者-消费者问题时非常有用,这是多线程编程中常见的一个模式。
Java的java.util.concurrent
包提供了几种阻塞队列的实现,包括以下常用的几种:
ArrayBlockingQueue
:一个由数组支持的有界阻塞队列。LinkedBlockingQueue
:一个基于链表结构的阻塞队列,此队列按 FIFO(先进先出)排序元素。PriorityBlockingQueue
:一个支持优先级排序的无界阻塞队列。SynchronousQueue
:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,反之亦然。DelayQueue
:一个使用优先级队列实现的无界阻塞队列,其中的元素只有在其指定的延迟时间到了才能从队列中取出。在多线程中使用阻塞队列
在多线程应用程序中使用阻塞队列通常遵循以下模式:
生产者(Producer)
put()
调用中被阻塞,直到队列中有可用空间。消费者(Consumer)
take()
调用中被阻塞,直到队列中有元素可用。示例代码
BlockingQueue<String> queue = new LinkedBlockingQueue<>(10); // 生产者线程 Thread producer = new Thread(() -> { try { while (true) { String product = produceProduct(); queue.put(product); } } catch (InterruptedException e) { // 处理异常 } }); // 消费者线程 Thread consumer = new Thread(() -> { try { while (true) { String product = queue.take(); consumeProduct(product); } } catch (InterruptedException e) { // 处理异常 } }); producer.start(); consumer.start();
使用阻塞队列的优势
总的来说,阻塞队列是实现安全高效的生产者-消费者模式的理想选择,并且在许多并发应用场景中得到广泛使用。
Java 并发集合是专门为多线程环境设计的数据结构,它们可以在多个线程同时访问时提供线程安全性,而不会牺牲太多性能。这些集合类型包含在 Java 的 java.util.concurrent
包中,是传统的同步集合(如 Vector
和 Collections.synchronizedList
)的现代替代品,提供了更好的并发性能和更丰富的功能。
关键特性
锁的细化:
许多并发集合通过使用细粒度的锁定或完全锁定策略来降低锁的争用,如分段锁(ConcurrentHashMap
)和无锁算法(ConcurrentLinkedQueue
)。
非阻塞算法:
一些并发集合实现无锁算法,通过使用原子类和 CAS(Compare-And-Swap)操作来保证线程安全,从而避免了同步造成的阻塞。
弱一致性迭代器:
许多并发集合提供的迭代器具有弱一致性(weakly consistent)特性,这意味着迭代器不会抛出 ConcurrentModificationException
,而且它可以容忍在迭代过程中集合结构的变化。
可伸缩性:
并发集合比传统同步集合在可伸缩性上有明显优势,因为它们在多线程访问时性能下降幅度较小。
示例集合类型
ConcurrentHashMap
:
一个线程安全的哈希表,得益于分段锁技术,提供比 Hashtable
和同步的 HashMap
明显更好的并发性能。
CopyOnWriteArrayList
和 CopyOnWriteArraySet
:
在写入时复制(copy-on-write)的线程安全列表和集合,非常适合读多写少的情况。
ConcurrentLinkedQueue
:
基于链接节点的线程安全无界队列,使用无锁算法实现,适用于高吞吐量场景。
BlockingQueue
接口的实现,如 LinkedBlockingQueue
和 ArrayBlockingQueue
:
支持阻塞的插入和删除操作的线程安全队列,非常适合用作生产者-消费者的数据共享通道。
ConcurrentSkipListMap
和 ConcurrentSkipListSet
:
分别提供线程安全的映射表和有序集合,内部通过跳表(SkipList)实现。
并发集合提供了在多线程并发环境中处理共享资源的安全和高效机制。在开发涉及多线程数据共享和操作的应用程序时,合理地使用这些并发集合类型能大大提升程序性能和线程安全性。
ConcurrentMap
和 Hashtable
都是 Java 中用于存储键值对的线程安全的 Map 实现,但是它们之间有一些关键的区别:
ConcurrentMap
ConcurrentMap
是 Java 5 中引入的一个新的 Map 接口,它是 java.util.concurrent
包的一部分。ConcurrentMap
接口提供了一些原子操作的方法,例如 putIfAbsent
、remove
和 replace
,它们能够帮助我们在无需额外同步的情况下实现线程安全的更新操作。ConcurrentHashMap
是 ConcurrentMap
接口的一个具体实现。它使用分段锁(或其它低级同步策略,比如根据 JDK 版本的不同,JDK 8 使用 CAS 和 volatile 变量)来提高并发访问性能。多个线程可以同时读取和写入 ConcurrentHashMap
而不会互相阻塞。Hashtable
Hashtable
是 Java 的早期集合类之一,自 JDK 1.0 起就存在。Hashtable
内部的方法基本上都是同步的,这就意味着在同一时刻只有一个线程可以访问 Hashtable
,这使得 Hashtable
在多线程环境下表现不佳。Hashtable
不允许 null
键或 null
值。区别总结
ConcurrentHashMap
比 Hashtable
有更好的并发性能,因为多个线程可以同时安全地更新 ConcurrentHashMap
的不同部分。而 Hashtable
在执行所有操作时都需要获取对象锁,这会导致较大的线程争用和性能下降。ConcurrentHashMap
基于更现代的并发模式设计,而 Hashtable
的设计反映了早起 Java 版本的线程安全策略。ConcurrentHashMap
支持扩展的原子操作,如 computeIfAbsent
等,而 Hashtable
不提供类似功能。ConcurrentHashMap
迭代器提供了弱一致性(创建迭代器后的修改不一定立即反映在迭代器上),而 Hashtable
迭代器的行为是不确定的。由于以上差异,建议在现代 Java 应用中优先使用 ConcurrentHashMap
而不是 Hashtable
,除非有特定的原因必须要用到 Hashtable
。在大多数情况下,ConcurrentHashMap
会提供更优的并发性能和扩展性。
CopyOnWriteArrayList
和同步的 ArrayList
(通常是通过 Collections.synchronizedList(new ArrayList<...>())
获得的)是 Java 中两种用于并发环境中的列表实现,它们采用不同的线程安全策略。
CopyOnWriteArrayList
CopyOnWriteArrayList
是一个线程安全的 ArrayList
变体,其核心思想在于,每次修改列表内容时(如添加、删除、设置元素等),都会创建列表的一个新副本,修改操作是在新副本上执行的。修改完成后,任何一个读取操作都会获取到修改前的列表状态,直到修改操作完成,新副本才会对外发布,之后的读取操作则会看到新的修改。
特点:
ConcurrentModificationException
。同步的 ArrayList
同步的 ArrayList
通过 Collections.synchronizedList(new ArrayList<...>())
创建,其实本质上是一个普通的 ArrayList
被包装在一个同步的 List
实现上。所有的列表操作都是通过持有一个锁来完成的。
特点:
ConcurrentModificationException
。区别总结
CopyOnWriteArrayList
基于写时复制,让读取无锁且性能较好;同步的 ArrayList
则是操作加锁,保证了操作的原子性。CopyOnWriteArrayList
中每次修改都需要复制整个列表,相比之下,同步的 ArrayList
修改操作只需要获取锁。CopyOnWriteArrayList
的迭代器具备快照特性,迭代过程中不会反映出列表的修改,也不会抛出 ConcurrentModificationException
;同步的 ArrayList
需要外部同步来保护迭代器的操作。总结:选择使用哪种列表实现,应该根据实际应用场景下的读写比率、并发级别和性能需求来决定。如果是读多写少,且要求迭代器是一致性的,则 CopyOnWriteArrayList
可能更适合。如果写操作较多或希望节约内存(不做副本),可以考虑使用同步的 ArrayList
。
ReentrantLock
和 synchronized
关键字都是Java提供的用于管理并发访问的同步机制。尽管它们在很多情况下都可以用来确保线程安全,但它们有一些关键的不同点:
ReentrantLock(java.util.concurrent.locks.ReentrantLock)
显式锁定:ReentrantLock
提供了显式的锁定与解锁操作。你必须使用 lock()
方法来获取锁,并在finally
块中使用 unlock()
来释放锁,以确保锁无论如何都能被释放。
可中断的锁获取:ReentrantLock
提供了一个可中断的获取锁的方法 lockInterruptibly()
,这允许一个线程在等待获取锁的过程中响应中断。
尝试非阻塞获取锁:使用 tryLock()
方法可以尝试获取锁而不被阻塞,如果锁立即可用(未被其他线程持有),则获取锁成功。
带超时的尝试获取锁:tryLock(long timeout, TimeUnit unit)
方法允许在特定的时间内等待锁;如果在给定的时间内未能获取锁,线程不会一直等待。
公平锁:ReentrantLock
可以配置为公平锁,意味着在多个线程竞争时,会按照线程等待的先后顺序来分配锁。
条件变量:ReentrantLock
可以结合使用 Condition
类,允许使用更为灵活的条件等待/通知模式。
可查询:你可以在不尝试获取锁的情况下查询其状态,例如查看是否被锁定、是否被某个线程持有、等待获取锁的线程数等。
synchronized 关键字
隐式锁定:synchronized
提供隐式锁定与解锁的操作。你只需要在一个方法或一个块上使用 synchronized
关键字,当线程进入这个同步块时它会自动获取锁,并在退出时自动释放锁。
不可中断:当一个线程在等待 synchronized
锁时,它无法响应中断。
不存在尝试获取锁的方法:没有像 tryLock()
那样不阻塞线程的方法直接用于 synchronized
。
内部锁:synchronized
依赖内部锁(Monitor)机制,每个对象都有内部锁,当使用一个对象作为锁时也被称为监视器锁。
不需要指定公平性:synchronized
没有公平性选择,不能保证等待的线程将按照它们请求访问的顺序来获取锁。
不支持条件变量:与 Condition
不同,synchronized
不支持多路等待/通知。
不可查询:你不能查询 synchronized
锁是否被持有、哪个线程持有锁、等待获取锁的线程等信息。
使用场景对比
synchronized
一般来说是一个好的选择。ReentrantLock
。总体说来,ReentrantLock
提供了比 synchronized
更强大和灵活的锁操作能力,但也需要开发者手动管理锁的释放,确保不会发生死锁。而 synchronized
是一个更简单、更易于使用的内建同步机制,广泛用于不需要重入锁提供的高级特性的场景。
ReadWriteLock
是 Java 中的一个接口,它定义了一个可以同时允许多个读取操作,但只允许一个写入操作的锁。这种锁通常用于提高程序在处理多线程读写操作时的性能,特别是在读取操作远多于写入操作的情况下。ReadWriteLock
是在 java.util.concurrent.locks
包中定义的。
ReadWriteLock
接口中最主要的实现是 ReentrantReadWriteLock
类。这个类实现了一个读写锁,允许多个线程同时读取共享资源,但如果一个线程正在写入资源,则其他线程无法读取或写入。
如何使用 ReadWriteLock
要使用 ReadWriteLock
,首先必须创建其实例,然后分别获取读锁和写锁。下面是使用 ReadWriteLock
的步骤:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
获取读锁(Shared Lock)
读锁可以被多个线程同时持有,只要没有线程持有写锁。获取读锁通常用于保护读操作:
readWriteLock.readLock().lock();
try {
// 安全地执行读取操作
} finally {
readWriteLock.readLock().unlock();
}
获取写锁(Exclusive Lock)
写锁是独占的。当写锁被获取后,其他线程将无法获取读锁或写锁,直到写锁被释放。获取写锁通常用于保护写操作:
readWriteLock.writeLock().lock();
try {
// 安全地执行写入操作
} finally {
readWriteLock.writeLock().unlock();
}
一些注意事项和最佳实践:
finally
块中释放锁,以确保锁能被释放,防止死锁。ReadWriteLock
,因为它的管理比普通的互斥锁(ReentrantLock
)要复杂。通过这种方式,ReadWriteLock
提供了一种可以大幅提升多线程程序性能的锁机制,特别是在高并发的读操作场景中。但要记住,它会比普通的互斥锁更复杂,因此它的管理需要更多的注意。在不需要处理大量并发读取的情况下,使用 ReentrantLock
或内建的 synchronized
通常会更简单并且足够。
死锁(Deadlock)是并发编程中的一个情况,当两个或多个线程在执行过程中因为争夺资源而互相等待,导致它们都无法继续执行。如果没有外部干预,这些线程将永远处于等待状态。
死锁通常发生在以下四个条件同时成立时:
预防和避免死锁的方法
避免互斥
这通常不切实际,因为有些资源本来就是不能共享的。但是针对可共享资源,使用无锁编程技巧或锁的替代方案(如信号量、读写锁等)可以降低互斥的需求。
打破请求与保持条件
方法包括一次性请求所有资源,避免分阶段获取资源,或在请求新资源之前释放所有已占有的资源。这样降低了因为等待而产生等待链的可能性。
强制剥夺资源
设计一个机制允许线程在得知会引起死锁的情况时主动释放其所占有的资源。实施点可能包括超时机制、检查线程状态、引入优先级等。
资源按序分配
规定所有线程按照一定顺序申请资源,打破了形成循环等待的闭环链,这是预防死锁最常见的方法。例如,所有资源都编号,线程必须按编号升序请求资源。
死锁检测
通过使用算法(如资源分配图、银行家算法)检测系统是否处于死锁状态,一旦检测到,采取措施解决。这通常涉及到资源剥夺和线程重启。
使用锁超时
在尝试获取锁时使用超时机制,在超过指定时间未能获取到资源时放弃,从而避免死锁。
固定资源分配
对每个线程事先固定分配需要的资源,无需在运行时请求。
应用并发编程工具
利用现成的并发库和框架(如 Java中的 java.util.concurrent
包、其他编程语言的并发库等)来规避多线程编程的复杂性和潜在问题。
检测和恢复
尽管预防至关重要,但在软件系统中检测到死锁并恢复也很重要。可以通过以下方式进行:
jconsole
或 jstack
,实时分析并找出死锁情况。防止死锁需要深入理解代码中的同步和并行行为,避免设计和编码中导致死锁的决策。代码审查、对并发编程模式的深入了解、针对并发场景的广泛测试也都是必不可少的。
在 Java 中,原子类是一组特殊的类,位于 java.util.concurrent.atomic
包中,用于执行原子操作。原子类主要用于多线程环境中,提供了一种无锁的线程安全编程方式,可以在不使用 synchronized
关键字的情况下实现同步。
原子类利用了底层硬件平台提供的原子性操作(比如 CAS - Compare-And-Swap)来保证变量操作的原子性。在多线程并发编程中,这可以避免使用昂贵的同步锁定机制,从而降低线程开销并提高性能。
原子类的功能包括:
基本类型的原子性更新:
提供了对单个变量如整数(AtomicInteger
)、布尔值(AtomicBoolean
)、长整型(AtomicLong
)等的原子性更新。
数组类型的原子性更新:
允许对数组中的元素(如 AtomicIntegerArray
、AtomicLongArray
)进行原子性操作。
对象属性的原子性更新:
类如 AtomicReference
、AtomicReferenceFieldUpdater
和 AtomicIntegerFieldUpdater
等使得我们能够对对象的字段进行原子性更新。
累加器和自增、自减操作:
提供了累加(LongAdder
、DoubleAdder
)和自增、自减操作以及获取当前值的函数。
延迟初始化:
AtomicStampedReference
和 AtomicMarkableReference
类型提供了对引用类型的原子更新,并能够同时更新一个"标记"或"时间戳"来控制版本或状态。
使用示例:
下面的示例展示了一个 AtomicInteger
的基本使用:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private AtomicInteger atomicInt = new AtomicInteger(0);
public void increment() {
atomicInt.incrementAndGet(); // 原子自增
}
public int getValue() {
return atomicInt.get(); // 获取当前值
}
}
在上面的例子中,incrementAndGet
方法将 AtomicInteger
原子性地自增,并立即返回新值。这能够保障在多线程环境下不会因为并发更新导致不一致状态。
作用总结:
原子类的主要作用是在多线程环境下,为程序员提供了一种简单、无锁、高效的方式来进行变量的同步更新。通过减少或消除锁的使用,原子类显著提高了并发程序的性能,并减少了死锁发生的可能性。在编写并发代码时,适合使用原子类的场景通常包括计数器、标志位、状态值等简单变量的访问和修改操作。
AtomicInteger
是 Java 中用于执行原子操作的一个类,在 java.util.concurrent.atomic
包中。它利用了底层的 CAS(Compare-And-Swap)指令来保证Atomic(原子)性的更新操作。
原子性的概念
原子性指一个操作要么全执行,要么全不执行,不会出现中途停滞的情况。也就是说,在多线程环境中,当一个线程对 AtomicInteger
进行修改时,不会被其他线程的操作所中断。
CAS 原理
AtomicInteger
的原子性是通过循环 CAS 操作实现的,CAS 操作包括以下三个步骤:
CAS 操作是由处理器直接支持的,它是一个不可分割的指令,用于保证某个特定内存位置的原子性更新。
Java内部实现
在 Java 中,AtomicInteger
核心的原子性保证是通过一个名为 unsafe
的类实现的,这是一个提供了硬件级别原子操作的内部类。unsafe
提供了一个方法 compareAndSwapInt
(即 CAS),它是本地方法(native method),由 JVM 直接调用操作系统及硬件支持的原子操作。
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
上述方法中,unsafe
类的 getAndAddInt
方法使用 CAS 操作来保证 valueOffset
(实际上就是 AtomicInteger
中的值)的原子性增量。
线程安全的好处
使用 CAS 机制的好处是,它不需要使用传统的锁定机制来保证线程安全,从而减少了线程阻塞和上下文切换带来的开销。相比于 synchronized
关键字或者显式的 Lock
,CAS 提供了一种无锁(lock-free)的方式来实现线程安全,进而能够在高并发场景下提供更好的性能。
不过,CAS 也有一个缺点,即在高竞争的环境下如果更新失败会不断重试,这种情况下可能会导致大量的 CPU 资源消耗,这被称为 “自旋”。此外,对于复杂的同步操作,AtomicInteger
可能无法提供足够的支持,那时需要使用更强大的 AtomicReference
、AtomicStampedReference
或 AtomicIntegerFieldUpdater
等类。
总的来说,AtomicInteger
使用 CAS 保证了高效的原子性操作,适用于计数器或累加器等简单的原子状态管理。
在 Java 中,原子类(如 AtomicInteger
、AtomicLong
、AtomicReference
等)是 java.util.concurrent 包提供的一组工具类,它们利用 CAS(Compare-and-Swap)机制提供了非阻塞性的原子操作,用于更新单个变量。使用原子类可以解决多线程环境中的以下并发问题:
数据竞争和原子性
在多线程程序中,多个线程对同一个变量进行读写,如果没有适当的同步措施,就会出现数据竞争问题(Race Condition),导致数据不一致。原子类确保了原子操作,即更新的操作不可分割,无论中间过程如何,要么全部执行,要么完全不执行。
避免阻塞和死锁
传统的同步控制,比如使用 synchronized
关键字,会导致线程阻塞,等待持有锁的线程释放。长时间的等待或错误的锁使用可能导致死锁。相比之下,原子类提供的是非阻塞性的同步,线程在尝试执行原子操作失败时,会立刻重试或在非关键的失败操作后退,而不是进入阻塞状态。
高性能和可扩展性
基于锁的同步可能在高竞争的环境下导致线程的上下文切换和阻塞,影响性能和可扩展性。原子类基于硬件级别的 CAS 支持,通常比基于锁的同步有更好的性能,特别是对于读多写少的场景。
如果 Java 中使用了原子类,可以解决以下具体的并发问题:
原子性保证
内存可见性
volatile
变量确保了对所有线程的可见性,当某个线程修改了共享变量的值后,其他线程可以立即看到最新的值。无锁同步
操作失败知晓
compareAndSet
提供了操作成功与否的反馈,开发者可以根据状态做进一步处理。复杂的原子操作
使用场景举例:
AtomicInteger atomicInteger = new AtomicInteger(0);
// 多个线程可以安全地改变单个 integer 的值
int oldVal = atomicInteger.get(); // 安全地读取值
int newVal = atomicInteger.incrementAndGet(); // 安全地自增并获取新值
boolean updated = atomicInteger.compareAndSet(expectedValue, updateValue); // 尝试根据期望值来更新
总结:
原子类不适合所有并发场景,但它们在处理单变量的并发更新时是一个非常有价值的工具。对于需要同步控制复杂状态或多变量的情况,可能还需要其他的并发控制手段,如使用 synchronized
、Locks
或 STM(软件事务内存)
。在选择适用的并发工具时,需要根据具体的并发模式和应用场景来决定。
单例模式:
单例模式(Singleton Pattern)是一种设计模式,它的核心目标是确保一个类在应用程序的生命周期中只有一个实例,并提供这个实例的全局访问点。单例模式通常用于控制对某些共享资源(如数据库连接池或配置管理器)的访问。
实现单例模式通常涉及以下几个关键步骤:
多线程中的挑战:
在多线程环境中实现单例模式存在挑战,因为有可能出现多个线程同时访问单例类的获取实例的方法,并导致创建多个实例。这就破坏了单例模式的核心原则,因此需要同步机制来保证线程安全。
实现线程安全的单例模式的几种方法:
1. 饿汉式(Eager Initialization):
这是最简单的线程安全单例实现方式。在这种实现中,单例实例在类被加载时就被创建。
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
2. 懒汉式(Lazy Initialization):
在懒汉式实现中,单例实例在第一次被需要时创建。这种实现在多线程环境下必须正确同步,否则可能导致单例合同被破坏。
简单的线程安全的懒汉式实现:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
双重检查加锁(Double-Checked Locking):
为了提高性能,可以使用"双重检查加锁"机制,这种方式只需要在实例尚未创建时进行同步。
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; } }
在这种实现中,volatile
关键字确保了 instance
引用的内存可见性和禁止指令重排。
3. 枚举方式(Enum Singleton):
枚举类型是实现单例的最佳方法之一。枚举实现方式能防止多线程同步问题,同时也能防止反序列化重新创建新的对象。
public enum Singleton {
INSTANCE;
public void someMethod() {
// 功能实现
}
}
总结:
实现线程安全的单例模式要求在多线程访问时正确地处理实例的创建和引用。考虑到性能和资源利用,需要在正确同步和懒惰加载之间进行权衡。Java 枚举类型提供了一种既简单又具有内置线程安全保证的单例实现方式。
对象的不变性(Immutability)是指一旦对象被创建,它的状态(对象属性的值)就不能改变。不可变对象提供了简化并发编程和保证线程安全的方法,因为它们自然地不需要同步即可在多线程环境中被共享。此外,不可变对象还有其他好处,比如减少错误和便于理解程序行为,它们的状态也更容易进行跟踪和调试。
实现一个不可变对象需要遵循以下原则:
使用 final 关键字:声明类为 final,防止被继承;声明所有成员变量为 final,确保它们只能被赋值一次。
无 Setter 方法:不提供修改对象状态的方法,包括 setter 方法或任何修改成员变量的方法。
所有成员变量为私有:将所有成员变量设置为私有,防止外部直接访问对象内部状态。
通过构造器初始化所有成员变量:只通过构造器设置成员变量的值。
在需要时进行深拷贝:如果对象包含可变的成员变量,如数组或集合,确保在初始化时进行深拷贝,并在需要提供外部访问时返回该变量的防御性拷贝(深拷贝)。
线程安全的成员变量:如果对象中引用了其他可变对象,则这些引用也必须是不可变的或线程安全的。
示例:不可变类
以下是一个实现不可变对象的 Java 示例:
public final class ImmutablePerson { private final String name; private final int age; // 构造函数 public ImmutablePerson(String name, int age) { this.name = name; this.age = age; } // getter 方法 public String getName() { return name; } public int getAge() { return age; } // 注意:没有 setter 方法 }
另一个例子,如果成员变量是一个可变对象:
public final class ImmutableArrayWrapper {
private final int[] myArray;
public ImmutableArrayWrapper(int[] arr) {
// 正确的初始化应该进行数组的拷贝,以避免原始数组变更导致 ImmutableArrayWrapper 状态改变
this.myArray = arr.clone();
}
public int[] getMyArray() {
// 返回 myArray 的拷贝版本而非原始引用
return myArray.clone();
}
}
这种方式确保,即使客户端修改了数组或集合,该类的内部表示也不会改变,从而保持了不变性。
Java 标准库中也有一些不可变类的例子,如 String
, BigDecimal
, BigInteger
, 等。这些类的实例一旦被分配就不能被修改,任何修改操作都会生成新的对象。
在实践中,使用不可变类通常会导致更清晰的程序设计和更简单的并发模型。不过,在创建大量小的和/或短生命期的不可变对象时需要注意可能对性能的影响,因为这可能会导致大量的内存分配和回收操作。在这些情况下,使用对象池或其他技术可以改善性能。
并发模式中的 Producer-Consumer 模式是一种常用的并发设计模式,用于在生产者线程(或进程)生成某种数据,并且由消费者线程(或进程)处理这些数据的情况。这个模式使用某种形式的共享队列来存储生产者生成的消息或任务,在消费者可用时由它们进行处理。
Producer-Consumer 模式的关键组件
模式工作流程
优势
实例
Java 中使用阻塞队列来实现 Producer-Consumer:
import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; class Producer implements Runnable { private final BlockingQueue<Integer> sharedQueue; public Producer(BlockingQueue<Integer> sharedQueue) { this.sharedQueue = sharedQueue; } public void run() { for (int i = 0; i < 10; i++) { try { System.out.println("Produced: " + i); sharedQueue.put(i); // 将生产的元素放入队列中 } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } } class Consumer implements Runnable { private final BlockingQueue<Integer> sharedQueue; public Consumer(BlockingQueue<Integer> sharedQueue) { this.sharedQueue = sharedQueue; } public void run() { while (true) { try { Integer item = sharedQueue.take(); // 从队列中取出元素 System.out.println("Consumed: " + item); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); break; } } } } public class Main { public static void main(String[] args) { BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>(); Thread prodThread = new Thread(new Producer(queue)); Thread consThread = new Thread(new Consumer(queue)); prodThread.start(); consThread.start(); } }
注意事项
Producer-Consumer 模式有效地提高了多线程程序处理任务和数据的效率,同时也隐藏了程序内部的并发复杂性。它是一种适用于多种情况的设计模式,可以被应用于生产环境中的任务分发、工作队列管理和异步处理等场景。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。