84、Java并发工具包:提升多线程编程效率的利器

Java并发工具包:提升多线程编程效率的利器

1. Java成像与并发工具概述

Java不仅在图形成像方面提供了丰富的类库,还在多线程和并发编程领域有着强大的支持。在成像方面,除了本章介绍的成像类, java.awt.image 包还提供了其他类,能对成像过程进行更精细的控制,并支持高级成像技术。此外, javax.imageio 包支持处理各种图像格式的插件,对于追求复杂图形输出的开发者来说,这些额外的类值得深入探索。

在多线程和并发编程领域,Java从一开始就提供了内置的多线程和同步支持,例如可以通过实现 Runnable 接口或继承 Thread 类来创建新线程,使用 synchronized 关键字实现同步,通过 Object 类的 wait() notify() 方法实现线程间通信。然而,对于一些大量使用多线程的应用程序来说,这种内置支持并不理想,因为它缺乏一些高级特性,如信号量、线程池和执行管理器等,这些特性有助于创建高度并发的程序。

2. 并发API的发展历程

为了满足并发程序的需求,JDK 5引入了并发工具包,也称为并发API。这个初始版本提供了许多并发应用程序开发者长期以来所期望的功能,包括同步器(如信号量)、线程池、执行管理器、锁、并发集合,以及一种简洁的使用线程获取计算结果的方式。

JDK 7对并发API进行了重大扩展,其中最重要的是引入了Fork/Join框架。该框架有助于创建利用多个处理器的程序,实现真正的并行执行,而不仅仅是时间片轮转。随着多核系统的普及,Fork/Join框架的引入既及时又强大。JDK 8进一步增强了Fork/Join框架,并为并发API的其他部分添加了一些新特性。并发API不断发展和扩展,以满足当代计算环境的需求。

3. 并发API的核心包

并发工具包主要包含在 java.util.concurrent 包及其两个子包 java.util.concurrent.atomic java.util.concurrent.locks 中。下面是这些包的简要概述:
- java.util.concurrent :定义了支持替代内置同步和线程间通信方法的核心功能,包括同步器、执行器、并发集合和Fork/Join框架。
- 同步器 :提供了高级的多线程交互同步方式,包括信号量(Semaphore)、倒计时门闩(CountDownLatch)、循环屏障(CyclicBarrier)、交换器(Exchanger)和移相器(Phaser)。
- 执行器 :管理线程执行,包括执行器接口(Executor)、执行器服务(ExecutorService)及其实现类,如线程池执行器(ThreadPoolExecutor)、定时线程池执行器(ScheduledThreadPoolExecutor)和ForkJoin池(ForkJoinPool)。
- 并发集合 :提供了与集合框架中相关类对应的并发替代类,如 ConcurrentHashMap ConcurrentLinkedQueue CopyOnWriteArrayList
- Fork/Join框架 :支持并行编程,主要类包括 ForkJoinTask ForkJoinPool RecursiveTask RecursiveAction
- java.util.concurrent.atomic :便于在并发环境中使用变量,提供了一种无需使用锁即可高效更新变量值的方法,通过 AtomicInteger AtomicLong 等类以及 compareAndSet() decrementAndGet() getAndSet() 等方法实现。
- java.util.concurrent.locks :提供了使用同步方法的替代方案,核心是 Lock 接口,定义了获取和释放对象访问权限的基本机制,关键方法包括 lock() tryLock() unlock() ,使用这些方法可以更好地控制同步。

4. 同步对象的使用

同步对象由 Semaphore CountDownLatch CyclicBarrier Exchanger Phaser 类支持,它们可以轻松处理以前难以解决的同步问题,适用于各种类型的程序,即使是并发程度有限的程序。下面详细介绍信号量(Semaphore)和倒计时门闩(CountDownLatch)的使用。

4.1 信号量(Semaphore)

信号量是一种经典的同步对象,通过计数器控制对共享资源的访问。如果计数器大于零,则允许访问;如果计数器为零,则拒绝访问。计数器计数的是允许访问共享资源的许可。线程要访问资源,必须从信号量获取许可。

Java的 Semaphore 类实现了这种机制,它有两个构造函数:

Semaphore(int num)
Semaphore(int num, boolean how)

其中, num 指定初始许可计数,即同时可以访问共享资源的线程数量。 how true 时,确保等待的线程按请求访问的顺序获得许可。

