86、Java并发工具详解

Java并发工具详解

1. 使用执行器(Executor)

在Java并发编程中,执行器(Executor)是一个强大的特性,它可以启动和控制线程的执行,为通过 Thread 类管理线程提供了一种替代方案。

执行器的核心是 Executor 接口,它定义了一个方法:

void execute(Runnable thread)

该方法用于执行指定的线程,即启动指定的线程。

ExecutorService 接口扩展了 Executor ,添加了一些有助于管理和控制线程执行的方法。例如, shutdown() 方法用于停止调用的 ExecutorService

void shutdown()

同时, ExecutorService 还定义了执行返回结果的线程、执行一组线程以及确定关闭状态的方法。

ScheduledExecutorService 接口进一步扩展了 ExecutorService ,支持线程的调度。

Java并发API定义了三个预定义的执行器类: ThreadPoolExecutor ScheduledThreadPoolExecutor ForkJoinPool
- ThreadPoolExecutor 实现了 Executor ExecutorService 接口,支持管理线程池。
- ScheduledThreadPoolExecutor 还实现了 ScheduledExecutorService 接口,允许对线程池进行调度。
- ForkJoinPool 实现了 Executor ExecutorService 接口,供Fork/Join框架使用。

线程池提供了一组线程,用于执行各种任务。与每个任务使用自己的线程不同,线程池中的线程被复用,从而减少了创建多个独立线程的开销。通常,可以通过调用 Executors 工具类定义的静态工厂方法来获取执行器,例如:

static ExecutorService newCachedThreadPool()
static ExecutorService newFixedThreadPool(int numThreads)
static ScheduledExecutorService newScheduledThreadPool(int numThreads)
  • newCachedThreadPool() 创建一个根据需要添加线程但尽可能复用线程的线程池。
  • newFixedThreadPool() 创建一个包含指定数量线程的线程池。
  • newScheduledThreadPool() 创建一个支持线程调度的线程池。

下面是一个简单的执行器示例:

// A simple example that uses an Executor.
import java.util.concurrent.*;

class SimpExec {
  public static void main(String args[]) {
    CountDownLatch cdl = new CountDownLatch(5);
    CountDownLatch cdl2 = new CountDownLatch(5);
    CountDownLatch cdl3 = new CountDownLatch(5);
    CountDownLatch cdl4 = new CountDownLatch(5);
    ExecutorService es = Executors.newFixedThreadPool(2);

    System.out.println("Starting");

    // Start the threads.
    es.execute(new MyThread(cdl, "A"));
    es.execute(new MyThread(cdl2, "B"));
    es.execute(new MyThread(cdl3, "C"));
    es.execute(new MyThread(cdl4, "D"));

    try {
      cdl.await();
      cdl2.await();
      cdl3.await();
      cdl4.await();
    } catch (InterruptedException exc) {
      System.out.println(exc);
    }

    es.shutdown();
    System.out.println("Done");
  }
}

class MyThread implements Runnable {
  String name;
  CountDownLatch latch;

  MyThread(CountDownLatch c, String n) {
    latch = c;
    name = n;

    new Thread(this);
  }

  public void run() {
    for(int i = 0; i < 5; i++) {
      System.out.println(name + ": " + i);
      latch.countDown();
    }
  }
}

该程序创建了一个包含两个线程的固定线程池,并使用该线程池执行四个任务。四个任务共享两个线程,任务完成后,线程池关闭,程序结束。

执行结果可能如下:

Starting
A: 0
A: 1
A: 2
A: 3
A: 4
C: 0
C: 1
C: 2
C: 3
C: 4
D: 0
D: 1
D: 2
D: 3
D: 4
B: 0
B: 1
B: 2
B: 3
B: 4
Done

从输出可以看出,即使线程池只包含两个线程,所有四个任务仍然可以执行,但同一时间只能有两个任务运行,其他任务需要等待线程池中的线程可用。

需要注意的是,调用 shutdown() 方法很重要,如果程序中没有调用该方法,程序将不会终止,因为执行器会保持活动状态。可以尝试注释掉 shutdown() 方法的调用,观察结果。

2. 使用 Callable Future

