MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java并发编程中的设计模式

2021-05-307.9k 阅读

单例模式在 Java 并发编程中的应用

单例模式是一种常用的设计模式,其目的是确保一个类在整个应用程序中只有一个实例,并提供一个全局访问点。在并发编程环境下,实现单例模式需要特别注意线程安全问题。

饿汉式单例

饿汉式单例在类加载时就创建实例,由于类加载由 JVM 保证线程安全,所以这种方式天生就是线程安全的。

public class EagerSingleton {
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    private EagerSingleton() {
        // 私有构造函数,防止外部实例化
    }

    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

这种方式虽然简单直接且线程安全,但如果单例实例占用资源较大,而应用程序可能并不需要马上使用该实例,那么这种提前创建实例的方式可能会造成资源浪费。

懒汉式单例(线程不安全)

懒汉式单例是在第一次调用 getInstance 方法时才创建实例。下面是一个线程不安全的懒汉式单例实现:

public class LazySingletonUnsafe {
    private static LazySingletonUnsafe instance;

    private LazySingletonUnsafe() {
        // 私有构造函数,防止外部实例化
    }

    public static LazySingletonUnsafe getInstance() {
        if (instance == null) {
            instance = new LazySingletonUnsafe();
        }
        return instance;
    }
}

在多线程环境下,如果多个线程同时进入 if (instance == null) 条件判断,就可能会创建多个实例,破坏了单例模式的唯一性。

懒汉式单例(线程安全,同步方法)

为了使懒汉式单例在多线程环境下安全,可以使用 synchronized 关键字修饰 getInstance 方法。

public class LazySingletonSafeSynchronized {
    private static LazySingletonSafeSynchronized instance;

    private LazySingletonSafeSynchronized() {
        // 私有构造函数,防止外部实例化
    }

    public static synchronized LazySingletonSafeSynchronized getInstance() {
        if (instance == null) {
            instance = new LazySingletonSafeSynchronized();
        }
        return instance;
    }
}

这种方式虽然保证了线程安全,但每次调用 getInstance 方法都需要进行同步操作,性能较低,特别是在高并发场景下,会成为性能瓶颈。

双重检查锁定(DCL)实现单例

双重检查锁定是一种优化的懒汉式单例实现方式,既能保证线程安全,又能提高性能。

public class DoubleCheckedLockingSingleton {
    private static volatile DoubleCheckedLockingSingleton instance;

    private DoubleCheckedLockingSingleton() {
        // 私有构造函数,防止外部实例化
    }

    public static DoubleCheckedLockingSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckedLockingSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckedLockingSingleton();
                }
            }
        }
        return instance;
    }
}

这里使用 volatile 关键字修饰 instance 变量,是为了防止指令重排序。在 instance = new DoubleCheckedLockingSingleton(); 这行代码中,实际上会有三个步骤:1. 分配内存空间;2. 初始化对象;3. 将 instance 指向分配的内存空间。如果没有 volatile,在多线程环境下,可能会出现指令重排序,导致一个线程在对象还未初始化完成时就看到了 instance 不为 null,从而返回一个未初始化好的对象。

静态内部类实现单例

静态内部类实现单例结合了懒汉式的延迟加载和饿汉式的线程安全优点。

public class StaticInnerClassSingleton {
    private StaticInnerClassSingleton() {
        // 私有构造函数,防止外部实例化
    }

