并发编程

141 阅读5分钟

什么是并发编程

并发编程是指在程序中同时执行多个独立的任务或操作的能力。它涉及到处理同时发生的多个任务,并通过合适的机制来协调和管理这些任务的执行。

并发编程的目标是提高程序的性能和响应能力,以充分利用计算资源。通过并发编程,可以同时处理多个任务,减少等待时间,提高系统的吞吐量和效率。

并发编程可以在多个层面和维度上实现,包括多线程编程、多进程编程、异步编程等。它可以在单个计算机上的多个核心或处理器之间实现并行计算,也可以在分布式系统中的多台计算机之间实现任务的并发执行。

并发编程需要注意并发访问共享资源的并发控制问题,如避免竞态条件、死锁和资源争用等。常用的并发编程技术包括使用锁、信号量、条件变量、原子操作等。

总而言之,并发编程是一种编程范式,用于处理多个任务的同时执行,提高程序的性能和响应能力。

并发编程的场景

两个没有先后关系的接口、数据的查询

并发场景参考

常见并发工具

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提供了丰富的方法来处理异步操作,如thenApplythenComposethenCombineallOf等。它还支持异常完成和超时等功能。

下面详细解释一些CompletableFuture的常用方法和功能:

  1. 创建CompletableFuture对象:

    • CompletableFuture.supplyAsync(Supplier<U> supplier):使用指定的Supplier在异步线程中执行计算,并返回一个CompletableFuture对象。
    • CompletableFuture.runAsync(Runnable runnable):使用指定的Runnable在异步线程中执行计算,并返回一个CompletableFuture<Void>对象。
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
        // 异步执行的任务
        return "Hello, world!";
    });
    
  2. 转换和组合操作:

    • 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>对象。
  3. 异常处理:

    • 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对象。
  4. 结果处理:

    • thenAccept(Consumer<T> action):在前一个阶段完成时,使用指定的消费者函数处理结果,没有返回值。
    • thenRun(Runnable action):在前一个阶段完成时,执行指定的操作,没有返回值。
  5. 其他功能:

    • 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: 线程池的饱和策略事件,主要有四种类型

线程池的顺序

image.png

线程池饱和处理

  • AbortPolicy(抛出一个异常,默认的)
  • DiscardPolicy(直接丢弃任务)
  • DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
  • CallerRunsPolicy(交给线程池调用所在的线程进行处理)