Java并发API中一个有趣的特性是 Callable 接口,它表示一个返回值的线程。应用程序可以使用 Callable 对象计算结果,并将结果返回给调用线程。这是一种强大的机制,有助于编写许多类型的数值计算,其中部分结果可以同时计算。它还可以用于运行返回状态码的线程,以指示线程的成功完成。

Callable 是一个泛型接口,定义如下:

interface Callable<V>

其中, V 表示任务返回的数据类型。 Callable 只定义了一个方法 call()

V call() throws Exception

call() 方法中定义要执行的任务,任务完成后返回结果。如果无法计算结果, call() 方法必须抛出异常。

Callable 任务由 ExecutorService 通过调用其 submit() 方法执行,其中一种形式用于执行 Callable

<T> Future<T> submit(Callable<T> task)

这里, task 是要在其自己的线程中执行的 Callable 对象,结果通过 Future 对象返回。

Future 是一个泛型接口,表示 Callable 对象将返回的值。由于该值是在未来某个时间获得的,因此命名为 Future 是合适的。 Future 定义如下:

interface Future<V>

其中, V 指定结果的类型。

要获取返回值,可以调用 Future get() 方法,有两种形式:

V get() throws InterruptedException, ExecutionException
V get(long wait, TimeUnit tu) throws InterruptedException, ExecutionException, TimeoutException

第一种形式会无限期等待结果,第二种形式允许指定等待的超时时间。

下面是一个示例程序,展示了如何使用 Callable Future

import java.util.concurrent.*;

class CallableDemo {
  public static void main(String args[]) {
    ExecutorService es = Executors.newFixedThreadPool(3);
    Future<Integer> f;
    Future<Double> f2;
    Future<Integer> f3;

    System.out.println("Starting");

    f = es.submit(new Sum(10));
    f2 = es.submit(new Hypot(3, 4));
    f3 = es.submit(new Factorial(5));

    try {
      System.out.println(f.get());
      System.out.println(f2.get());
      System.out.println(f3.get());
    } catch (InterruptedException exc) {
      System.out.println(exc);
    } catch (ExecutionException exc) {
      System.out.println(exc);
    }

    es.shutdown();
    System.out.println("Done");
  }
}

class Sum implements Callable<Integer> {
  int stop;

  Sum(int v) {
    stop = v;
  }

  public Integer call() {
    int sum = 0;
    for(int i = 1; i <= stop; i++) {
      sum += i;
    }
    return sum;
  }
}

class Hypot implements Callable<Double> {
  double side1, side2;

  Hypot(double s1, double s2) {
    side1 = s1;
    side2 = s2;
  }

  public Double call() {
    return Math.sqrt((side1 * side1) + (side2 * side2));
  }
}

class Factorial implements Callable<Integer> {
  int stop;

  Factorial(int v) {
    stop = v;
  }

  public Integer call() {
    int fact = 1;
    for(int i = 2; i <= stop; i++) {
      fact *= i;
    }
    return fact;
  }
}

程序创建了三个任务,分别执行不同的计算。第一个任务返回一个值的求和结果,第二个任务计算直角三角形的斜边长度,第三个任务计算一个值的阶乘。所有三个计算同时进行。

执行结果如下:

Starting
55
5.0
120
Done
3. TimeUnit 枚举

Java并发API定义了一些方法,接受 TimeUnit 类型的参数,表示超时时间。 TimeUnit 是一个枚举,用于指定时间的粒度(或分辨率)。 TimeUnit 定义在 java.util.concurrent 中,可能的值包括:
- DAYS
- HOURS
- MINUTES
- SECONDS
- MICROSECONDS
- MILLISECONDS
- NANOSECONDS

虽然 TimeUnit 允许在调用接受定时参数的方法时指定这些值,但不能保证系统能够达到指定的分辨率。

下面是一个使用 TimeUnit 的示例,修改了前面的 CallableDemo 类,使用 get() 方法的第二种形式:

try {
  System.out.println(f.get(10, TimeUnit.MILLISECONDS));
  System.out.println(f2.get(10, TimeUnit.MILLISECONDS));
  System.out.println(f3.get(10, TimeUnit.MILLISECONDS));
} catch (InterruptedException exc) {
  System.out.println(exc);
} catch (ExecutionException exc) {
  System.out.println(exc);
} catch (TimeoutException exc) {
  System.out.println(exc);
}

在这个版本中,每个 get() 方法的调用等待时间不会超过10毫秒。

TimeUnit 枚举还定义了各种单位转换方法和定时方法:
| 方法 | 描述 |
| — | — |
| long convert(long tval, TimeUnit tu) | 将 tval 转换为指定的单位并返回结果 |
| long toMicros(long tval) | 将 tval 转换为微秒 |
| long toMillis(long tval) | 将 tval 转换为毫秒 |
| long toNanos(long tval) | 将 tval 转换为纳秒 |
| long toSeconds(long tval) | 将 tval 转换为秒 |
| long toDays(long tval) | 将 tval 转换为天 |
| long toHours(long tval) | 将 tval 转换为小时 |
| long toMinutes(long tval) | 将 tval 转换为分钟 |
| void sleep(long delay) throws InterruptedExecution | 暂停执行指定的延迟时间,相当于调用 Thread.sleep() |
| void timedJoin(Thread thrd, long delay) throws InterruptedExecution | 是 Thread.join() 的特殊版本, thrd 暂停指定的时间 |
| void timedWait(Object obj, long delay) throws InterruptedExecution | 是 Object.wait() 的特殊版本,等待 obj 指定的时间 |

4. 并发集合

Java并发API定义了几个为并发操作而设计的集合类,包括:
- ArrayBlockingQueue
- ConcurrentHashMap
- ConcurrentLinkedDeque
- ConcurrentLinkedQueue
- ConcurrentSkipListMap
- ConcurrentSkipListSet
- CopyOnWriteArrayList
- CopyOnWriteArraySet
- DelayQueue
- LinkedBlockingDeque
- LinkedBlockingQueue
- LinkedTransferQueue
- PriorityBlockingQueue
- SynchronousQueue

这些集合为集合框架中相关类提供了并发替代方案,它们的工作方式与其他集合类似,但提供了并发支持。熟悉集合框架的程序员使用这些并发集合不会有困难。

5. 锁

java.util.concurrent.locks 包提供了对锁的支持,锁是一种对象,为使用 synchronized 控制对共享资源的访问提供了一种替代方案。

一般来说,锁的工作方式如下:在访问共享资源之前,获取保护该资源的锁;访问资源完成后,释放锁。如果第二个线程在另一个线程使用锁时尝试获取锁,第二个线程将暂停,直到锁被释放。这样可以防止对共享资源的冲突访问。

锁在多个线程需要访问共享数据的值时特别有用。例如,一个库存应用程序可能有一个线程,先确认某个商品有库存,然后在每次销售时减少库存数量。如果有两个或更多这样的线程运行,没有某种同步机制,可能会出现一个线程正在进行交易时,第二个线程开始其交易的情况。结果可能是两个线程都认为有足够的库存,即使手头的库存只够满足一次销售。在这种情况下,锁提供了一种方便的方式来处理所需的同步。

Lock 接口定义了一个锁,其定义的方法如下表所示:
| 方法 | 描述 |
| — | — |
| void lock() | 等待直到可以获取调用的锁 |
| void lockInterruptibly() throws InterruptedException | 等待直到可以获取调用的锁,除非被中断 |
| Condition newCondition() | 返回与锁关联的 Condition 对象 |
| boolean tryLock() | 尝试获取锁,如果锁不可用,不会等待,获取锁返回 true ,否则返回 false |
| boolean tryLock(long wait, TimeUnit tu) throws InterruptedException | 尝试获取锁,如果锁不可用,等待不超过 wait 指定的时间,获取锁返回 true ,否则返回 false |
| void unlock() | 释放锁 |

java.util.concurrent.locks 提供了 Lock 的一个实现 ReentrantLock ,它实现了可重入锁,即当前持有锁的线程可以多次进入该锁。当然,在这种情况下,所有对 lock() 的调用必须有相同数量的 unlock() 调用相匹配,否则,试图获取锁的线程将暂停,直到锁可用。