要获取许可,调用 acquire() 方法:

void acquire() throws InterruptedException
void acquire(int num) throws InterruptedException

要释放许可,调用 release() 方法:

void release()
void release(int num)

下面是一个简单的信号量使用示例:

// A simple semaphore example.
import java.util.concurrent.*;

class SemDemo {
    public static void main(String args[]) {
        Semaphore sem = new Semaphore(1);
        new IncThread(sem, "A");
        new DecThread(sem, "B");
    }
}

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

// A thread of execution that increments count.
class IncThread implements Runnable {
    String name;
    Semaphore sem;

    IncThread(Semaphore s, String n) {
        sem = s;
        name = n;
        new Thread(this).start();
    }

    public void run() {
        System.out.println("Starting " + name);
        try {
            // First, get a permit.
            System.out.println(name + " is waiting for a permit.");
            sem.acquire();
            System.out.println(name + " gets a permit.");
            // Now, access shared resource.
            for(int i=0; i < 5; i++) {
                Shared.count++;
                System.out.println(name + ": " + Shared.count);
                // Now, allow a context switch -- if possible.
                Thread.sleep(10);
            }
        } catch (InterruptedException exc) {
            System.out.println(exc);
        }
        // Release the permit.
        System.out.println(name + " releases the permit.");
        sem.release();
    }
}

// A thread of execution that decrements count.
class DecThread implements Runnable {
    String name;
    Semaphore sem;

    DecThread(Semaphore s, String n) {
        sem = s;
        name = n;
        new Thread(this).start();
    }

    public void run() {
        System.out.println("Starting " + name);
        try {
            // First, get a permit.
            System.out.println(name + " is waiting for a permit.");
            sem.acquire();
            System.out.println(name + " gets a permit.");
            // Now, access shared resource.
            for(int i=0; i < 5; i++) {
                Shared.count--;
                System.out.println(name + ": " + Shared.count);
                // Now, allow a context switch -- if possible.
                Thread.sleep(10);
            }
        } catch (InterruptedException exc) {
            System.out.println(exc);
        }
        // Release the permit.
        System.out.println(name + " releases the permit.");
        sem.release();
    }
}

该程序使用信号量控制对 Shared 类中静态变量 count 的访问,确保每次只有一个线程可以访问 count

信号量还可以用于更复杂的场景,例如生产者 - 消费者问题:

// An implementation of a producer and consumer
// that use semaphores to control synchronization.
import java.util.concurrent.Semaphore;

class Q {
    int n;
    // Start with consumer semaphore unavailable.
    static Semaphore semCon = new Semaphore(0);
    static Semaphore semProd = new Semaphore(1);

    void get() {
        try {
            semCon.acquire();
        } catch(InterruptedException e) {
            System.out.println("InterruptedException caught");
        }
        System.out.println("Got: " + n);
        semProd.release();
    }

    void put(int n) {
        try {
            semProd.acquire();
        } catch(InterruptedException e) {
            System.out.println("InterruptedException caught");
        }
        this.n = n;
        System.out.println("Put: " + n);
        semCon.release();
    }
}

class Producer implements Runnable {
    Q q;

    Producer(Q q) {
        this.q = q;
        new Thread(this, "Producer").start();
    }

    public void run() {
        for(int i=0; i < 20; i++) q.put(i);
    }
}

class Consumer implements Runnable {
    Q q;

    Consumer(Q q) {
        this.q = q;
        new Thread(this, "Consumer").start();
    }

    public void run() {
        for(int i=0; i < 20; i++) q.get();
    }
}

class ProdCon {
    public static void main(String args[]) {
        Q q = new Q();
        new Consumer(q);
        new Producer(q);
    }
}

在这个例子中,使用两个信号量 semProd semCon 确保每次 put() 调用后都会有对应的 get() 调用,避免了数据丢失。

5. 倒计时门闩(CountDownLatch)

有时候,我们希望一个线程等待一个或多个事件发生后再继续执行。 CountDownLatch 可以满足这种需求,它在创建时指定一个事件计数,每次事件发生时计数减一,当计数达到零时,门闩打开。

CountDownLatch 有一个构造函数:

CountDownLatch(int num)

其中, num 指定门闩打开所需发生的事件数量。

要等待门闩打开,调用 await() 方法:

