Java 并发
指的是在 Java 程序中处理多个任务或线程的能力。Java 提供了丰富的并发编程工具和框架,使得开发者能够更轻松地编写多线程应用程序。以下是 Java 中常用的并发编程技术和工具:
-
线程:Java 通过
java.lang.Thread
类来表示线程。开发者可以通过创建Thread
对象并调用其start()
方法来创建和启动新线程。 -
Runnable 接口:
java.lang.Runnable
接口定义了一个线程可以执行的任务。开发者可以通过实现Runnable
接口,并将其作为参数传递给Thread
对象,从而指定线程的执行任务。 -
Callable 和 Future:
java.util.concurrent.Callable
接口类似于Runnable
,但它可以返回一个结果,并且可以抛出受检查的异常。java.util.concurrent.Future
接口表示异步计算的结果。开发者可以使用Callable
和Future
来实现异步任务的执行和结果获取。 -
Executor 框架:
java.util.concurrent.Executor
接口定义了一种执行线程任务的方式。java.util.concurrent.Executors
类提供了一组工厂方法来创建不同类型的线程池,如单线程池、固定大小线程池、可缓存线程池等。 -
同步机制:Java 提供了多种同步机制来确保多线程访问共享资源的安全性,如 synchronized 关键字、Lock 接口及其实现类、volatile 关键字等。
-
并发集合:Java 提供了一些线程安全的集合类,如
java.util.concurrent.ConcurrentHashMap
、java.util.concurrent.CopyOnWriteArrayList
等,用于在多线程环境下安全地操作集合。 -
同步器:Java 并发包中提供了一些同步器来帮助管理线程的同步,如
java.util.concurrent.CountDownLatch
、java.util.concurrent.CyclicBarrier
、java.util.concurrent.Semaphore
等。 -
并发工具类:Java 并发包中提供了一些用于处理常见并发问题的工具类,如
java.util.concurrent.atomic
包中的原子类、java.util.concurrent.locks
包中的锁机制等。 -
并发编程模型:Java 并发包中提供了一些用于编写并发程序的高级编程模型,如 Fork/Join 框架、CompletableFuture、并行流等。
以上是 Java 中常用的并发编程技术和工具,开发者可以根据具体需求选择合适的并发模型来实现多线程应用程序。
线程
是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。在 Java 中,线程是程序中的执行流程,可以独立执行任务。
Java 中的线程由 java.lang.Thread
类表示,线程的创建和启动通常有两种方式:
-
继承 Thread 类:创建一个继承自 Thread 类的子类,并重写其 run() 方法来定义线程的执行任务。然后可以通过调用子类的 start() 方法来启动线程。
public class MyThread extends Thread { public void run() { // 线程执行的任务 } } public class Main { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // 启动线程 } }
-
实现 Runnable 接口:创建一个实现了 Runnable 接口的类,并实现其 run() 方法。然后创建 Thread 对象,将实现了 Runnable 接口的对象作为参数传递给 Thread 对象,并调用 Thread 对象的 start() 方法来启动线程。
public class MyRunnable implements Runnable { public void run() { // 线程执行的任务 } } public class Main { public static void main(String[] args) { MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); // 启动线程 } }
Java 线程的生命周期包括新建状态、就绪状态、运行状态、阻塞状态和死亡状态。开发者可以通过调用 Thread 类的方法来控制线程的状态和行为,例如:
- start()
方法用于启动线程。
- sleep(long millis)
方法使当前线程睡眠指定的时间。
- join()
方法等待线程终止。
- yield()
方法让出 CPU 执行权,让其他线程运行。
- interrupt()
方法中断线程的执行。
下面是对 join()
、yield()
和 interrupt()
方法的简单示例:
join()
方法示例:
public class JoinExample {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Thread 1: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
try {
thread1.join(); // 等待 thread1 执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5; i++) {
System.out.println("Thread 2: " + i);
}
});
thread1.start();
thread2.start();
}
}
yield()
方法示例:
public class YieldExample {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Thread 1: " + i);
Thread.yield(); // 让出 CPU 执行权
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Thread 2: " + i);
Thread.yield(); // 让出 CPU 执行权
}
});
thread1.start();
thread2.start();
}
}
interrupt()
方法示例:
public class InterruptExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(5000); // 线程睡眠 5 秒钟
} catch (InterruptedException e) {
System.out.println("Thread interrupted!");
}
});
thread.start(); // 启动线程
thread.interrupt(); // 中断线程
}
}
这些示例展示了 join()
、yield()
和 interrupt()
方法的基本用法,可以根据需要进行调整和扩展。
Callable 和 Future
是 Java 中用于支持异步执行任务的接口和类。
- Callable:
Callable
是一个泛型接口,它类似于Runnable
,但是可以返回一个结果并且可以抛出一个受检异常。Callable
接口定义了一个方法call()
,该方法可以在其中执行具体的任务,并返回一个结果。- 通常,
Callable
接口与ExecutorService
结合使用,以便将任务提交给线程池执行。
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(3000);
return "Task completed";
}
}
- Future:
Future
表示一个异步计算的结果,它提供了方法来检查计算是否已经完成,以等待计算的完成,并检索计算的结果。Future
接口提供了一个get()
方法,该方法可以阻塞当前线程,直到计算完成并返回结果。
import java.util.concurrent.*;
public class FutureExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<String> callable = new MyCallable();
Future<String> future = executor.submit(callable);
System.out.println("Waiting for the result...");
String result = future.get();
System.out.println("Result: " + result);
executor.shutdown();
}
}
在这个示例中,MyCallable
类实现了 Callable
接口,在 call()
方法中执行了一个简单的任务,休眠了 3 秒钟,然后返回一个字符串。然后,通过 executor.submit()
方法将 Callable
对象提交给线程池执行,返回一个 Future
对象。Future.get()
方法用于获取异步任务的结果,如果任务还未完成,则会阻塞当前线程直到任务完成。
总的来说,Callable
和 Future
接口是 Java 中用于异步执行任务和获取任务结果的重要组件。
`
Executor(线程池)
是 Java 并发编程中的一个接口,用于执行提交的任务。它提供了一种将任务提交与任务的执行分离的机制,从而更有效地管理线程,并提高应用程序的性能和可扩展性。
Executor
接口定义了一个单一方法 execute()
,该方法用于执行给定的任务。具体的任务执行策略由实现 Executor
接口的类确定,例如 ThreadPoolExecutor
和 ScheduledThreadPoolExecutor
。
使用 Executor
接口,可以将任务的执行细节与任务提交者分离开来,从而更容易地管理线程资源,并提高系统的可维护性和性能。
其原理主要包括以下几个方面:
-
线程池的创建:在创建线程池时,会初始化一定数量的线程,这些线程处于等待状态,随时准备执行任务。
-
任务提交:当有任务需要执行时,可以将任务提交给线程池。线程池会从线程池的工作队列中选择一个空闲线程来执行任务。
-
任务执行:线程池会管理线程的执行过程,确保任务按照指定的方式执行。一般来说,线程池会采用先进先出的策略执行任务。
-
线程复用:执行完任务后,线程并不会被销毁,而是被放回线程池中,以便下次再次利用。这样可以避免频繁创建和销毁线程所带来的性能开销。
-
线程管理:线程池会动态管理线程的数量,根据任务的数量和系统负载情况来调整线程池中线程的数量,以保证系统的稳定性和性能。
通过合理配置线程池的大小、队列容量和线程池的策略,可以有效地管理和调度线程,提高系统的性能和吞吐量。
Executor 的几种实现
当涉及到并发编程时,Java提供了许多不同的Executor实现,每种实现都有其独特的特点和用途。下面是几种常见的Executor实现以及它们之间的比较:
-
ThreadPoolExecutor:
- 特点:
ThreadPoolExecutor
是一个灵活的线程池实现,它允许你在应用程序中管理线程的生命周期。你可以指定线程池的大小、任务队列的容量以及拒绝策略等参数。 - 示例:
ExecutorService executor = Executors.newFixedThreadPool(5); // 创建一个固定大小的线程池 executor.execute(new MyTask()); // 提交任务给线程池执行 executor.shutdown(); // 关闭线程池
- 适用场景:适用于需要管理线程生命周期、控制资源消耗和提高性能的场景。
- 特点:
-
ScheduledThreadPoolExecutor:
- 特点:
ScheduledThreadPoolExecutor
是ThreadPoolExecutor
的一个子类,它可以在指定的时间间隔内执行任务,或者延迟一段时间后执行任务。 - 示例:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); // 创建一个大小为2的定时任务线程池 executor.schedule(new MyTask(), 1, TimeUnit.SECONDS); // 延迟1秒后执行任务 executor.scheduleAtFixedRate(new MyTask(), 0, 1, TimeUnit.SECONDS); // 每隔1秒执行一次任务
- 适用场景:适用于需要定时执行任务或者延迟执行任务的场景。
- 特点:
-
ForkJoinPool:
- 特点:
ForkJoinPool
是一个专门用于执行分解并行任务的线程池实现。它通过工作窃取算法(work-stealing)来实现任务的负载均衡,可以在处理大规模数据时提供更好的性能。 - 示例:
ForkJoinPool pool = new ForkJoinPool(); MyRecursiveTask task = new MyRecursiveTask(); Integer result = pool.invoke(task); // 同步执行任务并获取结果
- 适用场景:适用于处理递归任务、分治算法以及其他需要并行处理大规模数据的场景。
- 特点:
-
SingleThreadExecutor:
- 特点:
SingleThreadExecutor
是一个单线程的线程池实现,它只会使用一个工作线程来执行任务。它通常用于需要顺序执行任务、保持任务顺序或限制并发性的场景。 - 示例:
ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(new MyTask());
- 适用场景:适用于需要按顺序执行任务或者限制并发性的场景。
- 特点:
-
CachedThreadPool:
- 特点:
CachedThreadPool
是一个根据需要创建新线程的线程池实现。如果有空闲线程可用,则会重用这些线程来执行新任务;如果没有空闲线程,则会创建一个新线程。适用于执行大量短期异步任务的场景。 - 示例:
ExecutorService executor = Executors.newCachedThreadPool(); executor.execute(new MyTask());
- 适用场景:适用于需要灵活管理线程数量、处理大量短期异步任务的场景。
- 特点:
总的来说,选择合适的Executor实现取决于你的具体需求和应用场景。
ThreadPoolExecutor是一个通用的、灵活的选择;
ScheduledThreadPoolExecutor用于定时执行任务;
ForkJoinPool用于处理并行任务;
SingleThreadExecutor用于按顺序执行任务;
CachedThreadPool用于处理大量短期异步任务。
同步机制
是多线程编程中用于控制多个线程对共享资源的访问的一种手段,目的是确保在任何时候,最多只有一个线程可以访问共享资源,从而避免竞态条件(race condition)和数据不一致性等问题。在Java中,主要有以下几种同步机制:
- 关键字 synchronized:
- 使用
synchronized
关键字可以修饰方法或代码块,实现对对象的同步访问。当线程进入 synchronized 方法或代码块时,会自动获取对象的锁,其他线程必须等待锁释放后才能执行该方法或代码块。 - 例:当多个线程需要访问共享资源时,可以使用
synchronized
关键字来确保线程安全。以下是一个示例,展示了如何使用synchronized
来控制对共享资源的访问:
- 使用
public class SynchronizedExample {
private int count = 0;
// 使用 synchronized 关键字修饰方法,确保线程安全
public synchronized void increment() {
count++;
}
// 使用 synchronized 关键字修饰代码块,确保线程安全
public void synchronizedMethod() {
synchronized (this) {
// 同步的代码块
count++;
}
}
public int getCount() {
return count;
}
public static void main(String[] args) {
final SynchronizedExample example = new SynchronizedExample();
// 创建多个线程并启动
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
// 等待线程执行完成
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出共享资源的值
System.out.println("Count: " + example.getCount()); // 期望值:2000
}
}
在这个示例中,我们有一个共享资源 count
,它被多个线程同时访问。通过在 increment()
方法和 synchronizedMethod()
方法中使用 synchronized
关键字,确保了对 count
的操作是线程安全的。这样,即使多个线程同时访问 count
,也不会出现数据不一致的情况。
- ReentrantLock:
ReentrantLock
是 Java 中的一个显示锁实现,它提供了比 synchronized 更灵活的锁定机制。与 synchronized 不同,ReentrantLock 可以实现公平性策略和中断响应等功能。- 下面是一个使用
ReentrantLock
的示例,展示了如何在具体场景中应用它:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
// 获取锁
lock.lock();
try {
count++;
} finally {
// 释放锁
lock.unlock();
}
}
public int getCount() {
return count;
}
public static void main(String[] args) {
final ReentrantLockExample example = new ReentrantLockExample();
// 创建多个线程并启动
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
// 等待线程执行完成
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出共享资源的值
System.out.println("Count: " + example.getCount()); // 期望值:2000
}
}
在这个示例中,我们使用 ReentrantLock
来保护共享资源 count
。通过调用 lock()
方法获取锁,在使用完共享资源后调用 unlock()
方法释放锁。这样可以确保在同一时刻只有一个线程可以访问 count
,从而避免了多线程并发访问造成的数据不一致问题。
- Condition:
Condition
是与ReentrantLock
结合使用的一种条件等待机制,用于实现更复杂的线程间通信和协调。。它可以让线程在等待某个条件成立时暂时释放锁,并在条件满足时重新获取锁并继续执行。- 下面是一个使用
Condition
的示例,展示了如何在具体场景中应用它:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
private Queue<Integer> buffer = new LinkedList<>();
private int capacity = 10;
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
public void produce(int value) throws InterruptedException {
lock.lock();
try {
while (buffer.size() == capacity) {
// 缓冲区已满,等待消费者消费
notFull.await();
}
buffer.offer(value);
System.out.println("Produced: " + value);
// 通知消费者可以消费了
notEmpty.signal();
} finally {
lock.unlock();
}
}
public int consume() throws InterruptedException {
lock.lock();
try {
while (buffer.isEmpty()) {
// 缓冲区为空,等待生产者生产
notEmpty.await();
}
int value = buffer.poll();
System.out.println("Consumed: " + value);
// 通知生产者可以生产了
notFull.signal();
return value;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final ConditionExample example = new ConditionExample();
Thread producer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
example.produce(i);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread consumer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
example.consume();
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
consumer.start();
try {
producer.join();
consumer.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在这个示例中,我们使用 Condition
实现了一个简单的生产者-消费者模型。生产者在缓冲区未满时生产数据,并通知消费者可以消费;消费者在缓冲区非空时消费数据,并通知生产者可以继续生产。这样可以确保在合适的时机生产者和消费者线程间进行通信和协调,避免了生产者过度生产或消费者过度消费的问题。
- volatile 关键字:
volatile
关键字用于修饰变量,保证了变量的可见性,并且禁止指令重排序优化。当一个变量被 volatile 修饰时,对该变量的读写操作都会直接在主存中进行,而不会被缓存在线程的工作内存中。- volatile 当一个线程修改了该变量的值时,其他线程能够立即看到最新的值。
- 下面是一个简单的示例,展示了如何在具体场景中应用 volatile:
public class VolatileExample {
private volatile boolean flag = false;
public void toggleFlag() {
flag = !flag;
}
public boolean isFlag() {
return flag;
}
public static void main(String[] args) {
VolatileExample example = new VolatileExample();
Thread writer = new Thread(() -> {
while (true) {
example.toggleFlag();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread reader = new Thread(() -> {
while (true) {
if (example.isFlag()) {
System.out.println("Flag is true");
}
}
});
writer.start();
reader.start();
}
}
在这个示例中,flag 变量被声明为 volatile,这样在一个线程修改了 flag 的值后,另一个线程能够立即看到最新的值。reader 线程不断地检查 flag 是否为 true,并在 flag 变为 true 时打印消息。
- Atomic 类:
java.util.concurrent.atomic
包提供了一系列原子操作类,如AtomicInteger
、AtomicBoolean
等,它们提供了一种线程安全的方式来进行原子操作,避免了使用锁的开销。- 这些原子类提供了比 volatile 更强的保证,可以确保多个线程对共享变量的原子性操作。
- 下面是一个使用 AtomicInteger 的示例:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
public static void main(String[] args) {
AtomicExample example = new AtomicExample();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
example.increment();
}
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Count: " + example.getCount()); // 应该输出 10000
}
}
在这个示例中,我们创建了一个 AtomicInteger 实例 count,并在多个线程中对其进行增加操作。由于 AtomicInteger 提供了原子性的增加操作,所以不需要额外的同步措施来保证线程安全。
这些同步机制各有特点,选择合适的同步机制取决于具体的应用场景和需求。
同步器(synchronizer)
是多线程编程中的一个重要概念,用于协调多个线程之间的操作,确保线程之间的同步和互斥。Java 中的同步器通常是一些类或接口,用于控制多线程访问共享资源的方式。
常见的同步器包括 synchronized
关键字、ReentrantLock
、Semaphore
、CountDownLatch
、CyclicBarrier
等。下面简要介绍这些同步器的特点和使用场景:
-
synchronized 关键字:
synchronized
是 Java 中最基本的同步机制,可以用于实现线程之间的互斥和同步。- 适用于简单的同步需求,例如对共享资源的读写操作。
-
ReentrantLock:
ReentrantLock
是Lock
接口的一个实现,提供了比synchronized
更灵活的锁操作。- 可以实现可重入性、公平性等特性,并提供了更丰富的条件等待机制。
- 适用于需要更精细控制的同步场景,例如实现读写锁、公平锁等。
- 读写锁(ReadWriteLock)和公平锁(Fair Lock),可以使用
ReentrantReadWriteLock
和ReentrantLock
类来实现。
- 读写锁(ReadWriteLock):
import java.util.concurrent.locks.*;
public class ReadWriteLockExample {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
private int data;
public int readData() {
readLock.lock();
try {
return data;
} finally {
readLock.unlock();
}
}
public void writeData(int newData) {
writeLock.lock();
try {
data = newData;
} finally {
writeLock.unlock();
}
}
}
在上面的示例中,ReadWriteLockExample
类实现了一个简单的读写数据的功能,使用 ReentrantReadWriteLock
来保护数据的读写操作。通过调用 readLock()
和 writeLock()
方法获取读锁和写锁,从而实现多个线程之间的读共享和写互斥。
- 公平锁(Fair Lock):
import java.util.concurrent.locks.*;
public class FairLockExample {
private final Lock fairLock = new ReentrantLock(true); // 使用公平锁
public void doSomething() {
fairLock.lock();
try {
// 执行需要同步的操作
} finally {
fairLock.unlock();
}
}
}
在上面的示例中,FairLockExample
类使用 ReentrantLock
的构造方法传入 true
来创建一个公平锁。公平锁会尽量按照线程的请求顺序分配锁,保证了线程的公平性。
使用这些实现,可以在多线程环境中安全地实现读写锁和公平锁的功能,确保线程间的同步和互斥操作。
- Semaphore:
Semaphore
是一个计数信号量,用于控制同时访问某个共享资源的线程数量。- 可以指定信号量的初始许可数量,并在需要时进行申请和释放许可。
- 适用于控制并发线程数量的场景,例如资源池管理、流量控制等。
- 下面是一个 Semaphore 在具体场景中的应用示例:
假设有一个资源池,里面存放了一定数量的资源,多个线程需要从资源池中获取资源并进行处理,但是资源池中的资源是有限的,因此需要使用 Semaphore 来控制对资源的访问。
import java.util.concurrent.*;
public class SemaphoreExample {
private static final int MAX_AVAILABLE_RESOURCES = 5; // 资源池中资源的最大数量
private static final Semaphore semaphore = new Semaphore(MAX_AVAILABLE_RESOURCES, true); // 使用公平的信号量
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executor.execute(new ResourceUser(i)); // 创建多个线程来使用资源
}
executor.shutdown();
}
static class ResourceUser implements Runnable {
private final int userId;
public ResourceUser(int userId) {
this.userId = userId;
}
@Override
public void run() {
try {
System.out.println("User " + userId + " is trying to acquire resource...");
semaphore.acquire(); // 获取资源
System.out.println("User " + userId + " has acquired resource.");
// 模拟用户使用资源的过程
Thread.sleep((long) (Math.random() * 1000));
System.out.println("User " + userId + " is releasing resource...");
semaphore.release(); // 释放资源
System.out.println("User " + userId + " has released resource.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在上面的示例中,SemaphoreExample
类模拟了多个用户(线程)从资源池中获取资源的过程。通过调用 acquire()
和 release()
方法来获取和释放资源,Semaphore 会根据可用资源的数量来控制用户的访问,当资源不足时,用户会被阻塞直到有可用资源为止。
- CountDownLatch (倒计数门闩):
CountDownLatch
是一个倒计数器,用于等待一组线程执行完毕后再执行其他操作。- 可以指定初始计数值,并在每个线程执行完毕时递减计数。
- 适用于等待多个线程完成后再进行后续操作的场景,例如多线程任务协同处理。
- 下面是一个 CountDownLatch 在具体场景中的应用示例:
假设有一个任务,需要等待多个子任务全部完成后才能执行,这时就可以使用 CountDownLatch 来实现等待子任务完成的功能。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchExample {
private static final int NUM_SUB_TASKS = 5; // 子任务的数量
private static final CountDownLatch latch = new CountDownLatch(NUM_SUB_TASKS); // 初始化倒计数门闩
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(NUM_SUB_TASKS);
// 提交多个子任务
for (int i = 0; i < NUM_SUB_TASKS; i++) {
executor.execute(new SubTask(i));
}
// 等待所有子任务完成
latch.await();
System.out.println("All sub tasks completed. Main task can proceed.");
executor.shutdown();
}
static class SubTask implements Runnable {
private final int taskId;
public SubTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
try {
// 模拟子任务执行的过程
System.out.println("Sub task " + taskId + " is running...");
Thread.sleep((long) (Math.random() * 2000));
System.out.println("Sub task " + taskId + " has completed.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 子任务完成后调用 countDown() 方法,表示计数减少一个
}
}
}
}
在上面的示例中,CountDownLatchExample
类模拟了一个主任务等待多个子任务完成的过程。每个子任务执行完毕后会调用 countDown()
方法,表示倒计数门闩的计数减少一个。主任务调用 await()
方法来等待倒计数门闩的计数归零,一旦所有子任务都执行完毕,主任务就可以继续执行。
- CyclicBarrier(循环屏障):
CyclicBarrier
是一个循环栅栏,用于等待一组线程达到某个同步点后再同时执行。- 可以指定同步点数量,并在每个线程到达同步点后等待其他线程到达。
- 适用于分阶段任务的并发执行,例如分阶段数据计算、多阶段流水线处理等。
- 下面是一个 CyclicBarrier 在具体场景中的应用示例:
假设有一个任务,需要等待多个子任务全部完成后才能执行,并且在所有子任务完成后,需要执行额外的操作。这时就可以使用 CyclicBarrier 来实现等待子任务完成并执行额外操作的功能。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CyclicBarrierExample {
private static final int NUM_SUB_TASKS = 5; // 子任务的数量
private static final CyclicBarrier barrier = new CyclicBarrier(NUM_SUB_TASKS + 1); // 初始化循环屏障,加上主任务本身
public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
ExecutorService executor = Executors.newFixedThreadPool(NUM_SUB_TASKS);
// 提交多个子任务
for (int i = 0; i < NUM_SUB_TASKS; i++) {
executor.execute(new SubTask(i));
}
// 等待所有子任务完成,并执行额外操作
barrier.await();
System.out.println("All sub tasks completed. Main task can proceed with additional operation.");
executor.shutdown();
}
static class SubTask implements Runnable {
private final int taskId;
public SubTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
try {
// 模拟子任务执行的过程
System.out.println("Sub task " + taskId + " is running...");
Thread.sleep((long) (Math.random() * 2000));
System.out.println("Sub task " + taskId + " has completed.");
barrier.await(); // 子任务执行完毕后,等待其他子任务
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
在上面的示例中,CyclicBarrierExample
类模拟了一个主任务等待多个子任务完成并执行额外操作的过程。每个子任务执行完毕后会调用 await()
方法,等待其他子任务完成。当所有子任务都完成后,主任务会执行额外操作。
这些同步器在多线程编程中都有着各自的优势和适用场景,开发者可以根据具体需求选择合适的同步器来实现线程之间的同步和协作。
并发集合
是专为多线程环境设计的数据结构,它们提供了线程安全的操作,并且在并发访问时能够保持一致的状态。(它支持并发访问,可以在多线程环境下安全地进行读写操作,而不需要显式地进行同步)。常见的并发集合包括 ConcurrentHashMap、ConcurrentLinkedQueue、CopyOnWriteArrayList 等。
CopyOnWriteArrayList
是Java中线程安全的List实现之一,它通过在写操作(例如添加、删除、修改)时创建一个新的底层数组来实现线程安全性。这意味着读操作(例如遍历)不需要加锁,并且不会受到写操作的影响,因此适用于读多写少的场景。
CopyOnWriteArrayList的特点包括:
-
线程安全性:CopyOnWriteArrayList是线程安全的,多个线程可以同时进行读操作,而写操作会创建一个新的底层数组,因此不会影响读操作。
-
写时复制:写操作会创建一个底层数组的副本,并在副本上执行写操作,写完后再将副本赋给原来的数组,这样可以避免并发修改异常(ConcurrentModificationException)。
-
可预期的迭代行为:由于读操作不会受到写操作的影响,所以迭代时不会抛出ConcurrentModificationException异常,但是可能会迭代到旧的或者新增的元素。
-
适用于读多写少的场景:CopyOnWriteArrayList适用于读操作频繁、写操作相对较少的场景,因为写操作需要复制整个数组,性能较差。
-
下面是一个简单的示例,演示了如何使用CopyOnWriteArrayList:
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListExample {
public static void main(String[] args) {
// 创建一个CopyOnWriteArrayList实例
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 创建一个写线程
Thread writeThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
list.add("Thread-" + i);
System.out.println("Added: Thread-" + i);
}
});
// 创建一个读线程
Thread readThread = new Thread(() -> {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println("Read: " + iterator.next());
}
});
// 启动线程
writeThread.start();
readThread.start();
try {
// 等待线程结束
writeThread.join();
readThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在这个示例中,我们创建了一个CopyOnWriteArrayList实例,并向列表中添加了几个元素。然后,我们使用迭代器遍历列表,并在遍历过程中添加了一个新的元素。即使在迭代过程中进行了写操作,迭代器仍然可以正常工作,这是因为CopyOnWriteArrayList使用了写时复制的机制来保证线程安全性。
ConcurrentLinkedQueue
是Java中线程安全的队列实现,它使用链表结构实现队列,支持高并发的生产者-消费者模式。ConcurrentLinkedQueue提供了非阻塞的插入和删除操作,可以在多线程环境下安全地进行操作。
ConcurrentLinkedQueue的特点包括:
-
线程安全性:ConcurrentLinkedQueue是线程安全的,多个线程可以同时进行插入和删除操作,而不会导致数据不一致的问题。
-
非阻塞操作:ConcurrentLinkedQueue的插入和删除操作都是非阻塞的,不会因为队列为空或已满而阻塞线程,可以在任何时候进行操作。
-
高并发性能:由于使用了非阻塞操作和一些优化技术,ConcurrentLinkedQueue在高并发场景下性能表现较好,可以提供较高的并发读写能力。
-
无界队列:ConcurrentLinkedQueue是一个无界队列,理论上可以存放无限数量的元素,不会出现队列满的情况。
-
下面是一个使用
ConcurrentLinkedQueue
的简单示例,其中包含两个线程,一个用于生产元素,另一个用于消费元素。
import java.util.concurrent.ConcurrentLinkedQueue;
public class ConcurrentLinkedQueueExample {
public static void main(String[] args) {
ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
// 生产者线程
Thread producerThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
queue.offer(i); // 向队列中添加元素
System.out.println("Produced: " + i);
try {
Thread.sleep(1000); // 模拟生产过程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 消费者线程
Thread consumerThread = new Thread(() -> {
while (!queue.isEmpty()) {
Integer element = queue.poll(); // 从队列中获取并移除元素
if (element != null) {
System.out.println("Consumed: " + element);
}
try {
Thread.sleep(2000); // 模拟消费过程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动线程
producerThread.start();
consumerThread.start();
}
}
在这个示例中,生产者线程负责向ConcurrentLinkedQueue
中添加元素,而消费者线程负责从队列中取出元素。由于ConcurrentLinkedQueue
是线程安全的,因此可以安全地在多个线程之间共享。其他的并发集合类也可以类似地使用。
ConcurrentHashMap
是Java中线程安全的哈希表实现,它支持并发访问,可以在多线程环境下安全地进行读写操作,而不需要显式地进行同步。
ConcurrentHashMap相比于普通的HashMap,具有以下几个特点:
-
线程安全性:ConcurrentHashMap是线程安全的,多个线程可以同时进行读取和写入操作,而不会导致数据不一致的问题。
-
分段锁:ConcurrentHashMap内部使用了分段锁机制,将整个Map分成多个段(Segment),每个段拥有自己的锁,不同段之间的操作互不影响,提高了并发性能。
-
高并发性能:由于使用了分段锁和一些优化技术,ConcurrentHashMap在高并发场景下性能表现较好,可以提供较高的并发读写能力。
-
支持原子性操作:ConcurrentHashMap提供了一些原子性操作,如
putIfAbsent
、remove
等,可以在不需要额外的同步手段的情况下进行安全的操作。 -
使用ConcurrentHashMap通常是在需要在多线程环境下进行并发访问的情况下。
并发工具类
是用于在多线程编程中进行协调和控制的工具集合,它们可以帮助开发人员编写高效且线程安全的并发代码。常见的并发工具类包括 CountDownLatch、CyclicBarrier、Semaphore、Exchanger 等。下面以 CountDownLatch 和 CyclicBarrier 为例,介绍它们的基本用法:
- CountDownLatch(倒计时门闩):
CountDownLatch 是一个同步工具类,它允许一个或多个线程等待其他线程完成操作后再继续执行。它通过一个计数器来实现,该计数器初始化为一个正整数,每当一个线程完成自己的任务时,计数器值减 1,当计数器值为 0 时,所有等待线程被释放。
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
// 创建三个线程,并在每个线程中调用 countDown 方法
Thread thread1 = new Thread(() -> {
System.out.println("Thread 1 completed");
latch.countDown();
});
Thread thread2 = new Thread(() -> {
System.out.println("Thread 2 completed");
latch.countDown();
});
Thread thread3 = new Thread(() -> {
System.out.println("Thread 3 completed");
latch.countDown();
});
// 启动线程
thread1.start();
thread2.start();
thread3.start();
// 等待所有线程完成
latch.await();
System.out.println("All threads completed");
}
}
- CyclicBarrier(循环栅栏):
CyclicBarrier 也是一个同步工具类,它允许一组线程相互等待,直到所有线程都到达某个屏障点后才继续执行。与 CountDownLatch 不同的是,CyclicBarrier 的计数器在每次调用 await 方法时都会递减,当计数器减为 0 时,屏障点被触发,所有等待线程被释放,并可以重复使用。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All threads reached the barrier");
});
// 创建三个线程,每个线程到达屏障点后都会调用 barrier.await() 方法
Thread thread1 = new Thread(() -> {
System.out.println("Thread 1 reached the barrier");
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
System.out.println("Thread 2 reached the barrier");
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
Thread thread3 = new Thread(() -> {
System.out.println("Thread 3 reached the barrier");
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
// 启动线程
thread1.start();
thread2.start();
thread3.start();
}
}
在上面的示例中,我们使用了 CountDownLatch 和 CyclicBarrier 分别实现了多个线程的同步等待。这些并发工具类能够帮助我们更好地控制线程之间的协作和同步。
同步机制、同步器、并发工具类之间的关系
同步机制、同步器和并发工具类是在多线程编程中用于协调和控制线程执行的重要概念,它们之间存在着一定的关系:
-
同步机制:同步机制是指在多线程环境下,通过控制多个线程之间的访问顺序,以及对共享资源的访问权限,来确保线程安全的一种机制。常见的同步机制包括 synchronized 关键字、Lock 接口及其实现类(如 ReentrantLock)、volatile 关键字等。同步机制通常用于保护共享资源,避免多个线程同时修改共享资源导致的数据不一致或竞态条件等问题。
-
同步器:同步器是一个更加通用化和抽象化的概念,它是用于实现各种同步工具类的基础框架。Java 中的同步器主要体现在 java.util.concurrent 包中,其中的同步器类包括 CountDownLatch、CyclicBarrier、Semaphore、Phaser 等。同步器提供了一种在多线程环境下实现协调和同步的通用机制,它可以根据具体需求实现不同的同步工具。
-
并发工具类:并发工具类是建立在同步机制和同步器基础上的一组实用工具,用于简化多线程编程中的并发控制和协作。这些工具类通常封装了常用的同步机制和同步器,提供了更高层次的抽象和封装,使得开发人员能够更加方便地编写线程安全的并发代码。常见的并发工具类包括CountDownLatch、CyclicBarrier、Semaphore、ConcurrentHashMap 等。
因此,可以说同步机制是实现线程同步的基础,同步器是对同步机制的抽象和封装,而并发工具类则是建立在同步机制和同步器之上的更高级别的工具,用于简化并发编程的复杂度。
并发编程模型是指用于处理多个任务并发执行的编程范例或模式。以下是几种常见的并发编程模型:
-
共享内存模型:在共享内存模型中,多个线程或进程共享同一块内存空间,它们通过读写共享内存来进行通信和同步。常见的共享内存模型包括使用线程共享同一对象或数据结构,例如使用 synchronized 关键字或 Lock 接口进行同步。
-
消息传递模型:在消息传递模型中,不同的任务通过发送消息来进行通信。消息可以是在不同线程或进程之间传递的任意数据结构,包括简单的原始数据、对象或事件通知。常见的消息传递模型包括使用队列、管道或者异步消息传递机制。
- 示例:Java中常用的消息传递方式包括队列、管道、Socket等。下面是一个简单的Java示例,演示了使用队列作为消息传递的模型:
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class MessagePassingModelExample { // 创建一个阻塞队列作为消息传递的通道 private static BlockingQueue<String> messageQueue = new ArrayBlockingQueue<>(10); public static void main(String[] args) { // 创建生产者线程,负责向队列发送消息 Thread producerThread = new Thread(() -> { try { for (int i = 0; i < 5; i++) { String message = "Message " + i; messageQueue.put(message); // 将消息放入队列 System.out.println("Producer: sent " + message); Thread.sleep(1000); // 模拟发送消息的过程 } } catch (InterruptedException e) { e.printStackTrace(); } }); // 创建消费者线程,负责从队列接收消息 Thread consumerThread = new Thread(() -> { try { for (int i = 0; i < 5; i++) { String message = messageQueue.take(); // 从队列中取出消息 System.out.println("Consumer: received " + message); Thread.sleep(1000); // 模拟接收消息的过程 } } catch (InterruptedException e) { e.printStackTrace(); } }); // 启动生产者和消费者线程 producerThread.start(); consumerThread.start(); } }
在这个示例中,
messageQueue
是一个阻塞队列,用作消息传递的通道。生产者线程向队列中发送消息,而消费者线程从队列中接收消息。使用阻塞队列可以确保线程安全,因为队列内部会处理线程之间的并发访问。 -
Actors 模型:Actors 模型是一种并发编程模型,其中的并发单元被称为 actors,每个 actor 都有自己的状态和行为,并且能够接收和发送消息。Actors 之间通过消息传递进行通信和协作,从而避免了共享内存模型中的竞态条件和锁。常见的 Actors 模型的实现包括 Akka 框架。
-
在Java中实现Actor模型可以借助第三方库,比如Akka。Akka是一个基于Actor模型的并发编程框架,可以很方便地在Java中使用。下面是一个简单的示例,演示了如何在Java中使用Akka来创建Actor并进行消息传递:
首先,你需要在Maven或Gradle中引入Akka的依赖:
<!-- Maven --> <dependency> <groupId>com.typesafe.akka</groupId> <artifactId>akka-actor_2.13</artifactId> <version>2.6.16</version> </dependency>
// Gradle implementation 'com.typesafe.akka:akka-actor_2.13:2.6.16'
然后,你可以编写一个简单的Actor类:
import akka.actor.AbstractActor; import akka.actor.ActorRef; import akka.actor.ActorSystem; import akka.actor.Props; // 定义一个消息类 class Message { public final String content; public Message(String content) { this.content = content; } } // 定义一个Actor类 class MyActor extends AbstractActor { @Override public Receive createReceive() { return receiveBuilder() .match(Message.class, message -> { System.out.println("Received message: " + message.content); }) .build(); } } public class ActorModelExample { public static void main(String[] args) { // 创建Actor系统 ActorSystem system = ActorSystem.create("actor-system"); // 创建MyActor实例 ActorRef myActor = system.actorOf(Props.create(MyActor.class), "my-actor"); // 发送消息给MyActor myActor.tell(new Message("Hello, Actor!"), ActorRef.noSender()); // 关闭Actor系统 system.terminate(); } }
在这个示例中,我们定义了一个简单的消息类
Message
,然后创建了一个继承自AbstractActor
的Actor类MyActor
,并实现了createReceive()
方法来定义消息处理逻辑。在ActorModelExample
中,我们创建了一个Actor系统,然后通过Props
类创建了MyActor
实例,并向它发送了一条消息。通过Akka,你可以很方便地创建更复杂的Actor系统,并实现高效的并发编程。
-
-
数据流模型:在数据流模型中,任务被组织成数据流图,其中的节点表示任务或操作,边表示数据流。数据流模型将计算任务分解为一系列独立的操作,每个操作都会接收输入数据,并产生输出数据。数据流模型可以有效地实现并行计算,例如流式处理系统和图处理系统。
在Java中实现数据流模型可以借助并行流(Parallel Streams)或使用并发工具类来实现。下面是一个示例,演示了如何使用并行流来处理数据流并发:import java.util.Arrays; public class DataFlowModelExample { public static void main(String[] args) { // 准备数据 int[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 使用并行流处理数据 Arrays.stream(data) .parallel() // 将顺序流转换为并行流 .map(DataFlowModelExample::processData) // 处理数据 .forEach(System.out::println); // 输出结果 } // 模拟数据处理过程 public static int processData(int num) { try { // 模拟耗时操作 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 返回处理结果 return num * 2; } }
在这个示例中,我们准备了一个简单的整型数组作为数据流,然后使用
Arrays.stream(data)
将其转换为流。接着使用.parallel()
方法将流转换为并行流,这样数据处理操作就会并发执行。最后,我们使用.map()
方法对数据进行处理,这里的processData()
方法模拟了数据处理的过程,最终使用.forEach()
方法输出处理结果。通过使用并行流,Java提供了一种简单而有效的方式来实现数据流模型并发处理。
-
函数式编程模型:函数式编程模型强调无副作用的函数和不可变数据结构,这使得函数式编程在并发环境中更容易实现并行性。通过避免共享状态和可变状态,函数式编程模型可以减少竞争条件和死锁,并提高并发程序的可维护性和可扩展性。
在Java中,函数式编程模型可以通过使用CompletableFuture
来展示并发。下面是一个示例,演示了如何使用CompletableFuture
实现函数式并发编程:import java.util.concurrent.CompletableFuture; public class FunctionalProgrammingExample { public static void main(String[] args) { // 使用CompletableFuture实现并发 CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> task1()); CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> task2()); // 等待两个任务完成并处理结果 CompletableFuture<Void> combinedFuture = CompletableFuture.allOf(future1, future2); combinedFuture.thenRun(() -> { String result1 = future1.join(); String result2 = future2.join(); System.out.println("Result from task1: " + result1); System.out.println("Result from task2: " + result2); }); // 主线程继续执行其他任务 System.out.println("Main thread continues executing other tasks..."); //让进程不被杀死 while (true){ try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } } } // 模拟任务1 public static String task1() { try { // 模拟耗时操作 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } return "Result of Task 1"; } // 模拟任务2 public static String task2() { try { // 模拟耗时操作 Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } return "Result of Task 2"; } }
在这个示例中,我们使用
CompletableFuture.supplyAsync()
方法创建了两个异步任务,并使用allOf()
方法组合这两个任务,以便在它们都完成时进行处理。在处理结果时,我们使用join()
方法等待任务完成并获取结果。最后,我们输出任务的结果。通过使用
CompletableFuture
,我们可以实现函数式编程模型的并发,这为编写高效的并发代码提供了一种简洁而强大的方式。
这些并发编程模型各有优缺点,选择合适的模型取决于应用的需求、性能要求和开发团队的经验。在实际应用中,通常会结合多种模型来解决复杂的并发编程问题。