下面是一个使用锁的示例程序:

import java.util.concurrent.locks.*;

class LockDemo {
  public static void main(String args[]) {
    ReentrantLock lock = new ReentrantLock();

    new LockThread(lock, "A");
    new LockThread(lock, "B");
  }
}

// A shared resource.
class Shared {
  static int count = 0;
}

// A thread of execution that increments count.
class LockThread implements Runnable {
  String name;
  ReentrantLock lock;

  LockThread(ReentrantLock lk, String n) {
    lock = lk;
    name = n;
    new Thread(this).start();
  }

  public void run() {
    System.out.println("Starting " + name);

    try {
      // First, lock count.
      System.out.println(name + " is waiting to lock count.");
      lock.lock();
      System.out.println(name + " is locking count.");

      Shared.count++;
      System.out.println(name + ": " + Shared.count);

      // Now, allow a context switch -- if possible.
      System.out.println(name + " is sleeping.");
      Thread.sleep(1000);
    } catch (InterruptedException exc) {
      System.out.println(exc);
    } finally {
      // Unlock
      System.out.println(name + " is unlocking count.");
      lock.unlock();
    }
  }
}

执行结果可能如下:

Starting A
A is waiting to lock count.
A is locking count.
A: 1
A is sleeping.
Starting B
B is waiting to lock count.
A is unlocking count.
B is locking count.
B: 2
B is sleeping.
B is unlocking count.

java.util.concurrent.locks 还定义了 ReadWriteLock 接口,该接口指定了一个为读和写访问维护单独锁的锁,只要资源没有被写入,就可以为资源的读取者授予多个锁。 ReentrantReadWriteLock 提供了 ReadWriteLock 的实现。

需要注意的是,JDK 8添加了一个特殊的锁 StampedLock ,它不实现 Lock ReadWriteLock 接口,但提供了一种机制,使其某些方面可以像 Lock ReadWriteLock 一样使用。

6. 原子操作

java.util.concurrent.atomic 包为读取或写入某些类型变量的值提供了一种替代其他同步功能的方法。该包提供了在一个不可中断(即原子)操作中获取、设置或比较变量值的方法,这意味着不需要锁或其他同步机制。

原子操作通过使用类(如 AtomicInteger AtomicLong )和方法(如 get() set() compareAndSet() decrementAndGet() getAndSet() )来实现,这些方法执行其名称所指示的操作。

下面是一个示例,展示了如何使用 AtomicInteger 同步对共享整数的访问:

import java.util.concurrent.atomic.*;

class AtomicDemo {
  public static void main(String args[]) {
    new AtomThread("A");
    new AtomThread("B");
    new AtomThread("C");
  }
}

class Shared {
  static AtomicInteger ai = new AtomicInteger(0);
}

class AtomThread implements Runnable {
  String name;

  AtomThread(String n) {
    name = n;
    new Thread(this).start();
  }

  public void run() {
    System.out.println("Starting " + name);
    for(int i = 1; i <= 3; i++) {
      System.out.println(name + " got: " + Shared.ai.getAndSet(i));
    }
  }
}

在这个程序中, Shared 类创建了一个静态的 AtomicInteger 对象 ai ,然后创建了三个 AtomThread 类型的线程。在 run() 方法中,通过调用 getAndSet() 方法修改 Shared.ai 的值,该方法返回前一个值,然后将值设置为传入的参数。使用 AtomicInteger 可以防止两个线程同时写入 ai

一般来说,当只涉及单个变量时,原子操作提供了一种方便(可能更高效)的替代其他同步机制的方法。从JDK 8开始, java.util.concurrent.atomic 还提供了四个支持无锁累积操作的类: DoubleAccumulator DoubleAdder LongAccumulator LongAdder 。累加器类支持一系列用户指定的操作,加法器类维护一个累积和。

7. 通过Fork/Join框架进行并行编程

近年来,软件开发中出现了一个重要的新趋势:并行编程。并行编程通常指的是利用包含两个或多个处理器(多核)的计算机的技术。正如大多数读者所知,多核计算机越来越普遍。多处理器环境的优势是能够显著提高程序性能。因此,越来越需要一种机制,使Java能够充分利用多核处理器的优势。

