什么是并发编程
并发编程是指在程序中同时执行多个独立的任务或操作的能力。它涉及到处理同时发生的多个任务,并通过合适的机制来协调和管理这些任务的执行。
并发编程的目标是提高程序的性能和响应能力,以充分利用计算资源。通过并发编程,可以同时处理多个任务,减少等待时间,提高系统的吞吐量和效率。
并发编程可以在多个层面和维度上实现,包括多线程编程、多进程编程、异步编程等。它可以在单个计算机上的多个核心或处理器之间实现并行计算,也可以在分布式系统中的多台计算机之间实现任务的并发执行。
并发编程需要注意并发访问共享资源的并发控制问题,如避免竞态条件、死锁和资源争用等。常用的并发编程技术包括使用锁、信号量、条件变量、原子操作等。
总而言之,并发编程是一种编程范式,用于处理多个任务的同时执行,提高程序的性能和响应能力。
并发编程的场景
两个没有先后关系的接口、数据的查询
常见并发工具
CountDownLatch
CountDownLatch 是一个同步工具类,可以用于在多个线程之间依次执行某些操作。它提供了一种控制多个线程执行顺序的方式,并且可以在所有线程都完成特定操作后继续执行下一个操作
常用方法
await():用于等待所有线程执行完毕。在使用countDown()方法减少等待线程数后,剩余的线程将会在await()方法调用后开始执行。如果在等待过程中发生异常,则会抛出CountDownLatch.await()方法中的异常。countDown():用于减少等待线程数。当countDown()方法被调用时,剩余的线程将会开始执行,而不用等待所有线程执行完毕。await(long time, TimeUnit unit):用于等待指定时间。当指定的时间到达时,该方法将自动抛出异常
使用示例
//开启线程获取阈值和实时曝光数据
ThreadPoolExecutor threadPool = BusinessDetailThreadPool.getThreadPool();
int countDown = 2;
CountDownLatch countDownLatch = new CountDownLatch(countDown);
threadPool.execute(() -> {
try {
//获取阈值
limitMapping.putAll(businessDetailService.getImplLimit(sourceId, repaymentMapping, bizList));
} catch (Exception e) {
log.error("获取曝光阈值异常:", e);
} finally {
countDownLatch.countDown();
}
});
threadPool.execute(() -> {
try {
//获取业务线实时曝光
implMapping.putAll(businessDetailService.getRealImplData(pid, eid, repaymentMapping, bizList));
} catch (Exception e) {
log.error("获取实时曝光值异常:", e);
} finally {
countDownLatch.countDown();
}
});
try {
countDownLatch.await(50L, TimeUnit.MILLISECONDS);
} catch (Exception e) {
log.error("获取曝光阈值异常 + 获取实时曝光值 error:", e);
}
if (log.isInfoEnabled()) {
log.info("曝光阈值数据 limitMapping={}", GsonUtil.toJsonString(limitMapping));
log.info("实时曝光数据 implMapping={}", GsonUtil.toJsonString(implMapping));
}
效果是:两个线程都获取数据完成后,才会执行下面的打日志操作。
Atomic
Java中的Atomic包提供了原子类型的变量,这些变量可以在多线程环境下被安全地使用。Atomic包中提供了AtomicBoolean、AtomicInteger、AtomicLong和AtomicIntegerArray类。这些类可以用于保护某些共享资源,以确保多个线程对它们进行读写操作时正确的顺序和数值。
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Counter value: " + counter.get());
}
}
在这个示例中,我们创建了一个AtomicInteger对象counter,并使用incrementAndGet()方法对其进行原子递增操作。我们创建了两个线程,每个线程都会对counter执行1000次递增操作。最后,我们打印出counter的值,预期结果应为2000。
使用AtomicInteger类可以确保对counter的并发访问是线程安全的,避免了竞态条件和数据不一致的问题。
原子类的原理:使用volatile关键字修饰值,保证值的可见性和内存栅栏;修改这个值是通过CAS来修改的也就保证了这个值的线程安全性,然后有用了循环来做,也就是如果修改失败会重新获取值然后继续CAS加一
几种并发编程的实现方式
Thread
new Thread(
@Override
public void run() {}
).start();
或者
public class MyThread extends Thread {
@Override
public void run() {
// 线程执行的逻辑
System.out.println("Thread is running.");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
但是这种方式显而易见的存在一个致命的缺陷:创建对象时就将线程执行者与要执行的任务绑定在了一块儿,使用起来不怎么灵活;并且只能extends一个类,很不方便。所以可以通过实现Runnable接口的实现类创建出多线程任务对象。
Runnable
public class Task implements Runnable{
@Override
public void run() {}
public static void main(String[] args){
Task task = new Task();
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();t2.start();
// 或者也可以这样!
Runnable taskRunnable = new Runnable(){
@Override
public void run() {}
};
Thread tA = new Thread(task);
Thread tB = new Thread(task);
tA.start();tB.start();
}
}
总的来说,上面的两种方式都将执行者线程实体Thread对象和任务Runnable对象分开,在实际编码过程中,可以选择多条线程同时执行一个task任务,这种方式会使得多线程编程更加灵活。
但是在实际开发过程中,往往有时候的多线程任务执行完成之后是需要返回值的,但是Runnable接口的run()方法是void无返回类型的,那么当需要返回值时又该怎么办呢?此时我们就可以用到Callable。
Callable
public class CallableThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 线程执行的逻辑
return 42;
}
public static void main(String[] args) throws Exception {
CallableThread callableThread = new CallableThread();
FutureTask<String> futureTask = new FutureTask<>(callableThread);
Thread thread = new Thread(futureTask);
thread.start();
String result = futureTask.get();
System.out.println("执行结果= " + result);
}
}
CompletableFuture
CompletableFuture是Java中的一个类,用于表示异步计算的未来结果。它用于组合和链接异步操作。
CompletableFuture提供了丰富的方法来处理异步操作,如thenApply、thenCompose、thenCombine和allOf等。它还支持异常完成和超时等功能。
下面详细解释一些CompletableFuture的常用方法和功能:
-
创建
CompletableFuture对象:CompletableFuture.supplyAsync(Supplier<U> supplier):使用指定的Supplier在异步线程中执行计算,并返回一个CompletableFuture对象。CompletableFuture.runAsync(Runnable runnable):使用指定的Runnable在异步线程中执行计算,并返回一个CompletableFuture<Void>对象。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { // 异步执行的任务 return "Hello, world!"; }); -
转换和组合操作:
thenApply(Function<T, U> fn):将一个函数应用于前一个阶段的结果,并返回一个新的CompletableFuture对象。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello") .thenApply(s -> s + " world");thenCompose(Function<T, CompletableFuture<U>> fn):将一个函数应用于前一个阶段的结果,该函数返回一个CompletableFuture对象,然后将这些对象连接起来形成一个新的CompletableFuture对象。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello") .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " world"));thenCombine(CompletionStage<U> other, BiFunction<T, U, V> fn):将当前和另一个CompletionStage对象的结果应用于指定的函数,并返回一个新的CompletableFuture对象。
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello"); CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> " world"); CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (s1, s2) -> s1 + s2);allOf(CompletableFuture<?>... cfs):接收一个CompletableFuture数组,等待所有的CompletableFuture对象都完成后返回一个新的CompletableFuture<Void>对象。
-
异常处理:
exceptionally(Function<Throwable, T> fn):在前一个阶段发生异常时,使用指定的函数处理异常,并返回一个新的CompletableFuture对象,用于处理异常情况。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { throw new RuntimeException("Oops!"); }).exceptionally(ex -> "Handled exception: " + ex.getMessage());handle(BiFunction<T, Throwable, U> fn):在前一个阶段完成时,使用指定的函数处理结果或异常,并返回一个新的CompletableFuture对象。
-
结果处理:
thenAccept(Consumer<T> action):在前一个阶段完成时,使用指定的消费者函数处理结果,没有返回值。thenRun(Runnable action):在前一个阶段完成时,执行指定的操作,没有返回值。
-
其他功能:
join():等待CompletableFuture完成并返回结果,如果计算过程中发生异常,则将异常抛出。get():等待CompletableFuture完成并返回结果,如果计算过程中发生异常,则将异常包装成ExecutionException抛出。complete(T value):手动完成CompletableFuture并设置结果值。completeExceptionally(Throwable ex):手动完成CompletableFuture并设置异常结果。
这只是CompletableFuture的一些常用方法和功能,它还提供了更多的方法来处理、组合和操作异步计算的结果。
线程池
线程池的关键参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize: 线程池核心线程数最大值
- maximumPoolSize: 线程池最大线程数大小
- keepAliveTime: 线程池中非核心线程空闲的存活时间大小
- unit: 线程空闲存活时间单位
- workQueue: 存放任务的阻塞队列
- threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
- handler: 线程池的饱和策略事件,主要有四种类型
线程池的顺序
线程池饱和处理
- AbortPolicy(抛出一个异常,默认的)
- DiscardPolicy(直接丢弃任务)
- DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
- CallerRunsPolicy(交给线程池调用所在的线程进行处理)