Java进阶-并发编程

117 阅读7分钟

1. 概念

Java Concurrency(并发) 是Java编程语言中用于处理多线程编程的一个关键部分。在并发编程中,多个线程可以同时执行,这可以提高程序的性能和响应性。然而,并发编程也带来了复杂性,如数据同步、线程安全、死锁等问题。

2. 主要组件

  • Executor
  • ExecutorService
  • ScheduledExecutorService
  • Future
  • CountDownLatch
  • CyclicBarrier
  • Semaphore
  • ThreadFactory
  • BlockingQueue
  • DelayQueue
  • Locks
  • Phaser

2.1.Executor

Executor 是一个接口,代表执行所提供任务的对象。 任务是否应在新线程或当前线程上运行取决于特定的实现(从哪里发起调用)。因此,使用此接口,我们可以将任务执行流程与实际任务执行机制分离开来。

这里需要注意的一点是,Executor并不严格要求任务的执行必须是异步的,最简单的情况是,执行器可以在调用线程中即时调用已提交的任务。

我们需要创建一个调用器来创建执行器实例:

public class Invoker implements Executor {
    @Override
    public void execute(Runnable r) {
        r.run();
    }
}

现在,我们可以使用该调用器来执行任务。

public void execute() {
        Executor executor = new Invoker();
        executor.execute( () -> {
        // task to be performed
        });
}

这里要注意的是,如果执行器不能接受任务执行 ,它将抛出RejectedExecutionException

2.2.ExecutorService

ExecutorService是异步处理的完整解决方案。它管理内存队列并根据线程可用性安排已提交的任务。

要使用ExecutorService, 我们需要创建一个Runnable类。

public class Task implements Runnable {
    @Override
    public void run() {
        // task details
    }
}

现在我们可以创建ExecutorService实例并分配此任务。在创建时,我们需要指定线程池大小。

ExecutorService executor = Executors.newFixedThreadPool(10);

如果我们想创建一个单线程的ExecutorService实例,我们可以使用newSingleThreadExecutor(ThreadFactory threadFactory) 来创建该实例。

一旦执行器创建完成,我们就可以使用它来提交任务。

public void execute() {
        executor.submit(new Task());
}

我们还可以在提交任务时创建Runnable实例。

executor.submit(() -> {
        new Task();
        });

它还带有两种现成的执行终止方法。第一种方法是shutdown() ;它会等待所有提交的任务完成执行。另一种方法是shutdownNow() ,它会尝试终止所有正在执行的任务并暂停等待任务的处理。

还有另一种方法awaitTermination(long timeout, TimeUnit unit), 该方法在触发关闭事件或发生执行超时后强制阻塞,直到所有任务都完成执行,或者执行线程本身被中断。

try {
        executor.awaitTermination( 20l, TimeUnit.NANOSECONDS );
        } catch (InterruptedException e) {
        e.printStackTrace();
    }

2.3. ScheduledExecutorService

ScheduledExecutorService是一个与 ExecutorService类似的接口,但是它可以定期执行任务。

Executor 和 ExecutorService的方法会当场调度,不会引入任何人为延迟。 零或任何负值表示需要立即执行请求。

方法描述
SingleThreadExecutor返回一个ExecutorService只有一个线程的。
newFixedThreadPool返回ExecutorService具有固定数量线程的。
newCachedThreadPool返回一个ExecutorService具有不同大小的线程池。
newSingleThreadScheduledExecutor返回一个ScheduledExecutorService具有单个线程的。
newScheduledThreadPool返回一ScheduledExecutorService组核心线程。
newWorkStealingPool返回一个工作窃取ExecutorService

我们可以使用RunnableCallable接口来定义任务。

public void execute() {
        ScheduledExecutorService executorService
        = Executors.newSingleThreadScheduledExecutor();

        Future<String> future = executorService.schedule(() -> {
        // ...
        return "Hello world";
        }, 1, TimeUnit.SECONDS);

        ScheduledFuture<?> scheduledFuture = executorService.schedule(() -> {
        // ...
        }, 1, TimeUnit.SECONDS);

        executorService.shutdown();
}

ScheduledExecutorService还可以 在给定的固定延迟后安排任务:

executorService.scheduleAtFixedRate(() -> {
        // ...
        }, 1, 10, TimeUnit.SECONDS);

        executorService.scheduleWithFixedDelay(() -> {
        // ...
        }, 1, 10, TimeUnit.SECONDS);

scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 方法创建并执行一个定期操作,该操作首先在提供的初始延迟后调用,然后以给定的周期执行,直到服务实例关闭。

scheduleWithFixedDelay ( Runnable command, long initialDelay, long delay, TimeUnit unit ) 方法创建并执行一个周期性操作,该操作在提供的初始延迟后首次调用,并在执行操作终止和下一个操作调用之间按照给定的延迟重复执行。

2.4.Future

Future用于表示异步操作的结果。 它带有用于检查异步操作是否完成、获取计算结果等方法。

cancel(boolean mayInterruptIfRunning)  API 会取消操作并释放正在执行的线程。如果mayInterruptIfRunning的值为 true,则执行任务的线程将立即终止。否则,正在进行的任务将被允许完成。

我们可以使用下面的代码来创建未来实例:

public void invoke() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        Future<String> future = executorService.submit(() -> {
        // ...
        Thread.sleep(10000l);
        return "Hello world";
        });
        }

我们可以使用以下代码片段来检查未来的结果是否准备就绪,并在计算完成后获取数据:

if (future.isDone() && !future.isCancelled()) {
        try {
        str = future.get();
        } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
        }
        }

我们还可以为给定操作指定超时。如果任务花费的时间超过此时间,则会抛出TimeoutException :

try {
        future.get(10, TimeUnit.SECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
        e.printStackTrace();
        }

2.5.CountDownLatch

CountDownLatch(在JDK 5中引入)是一个实用程序类,它会阻塞一组线程,直到某些操作完成。 CountDownLatch用计数器(整数类型)初始化;当相关线程完成执行时 该计数器会递减。但是一旦计数器达到零,其他线程就会被释放。

2.6.CyclicBarrier

CyclicBarrier 的工作原理与 CountDownLatch几乎相同,只是我们可以重复使用它。与CountDownLatch不同,它允许多个线程在调用最终任务之前使用 await() 方法(称为屏障条件)相互等待。

我们需要创建一个Runnable任务实例来启动屏障条件:

public class Task implements Runnable {

    private CyclicBarrier barrier;

    public Task(CyclicBarrier barrier) {
        this.barrier = barrier;
    }

    @Override
    public void run() {
        try {
            LOG.info(Thread.currentThread().getName() +
                    " is waiting");
            barrier.await();
            LOG.info(Thread.currentThread().getName() +
                    " is released");
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }

}

现在我们可以调用一些线程来竞争屏障条件: isBroken() 方法检查在执行期间是否有任何线程被中断。我们应该在执行实际过程之前始终执行此检查。

public void start() {

        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
        // ...
        LOG.info("All previous tasks are completed");
        });

        Thread t1 = new Thread(new Task(cyclicBarrier), "T1");
        Thread t2 = new Thread(new Task(cyclicBarrier), "T2");
        Thread t3 = new Thread(new Task(cyclicBarrier), "T3");

        if (!cyclicBarrier.isBroken()) {
        t1.start();
        t2.start();
        t3.start();
        }
}

2.7.信号量 Semaphore

信号用于阻止线程级访问部分物理或逻辑资源。信号量包含一组许可;每当线程尝试进入临界区时,它都需要检查信号量是否有许可。

如果许可证不可用(通过tryAcquire() ),则不允许线程跳转到临界区;但是,如果许可证可用,则授予访问权限,并且许可证计数器减少。 一旦执行线程释放临界区,允许计数器就会再次增加(通过release() 方法完成)。

我们可以使用tryAcquire(long timeout, TimeUnit unit) 方法指定获取访问权限的超时时间。

我们还可以检查可用许可证的数量或等待获取信号量的线程数。

下面的代码片段可用于实现信号量:

static Semaphore semaphore = new Semaphore(10);

public void execute() throws InterruptedException {

        LOG.info("Available permit : " + semaphore.availablePermits());
        LOG.info("Number of threads waiting to acquire: " +
        semaphore.getQueueLength());

        if (semaphore.tryAcquire()) {
        try {
        // ...
        }
        finally {
        semaphore.release();
        }
        }

}

2.8.线程工厂 ThreadFactory

ThreadFactory充当线程(不存在)池,可根据需要创建新线程。它消除了实现高效线程创建机制的大量样板代码的需要。

我们可以定义一个ThreadFactory

public class AppleThreadFactory implements ThreadFactory {
    private int threadId;
    private String name;

    public AppleThreadFactory(String name) {
        threadId = 1;
        this.name = name;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, name + "-Thread_" + threadId);
        LOG.info("created new thread with id : " + threadId +
                " and name : " + t.getName());
        threadId++;
        return t;
    }
}

我们可以使用这个newThread(Runnable r) 方法在运行时创建一个新线程:

AppleThreadFactory factory = new AppleThreadFactory(
        "AppleThreadFactory");
        for (int i = 0; i < 10; i++) {
        Thread t = factory.newThread(new Task());
        t.start();
        }

2.9. 阻塞队列 BlockingQueue

在异步编程中,最常见的集成模式之一是生产者-消费者模式。java.util.concurrent带有一个称为BlockingQueue的数据结构- 在这些异步场景中非常有用。

2.10.延迟队列 DelayQueue

DelayQueue是一个无限大小的元素阻塞队列,其中元素只有在到期时间(称为用户定义的延迟)完成后才能被提取。因此,最顶部的元素(head)将具有最大的延迟量,并且将最后被轮询。

2.11.锁 Lock

毫不奇怪,Lock是一个实用程序,用于阻止除当前正在执行的线程之外的其他线程访问特定的代码段。

Lock 和 Synchronized 块之间的主要区别在于,synchronized 块完全包含在一个方法中;但是,我们可以在单独的方法中使用 Lock API 的 lock() 和 unlock() 操作。

2.12.移相器 Phaser

Phaser是一种比CyclicBarrierCountDownLatch更灵活的解决方案- 用作可重复使用的屏障,动态数量的线程需要等待该屏障才能继续执行。我们可以协调多个执行阶段,并为每个程序阶段重复使用一个Phaser实例。