Fork/Join框架是Java提供的一种并行编程机制,它通过将一个大任务分解为多个小任务(Fork),然后并行执行这些小任务,最后将结果合并(Join),从而提高程序的性能。

Fork/Join框架的核心是 ForkJoinPool ForkJoinTask ForkJoinPool 是一个线程池,用于执行 ForkJoinTask ForkJoinTask 是一个抽象类,有两个重要的子类: RecursiveAction RecursiveTask RecursiveAction 用于没有返回值的任务, RecursiveTask 用于有返回值的任务。

下面是一个简单的使用Fork/Join框架的示例,计算一个数组中所有元素的和:

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

class SumTask extends RecursiveTask<Integer> {
    private static final int THRESHOLD = 10;
    private int[] array;
    private int start;
    private int end;

    public SumTask(int[] array, int start, int end) {
        this.array = array;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        if (end - start <= THRESHOLD) {
            int sum = 0;
            for (int i = start; i < end; i++) {
                sum += array[i];
            }
            return sum;
        } else {
            int mid = (start + end) / 2;
            SumTask leftTask = new SumTask(array, start, mid);
            SumTask rightTask = new SumTask(array, mid, end);

            leftTask.fork();
            int rightResult = rightTask.compute();
            int leftResult = leftTask.join();

            return leftResult + rightResult;
        }
    }
}

public class ForkJoinExample {
    public static void main(String[] args) {
        int[] array = new int[100];
        for (int i = 0; i < 100; i++) {
            array[i] = i + 1;
        }

        ForkJoinPool forkJoinPool = new ForkJoinPool();
        SumTask task = new SumTask(array, 0, array.length);
        int result = forkJoinPool.invoke(task);

        System.out.println("Sum: " + result);
    }
}

在这个示例中, SumTask 继承自 RecursiveTask ,用于计算数组中指定范围元素的和。如果任务的规模小于阈值 THRESHOLD ,则直接计算和;否则,将任务分解为两个子任务,分别计算左半部分和右半部分的和,然后将结果合并。

通过Fork/Join框架,可以充分利用多核处理器的并行计算能力,提高程序的性能。

总结

Java并发工具提供了丰富的功能,包括执行器、 Callable Future TimeUnit 枚举、并发集合、锁、原子操作和Fork/Join框架等。这些工具可以帮助开发者更方便地编写高效、安全的并发程序。在实际应用中,需要根据具体的需求选择合适的工具和技术,以充分发挥Java并发编程的优势。

Java并发工具详解

8. Fork/Join框架的工作流程

Fork/Join框架的工作流程可以用以下mermaid流程图表示:

graph TD;
    A[开始] --> B[创建大任务];
    B --> C{Fork: 任务是否可分解?};
    C -- 是 --> D[分解为小任务];
    D --> E[并行执行小任务];
    E --> F{所有小任务完成?};
    F -- 是 --> G[Join: 合并结果];
    G --> H[结束];
    C -- 否 --> I[直接执行任务];
    I --> H;
    F -- 否 --> E;

具体步骤如下:
1. 创建大任务 :定义一个继承自 ForkJoinTask (通常是 RecursiveAction RecursiveTask )的任务类,在主程序中创建该任务的实例。
2. Fork阶段 :在任务的 compute() 方法中,判断任务是否可以继续分解。如果可以,将大任务分解为多个小任务,并使用 fork() 方法将这些小任务提交到线程池中并行执行。
3. 并行执行小任务 ForkJoinPool 会自动调度这些小任务,让它们在不同的线程中并行执行。
4. Join阶段 :在小任务执行完成后,使用 join() 方法获取小任务的结果,并将这些结果合并。
5. 结束 :当所有小任务的结果都合并完成后,得到最终的结果。

9. 并发工具的选择建议