    private static class SingletonHolder {
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

这种方式利用了类加载机制,只有在调用 getInstance 方法时,才会加载 SingletonHolder 类,从而创建单例实例,保证了延迟加载和线程安全。

生产者 - 消费者模式在 Java 并发编程中的应用

生产者 - 消费者模式是一种经典的设计模式,用于解耦生产者和消费者的操作,提高系统的可扩展性和稳定性。在 Java 并发编程中,通常可以使用 BlockingQueue 来实现该模式。

使用 BlockingQueue 实现生产者 - 消费者

BlockingQueue 是一个线程安全的队列,当队列满时,生产者线程会被阻塞,直到队列有空间;当队列空时,消费者线程会被阻塞,直到队列有元素。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class Producer implements Runnable {
    private final BlockingQueue<Integer> queue;

    public Producer(BlockingQueue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                System.out.println("Producing: " + i);
                queue.put(i);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

class Consumer implements Runnable {
    private final BlockingQueue<Integer> queue;

    public Consumer(BlockingQueue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Integer item = queue.take();
                System.out.println("Consuming: " + item);
                if (item.equals(9)) {
                    break;
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

public class ProducerConsumerExample {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
        Thread producerThread = new Thread(new Producer(queue));
        Thread consumerThread = new Thread(new Consumer(queue));

        producerThread.start();
        consumerThread.start();

        try {
            producerThread.join();
            consumerThread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

在这个例子中,Producer 线程生成整数并放入 BlockingQueue 中,Consumer 线程从队列中取出整数并消费。BlockingQueue 的容量为 5,当队列满时,Producer 线程会被阻塞,直到 Consumer 线程取出元素;当队列空时,Consumer 线程会被阻塞,直到 Producer 线程放入元素。

使用 wait() 和 notify() 实现生产者 - 消费者

在 Java 中,也可以通过 Object 类的 wait()notify() 方法来实现生产者 - 消费者模式。

import java.util.ArrayList;
import java.util.List;

class ProducerWaitNotify implements Runnable {
    private final List<Integer> buffer;
    private final int capacity;

    public ProducerWaitNotify(List<Integer> buffer, int capacity) {
        this.buffer = buffer;
        this.capacity = capacity;
    }

    @Override
    public void run() {
        int value = 0;
        while (true) {
            synchronized (buffer) {
                while (buffer.size() == capacity) {
                    try {
                        buffer.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                System.out.println("Producing: " + value);
                buffer.add(value++);
                buffer.notify();
            }
        }
    }
}

class ConsumerWaitNotify implements Runnable {
    private final List<Integer> buffer;

    public ConsumerWaitNotify(List<Integer> buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (buffer) {
                while (buffer.isEmpty()) {
                    try {
                        buffer.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                int item = buffer.remove(0);
                System.out.println("Consuming: " + item);
                buffer.notify();
            }
        }
    }
}

public class ProducerConsumerWaitNotifyExample {
    public static void main(String[] args) {
        List<Integer> buffer = new ArrayList<>();
        int capacity = 5;

        Thread producerThread = new Thread(new ProducerWaitNotify(buffer, capacity));
        Thread consumerThread = new Thread(new ConsumerWaitNotify(buffer));

        producerThread.start();
        consumerThread.start();
    }
}

在这个实现中,ProducerWaitNotifyConsumerWaitNotify 线程通过 synchronized 关键字同步访问共享的 buffer 列表。当 buffer 满时,ProducerWaitNotify 线程调用 wait() 方法进入等待状态,直到 ConsumerWaitNotify 线程消费了元素并调用 notify() 方法唤醒它;当 buffer 空时,ConsumerWaitNotify 线程调用 wait() 方法进入等待状态,直到 ProducerWaitNotify 线程生产了元素并调用 notify() 方法唤醒它。

线程池模式在 Java 并发编程中的应用

线程池模式是一种管理和复用线程的设计模式,可以避免频繁创建和销毁线程带来的开销,提高系统的性能和资源利用率。

使用 Executors 创建线程池

Java 的 Executors 类提供了一些静态方法来创建不同类型的线程池。

  • FixedThreadPool:创建一个固定大小的线程池,线程池中的线程数量始终保持不变。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }
        executorService.shutdown();
    }
}

在这个例子中,newFixedThreadPool(3) 创建了一个包含 3 个线程的线程池。当提交 5 个任务时,前 3 个任务会立即被执行,剩下 2 个任务会在队列中等待,直到有线程空闲。

  • CachedThreadPool:创建一个可缓存的线程池,如果线程池中有空闲线程,则复用空闲线程执行任务;如果没有空闲线程,则创建新的线程执行任务。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }
        executorService.shutdown();
    }
}

这种线程池适用于执行大量短期异步任务的场景,因为它会根据任务的数量动态调整线程数量。

  • SingleThreadExecutor:创建一个单线程的线程池,所有任务会按照提交的顺序依次执行。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleThreadExecutorExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }
        executorService.shutdown();
    }
}

这种线程池适用于需要顺序执行任务,并且不希望有多个线程同时执行的场景。

使用 ThreadPoolExecutor 自定义线程池

ThreadPoolExecutor 是线程池的核心实现类,通过它可以更灵活地自定义线程池的参数。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomThreadPoolExample {
    public static void main(String[] args) {
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(5);
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(
                2,
                4,
                10,
                TimeUnit.SECONDS,
                workQueue
        );
        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }
        executorService.shutdown();
    }
}

在这个例子中,ThreadPoolExecutor 的构造函数参数含义如下:

  • corePoolSize:核心线程数,线程池会始终保持至少 corePoolSize 个线程。
  • maximumPoolSize:最大线程数,线程池允许的最大线程数量。
  • keepAliveTime:当线程池中的线程数量超过 corePoolSize 时,多余的空闲线程的存活时间。
  • unitkeepAliveTime 的时间单位。
  • workQueue:任务队列,用于存放等待执行的任务。

观察者模式在 Java 并发编程中的应用

观察者模式定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会得到通知并自动更新。在 Java 并发编程中,观察者模式可以用于实现事件驱动的编程。

使用 Java 内置的 Observable 和 Observer 实现观察者模式

Java 提供了 Observable 类和 Observer 接口来支持观察者模式。

import java.util.Observable;
import java.util.Observer;

class Subject extends Observable {
    private int state;

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
        setChanged();
        notifyObservers();
    }
}

class ObserverImpl implements Observer {
    @Override
    public void update(Observable o, Object arg) {
        Subject subject = (Subject) o;
        System.out.println("Observer notified, new state: " + subject.getState());
    }
}

public class ObserverExample {
    public static void main(String[] args) {
        Subject subject = new Subject();
        ObserverImpl observer = new ObserverImpl();
        subject.addObserver(observer);

        subject.setState(10);
    }
}

在这个例子中,Subject 类继承自 Observable,当调用 setState 方法改变状态时,先调用 setChanged() 方法标记状态已改变,然后调用 notifyObservers() 方法通知所有注册的观察者。ObserverImpl 类实现了 Observer 接口,其 update 方法在接收到通知时会被调用。

使用 Guava 库的 EventBus 实现观察者模式

Google Guava 库提供了 EventBus 来更方便地实现观察者模式,并且在并发环境下有更好的性能和功能。

import com.google.common.eventbus.EventBus;

class Event {
    private final int data;

    public Event(int data) {
        this.data = data;
    }

    public int getData() {
        return data;
    }
}

class EventListener {
    @com.google.common.eventbus.Subscribe
    public void handleEvent(Event event) {
        System.out.println("Handling event with data: " + event.getData());
    }
}

public class GuavaEventBusExample {
    public static void main(String[] args) {
        EventBus eventBus = new EventBus();
        EventListener eventListener = new EventListener();
        eventBus.register(eventListener);

        Event event = new Event(20);
        eventBus.post(event);
    }
}

在这个例子中,EventBus 用于管理事件和订阅者。EventListener 类通过 @Subscribe 注解标记处理事件的方法。当 eventBus.post(event) 被调用时,EventListenerhandleEvent 方法会被自动调用。

Future模式在 Java 并发编程中的应用

Future 模式是一种异步计算模式,允许程序在执行一个可能耗时的操作时,先返回一个 Future 对象,通过这个对象可以在将来的某个时刻获取操作的结果。

使用 FutureTask 实现 Future 模式

FutureTask 类实现了 FutureRunnable 接口,可以用于异步执行任务并获取结果。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class Task implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        Thread.sleep(2000); // 模拟耗时操作
        return 42;
    }
}

public class FutureTaskExample {
    public static void main(String[] args) {
        Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<>(task);
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            Integer result = futureTask.get();
            System.out.println("Task result: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

在这个例子中,Task 类实现了 Callable 接口,call 方法中模拟了一个耗时操作。FutureTask 封装了 Task,并可以通过 Thread 启动任务。futureTask.get() 方法会阻塞当前线程,直到任务执行完成并返回结果。

使用 ExecutorService 和 Future 实现 Future 模式

ExecutorService 也可以提交任务并返回 Future 对象来获取异步任务的结果。

import java.util.concurrent.*;

class CallableTask implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        Thread.sleep(2000); // 模拟耗时操作
        return 42;
    }
}

public class ExecutorServiceFutureExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<Integer> future = executorService.submit(new CallableTask());

        try {
            Integer result = future.get();
            System.out.println("Task result: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }
    }
}

在这个例子中,executorService.submit(new CallableTask()) 提交一个 Callable 任务并返回一个 Future 对象。通过 future.get() 可以获取任务的执行结果,同时使用 finally 块关闭 ExecutorService

并发编程中的设计模式总结与应用场景

不同的设计模式在 Java 并发编程中有着各自独特的应用场景。单例模式确保在多线程环境下一个类只有一个实例,常用于管理共享资源,如数据库连接池。生产者 - 消费者模式解耦了生产和消费的过程,适用于任务队列、消息队列等场景。线程池模式提高了线程的复用性和系统性能,广泛应用于需要处理大量并发任务的系统。观察者模式实现了事件驱动的编程,可用于处理系统中的各种事件通知。Future 模式用于异步计算,使程序在等待耗时操作结果时不会阻塞主线程。

在实际开发中,需要根据具体的需求和场景选择合适的设计模式。例如,在一个高并发的 Web 应用中,可能会同时使用线程池模式来处理大量的用户请求,使用生产者 - 消费者模式来处理任务队列,以及使用单例模式来管理数据库连接等共享资源。同时,要注意并发编程中的线程安全问题,合理使用同步机制和并发工具类,确保程序的正确性和稳定性。通过熟练掌握这些设计模式,可以提高 Java 并发编程的效率和质量,开发出更加健壮和高性能的应用程序。

以上就是 Java 并发编程中常见的设计模式及其应用,希望能帮助你在并发编程领域有更深入的理解和实践。在实际项目中,不断探索和应用这些模式,将有助于解决复杂的并发问题,提升系统的性能和可靠性。同时,随着 Java 技术的不断发展,新的并发工具和设计模式也可能会不断涌现,需要持续学习和关注。