void await() throws InterruptedException
boolean await(long wait, TimeUnit tu) throws InterruptedException

要信号事件发生,调用 countDown() 方法:

void countDown()

下面是一个倒计时门闩的使用示例:

// An example of CountDownLatch.
import java.util.concurrent.CountDownLatch;

class CDLDemo {
    public static void main(String args[]) {
        CountDownLatch cdl = new CountDownLatch(5);
        System.out.println("Starting");
        new MyThread(cdl);
        try {
            cdl.await();
        } catch (InterruptedException exc) {
            System.out.println(exc);
        }
        System.out.println("Done");
    }
}

class MyThread implements Runnable {
    CountDownLatch latch;

    MyThread(CountDownLatch c) {
        latch = c;
        new Thread(this).start();
    }

    public void run() {
        for(int i = 0; i<5; i++) {
            System.out.println(i);
            latch.countDown(); // decrement count
        }
    }
}

在这个例子中,主线程等待 MyThread 线程执行5次事件后再继续执行。

通过使用信号量和倒计时门闩等同步对象,我们可以更方便地处理多线程同步问题,提高程序的并发性能和稳定性。

下面是信号量和倒计时门闩的使用流程对比表格:
| 同步对象 | 构造函数 | 获取操作 | 释放/信号操作 | 使用场景 |
| — | — | — | — | — |
| 信号量(Semaphore) | Semaphore(int num)
Semaphore(int num, boolean how) | acquire()
acquire(int num) | release()
release(int num) | 控制对共享资源的访问,协调生产者 - 消费者问题等 |
| 倒计时门闩(CountDownLatch) | CountDownLatch(int num) | await()
await(long wait, TimeUnit tu) | countDown() | 让线程等待一个或多个事件发生 |

信号量使用的流程可以用mermaid流程图表示:

graph TD;
    A[线程请求访问资源] --> B{信号量计数器 > 0?};
    B -- 是 --> C[获取许可,计数器减1];
    B -- 否 --> D[线程阻塞等待];
    C --> E[访问共享资源];
    E --> F[释放许可,计数器加1];
    F --> G{有等待线程?};
    G -- 是 --> H[等待线程获取许可];
    G -- 否 --> I[无操作];
    D --> B;

倒计时门闩使用的流程可以用mermaid流程图表示:

graph TD;
    A[创建CountDownLatch,设置事件计数] --> B[线程等待门闩打开];
    C[事件发生] --> D[调用countDown(),计数减1];
    D --> E{计数 == 0?};
    E -- 是 --> F[门闩打开,等待线程继续执行];
    E -- 否 --> G[继续等待事件发生];
    B --> E;

综上所述,Java的并发工具包为开发者提供了丰富的功能和强大的支持,能够帮助我们更好地处理多线程和并发编程中的各种问题。无论是简单的同步需求还是复杂的并行计算,都可以通过合理使用这些工具来实现高效、稳定的程序。

Java并发工具包:提升多线程编程效率的利器

6. 循环屏障(CyclicBarrier)

循环屏障( CyclicBarrier )用于让一组线程在某个预定义的执行点等待,直到所有线程都到达该点后,它们才会继续执行。 CyclicBarrier 可以循环使用,即当所有线程通过屏障后,它可以被重置以用于下一轮的同步。

CyclicBarrier 有两个构造函数:

CyclicBarrier(int parties)
CyclicBarrier(int parties, Runnable barrierAction)
  • parties :指定需要等待的线程数量。
  • barrierAction :一个可选的 Runnable 对象,当所有线程到达屏障时,该对象的 run() 方法会被执行。

要让线程在屏障处等待,调用 await() 方法:

int await() throws InterruptedException, BrokenBarrierException
int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException

第一个方法会一直等待,直到所有线程都到达屏障;第二个方法可以指定等待的时间。

下面是一个循环屏障的使用示例:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

class BarrierDemo {
    public static void main(String args[]) {
        CyclicBarrier cb = new CyclicBarrier(3, new BarAction());

        new MyThread(cb, "A");
        new MyThread(cb, "B");
        new MyThread(cb, "C");
    }
}

class BarAction implements Runnable {
    public void run() {
        System.out.println("Barrier reached!");
    }
}

class MyThread implements Runnable {
    CyclicBarrier cbar;
    String name;