在实际开发中,需要根据不同的场景选择合适的并发工具,以下是一些选择建议:
| 场景 | 适用工具 |
| — | — |
| 简单的线程管理 | Executor 和线程池,如 Executors.newFixedThreadPool() |
| 需要返回值的线程任务 | Callable Future |
| 处理定时任务 | ScheduledExecutorService |
| 并发集合操作 | 并发集合类,如 ConcurrentHashMap ConcurrentLinkedQueue 等 |
| 控制对共享资源的访问 | 锁,如 ReentrantLock ReadWriteLock |
| 单个变量的原子操作 | java.util.concurrent.atomic 包中的类,如 AtomicInteger |
| 大规模并行计算 | Fork/Join框架 |

10. 并发编程的注意事项

在使用Java并发工具进行编程时,需要注意以下几点:
1. 线程安全 :确保对共享资源的访问是线程安全的,可以使用锁、原子操作等机制来保证。
2. 避免死锁 :在使用锁时,要注意避免死锁的发生。死锁通常是由于多个线程相互等待对方释放锁而导致的。可以通过合理的锁获取顺序、使用 tryLock() 方法等方式来避免死锁。
3. 资源管理 :在使用线程池、锁等资源时,要及时释放资源,避免资源泄漏。例如,在使用线程池时,要调用 shutdown() 方法关闭线程池。
4. 性能优化 :根据具体的场景选择合适的并发工具和算法,避免不必要的同步和锁竞争,以提高程序的性能。

11. 综合示例:并发库存管理系统

以下是一个综合示例,展示了如何使用并发工具实现一个简单的库存管理系统。该系统有多个线程同时处理商品的销售和库存查询操作,使用 ReentrantLock 来保证对库存的并发访问安全。

import java.util.concurrent.locks.ReentrantLock;

class Inventory {
    private int stock;
    private ReentrantLock lock;

    public Inventory(int stock) {
        this.stock = stock;
        this.lock = new ReentrantLock();
    }

    public boolean sell(int quantity) {
        lock.lock();
        try {
            if (stock >= quantity) {
                stock -= quantity;
                System.out.println("成功销售 " + quantity + " 件商品,剩余库存: " + stock);
                return true;
            } else {
                System.out.println("库存不足,无法销售 " + quantity + " 件商品,当前库存: " + stock);
                return false;
            }
        } finally {
            lock.unlock();
        }
    }

    public int getStock() {
        lock.lock();
        try {
            return stock;
        } finally {
            lock.unlock();
        }
    }
}

class SalesThread implements Runnable {
    private Inventory inventory;
    private int quantity;

    public SalesThread(Inventory inventory, int quantity) {
        this.inventory = inventory;
        this.quantity = quantity;
    }

    @Override
    public void run() {
        inventory.sell(quantity);
    }
}

public class InventoryManagementSystem {
    public static void main(String[] args) {
        Inventory inventory = new Inventory(100);

        // 创建多个销售线程
        Thread t1 = new Thread(new SalesThread(inventory, 20));
        Thread t2 = new Thread(new SalesThread(inventory, 30));
        Thread t3 = new Thread(new SalesThread(inventory, 50));

        // 启动线程
        t1.start();
        t2.start();
        t3.start();

        // 等待所有线程执行完成
        try {
            t1.join();
            t2.join();
            t3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出最终库存
        System.out.println("最终库存: " + inventory.getStock());
    }
}

在这个示例中, Inventory 类表示库存,使用 ReentrantLock 来保证对库存的并发访问安全。 SalesThread 类表示销售线程,负责处理商品的销售操作。在主程序中,创建了多个销售线程并启动它们,最后输出最终的库存数量。

12. 总结与展望

Java并发工具为开发者提供了丰富的功能和强大的支持,使得编写高效、安全的并发程序变得更加容易。通过合理使用 Executor Callable Future 、并发集合、锁、原子操作和Fork/Join框架等工具,可以充分发挥多核处理器的优势,提高程序的性能和响应能力。

在未来的开发中,随着计算机硬件的不断发展和应用场景的不断复杂化,并发编程将变得越来越重要。开发者需要不断学习和掌握新的并发技术和工具,以应对各种挑战。同时,也要注意并发编程中的线程安全、性能优化等问题,确保程序的稳定性和可靠性。

总之,Java并发工具是Java编程中不可或缺的一部分,掌握这些工具的使用方法,将有助于开发者编写更加高效、安全的并发程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值