    MyThread(CyclicBarrier c, String n) {
        cbar = c;
        name = n;
        new Thread(this).start();
    }

    public void run() {
        System.out.println(name);
        try {
            cbar.await();
        } catch (InterruptedException | BrokenBarrierException e) {
            System.out.println(e);
        }
    }
}

在这个例子中,三个线程会在 CyclicBarrier 处等待,当所有线程都到达后, BarAction run() 方法会被执行,输出 Barrier reached!

7. 交换器(Exchanger)

交换器( Exchanger )用于在两个线程之间交换数据。当两个线程都到达交换点时,它们会交换彼此的数据。

Exchanger 有一个无参构造函数:

Exchanger<V>()

要进行数据交换,调用 exchange() 方法:

V exchange(V x) throws InterruptedException
V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException

第一个方法会一直等待另一个线程到达交换点;第二个方法可以指定等待的时间。

下面是一个交换器的使用示例:

import java.util.concurrent.Exchanger;

class ExchDemo {
    public static void main(String args[]) {
        Exchanger<String> ex = new Exchanger<>();

        new UseString(ex);
        new MakeString(ex);
    }
}

class MakeString implements Runnable {
    Exchanger<String> ex;
    String str;

    MakeString(Exchanger<String> e) {
        ex = e;
        str = "";
        new Thread(this).start();
    }

    public void run() {
        char ch = 'A';
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 5; j++)
                str += ch++;
            try {
                str = ex.exchange(str);
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
    }
}

class UseString implements Runnable {
    Exchanger<String> ex;
    String str;

    UseString(Exchanger<String> e) {
        ex = e;
        new Thread(this).start();
    }

    public void run() {
        for (int i = 0; i < 3; i++) {
            try {
                str = ex.exchange("");
                System.out.println("Got: " + str);
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
    }
}

在这个例子中, MakeString 线程生成字符串, UseString 线程接收并打印字符串,两个线程通过 Exchanger 进行数据交换。

8. 移相器(Phaser)

移相器( Phaser )用于同步多个线程在多个操作阶段的执行。线程可以在不同的阶段进行同步,每个阶段都有一个序号,线程可以注册到 Phaser 中,并在每个阶段结束时进行同步。

Phaser 有几个构造函数:

Phaser()
Phaser(int parties)
Phaser(Phaser parent)
Phaser(Phaser parent, int parties)
  • parties :指定初始注册的线程数量。
  • parent :指定父 Phaser

要注册线程,调用 register() 方法;要到达某个阶段并等待其他线程,调用 arriveAndAwaitAdvance() 方法;要到达某个阶段并注销,调用 arriveAndDeregister() 方法。

下面是一个移相器的使用示例:

import java.util.concurrent.Phaser;

class PhaserDemo {
    public static void main(String args[]) {
        Phaser phsr = new Phaser(1);
        int curPhase;

        System.out.println("Starting");

        new MyThread(phsr, "A");
        new MyThread(phsr, "B");
        new MyThread(phsr, "C");

        // 主线程是第一个参与者
        curPhase = phsr.getPhase();
        phsr.arriveAndAwaitAdvance();
        System.out.println("Phase " + curPhase + " completed");

        curPhase = phsr.getPhase();
        phsr.arriveAndAwaitAdvance();
        System.out.println("Phase " + curPhase + " completed");

        curPhase = phsr.getPhase();
        phsr.arriveAndAwaitAdvance();
        System.out.println("Phase " + curPhase + " completed");

        // 注销主线程
        phsr.arriveAndDeregister();

        if (phsr.isTerminated())
            System.out.println("The phaser is terminated");
    }
}

class MyThread implements Runnable {
    Phaser phsr;
    String name;

    MyThread(Phaser p, String n) {
        phsr = p;
        name = n;
        phsr.register();
        new Thread(this).start();
    }

    public void run() {
        while (!phsr.isTerminated()) {
            System.out.println("Thread " + name + " beginning phase " + phsr.getPhase());
            phsr.arriveAndAwaitAdvance();
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
    }
}

在这个例子中,主线程和三个子线程通过 Phaser 在多个阶段进行同步,每个阶段完成后会输出相应的信息。

8. 并发工具包的综合应用

在实际开发中,我们可以根据具体的需求组合使用这些同步对象和并发工具。例如,在一个复杂的多线程任务中,可以使用 CountDownLatch 确保所有初始化任务完成后再开始主任务,使用 Semaphore 控制对有限资源的访问,使用 CyclicBarrier 让多个线程在某个关键步骤同步,使用 Exchanger 进行线程间的数据交换,使用 Phaser 协调多个阶段的任务执行。

下面是一个综合应用的示例,模拟一个多线程的文件处理任务:

import java.util.concurrent.*;

class FileProcessingDemo {
    public static void main(String args[]) {
        // 初始化CountDownLatch,确保所有文件读取线程完成
        CountDownLatch latch = new CountDownLatch(3);

        // 初始化Semaphore,控制对文件处理资源的访问
        Semaphore sem = new Semaphore(2);

        // 初始化CyclicBarrier,让所有文件处理线程在某个步骤同步
        CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All threads reached the barrier!"));

        // 初始化Exchanger,用于线程间数据交换
        Exchanger<String> exchanger = new Exchanger<>();

        // 初始化Phaser,协调多个阶段的任务执行
        Phaser phaser = new Phaser(3);

        new FileReader(latch, sem, barrier, exchanger, phaser, "File1");
        new FileReader(latch, sem, barrier, exchanger, phaser, "File2");
        new FileReader(latch, sem, barrier, exchanger, phaser, "File3");

        try {
            // 等待所有文件读取线程完成
            latch.await();
            System.out.println("All files are read!");
        } catch (InterruptedException e) {
            System.out.println(e);
        }
    }
}

class FileReader implements Runnable {
    CountDownLatch latch;
    Semaphore sem;
    CyclicBarrier barrier;
    Exchanger<String> exchanger;
    Phaser phaser;
    String fileName;

    FileReader(CountDownLatch l, Semaphore s, CyclicBarrier b, Exchanger<String> e, Phaser p, String f) {
        latch = l;
        sem = s;
        barrier = b;
        exchanger = e;
        phaser = p;
        fileName = f;
        new Thread(this).start();
    }

    public void run() {
        try {
            // 获取文件处理资源
            sem.acquire();
            System.out.println("Reading " + fileName);

            // 模拟文件读取
            Thread.sleep(100);

            // 释放文件处理资源
            sem.release();

            // 到达CyclicBarrier等待其他线程
            barrier.await();

            // 进行数据交换
            String data = exchanger.exchange("Data from " + fileName);
            System.out.println("Received data: " + data);

            // 到达Phaser的某个阶段并等待其他线程
            phaser.arriveAndAwaitAdvance();

            // 完成任务,通知CountDownLatch
            latch.countDown();
        } catch (InterruptedException | BrokenBarrierException e) {
            System.out.println(e);
        }
    }
}

在这个例子中,我们使用了 CountDownLatch Semaphore CyclicBarrier Exchanger Phaser 来协调多个文件处理线程的执行,确保任务的正确和高效完成。

9. 总结

Java的并发工具包为多线程编程提供了丰富的功能和强大的支持。通过合理使用同步对象(如 Semaphore CountDownLatch CyclicBarrier Exchanger Phaser )、执行器、并发集合和 Fork/Join 框架等工具,开发者可以更好地处理多线程同步和并发问题,提高程序的性能和稳定性。

在实际应用中,我们需要根据具体的需求选择合适的工具,并注意线程安全和资源管理。同时,随着多核系统的普及, Fork/Join 框架等并行编程工具将发挥越来越重要的作用,帮助我们充分利用多核处理器的性能。

以下是Java并发工具包中部分类的特点和适用场景总结表格:
| 类名 | 特点 | 适用场景 |
| — | — | — |
| Semaphore | 通过计数器控制对共享资源的访问 | 控制对有限资源的并发访问,如数据库连接池 |
| CountDownLatch | 等待一个或多个事件发生 | 确保所有初始化任务完成后再开始主任务 |
| CyclicBarrier | 让一组线程在预定义的执行点同步 | 多个线程在某个关键步骤需要同步执行 |
| Exchanger | 在两个线程之间交换数据 | 线程间的数据交换,如生产者 - 消费者场景中的数据传递 |
| Phaser | 协调多个阶段的任务执行 | 复杂的多阶段多线程任务,如游戏中的多阶段关卡 |

总之,掌握Java并发工具包的使用,将有助于开发者编写更加高效、稳定和可扩展的多线程程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值