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

Java并发编程中的性能调优

2021-07-262.0k 阅读

Java 并发编程基础回顾

在深入探讨性能调优之前,先简单回顾一下 Java 并发编程的基础知识。

线程与线程池

线程是 Java 并发编程的基本执行单元。在 Java 中,可以通过继承 Thread 类或实现 Runnable 接口来创建线程。例如:

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread is running");
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRunnable is running");
    }
}

使用线程池可以更好地管理线程,避免频繁创建和销毁线程带来的开销。Java 提供了 ExecutorService 接口及其实现类 ThreadPoolExecutor 来创建线程池。

ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.submit(() -> {
    System.out.println("Task is running in thread pool");
});
executorService.shutdown();

同步机制

  1. synchronized 关键字:用于实现线程同步,它可以修饰方法或代码块。修饰方法时,锁对象是 this;修饰静态方法时,锁对象是类的 Class 对象。修饰代码块时,可以指定锁对象。
public class SynchronizedExample {
    public synchronized void synchronizedMethod() {
        // 同步方法
    }

    public void synchronizedBlock() {
        synchronized (this) {
            // 同步代码块
        }
    }
}
  1. ReentrantLock:是 Java 5 引入的一种可重入的互斥锁,相比 synchronized 更灵活,提供了更多的功能,如公平锁、可中断的锁获取等。
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();
        try {
            // 临界区代码
        } finally {
            lock.unlock();
        }
    }
}

并发容器

  1. ConcurrentHashMap:线程安全的哈希表,相比 Hashtable 具有更高的并发性能。它采用分段锁机制,允许多个线程同时对不同的段进行操作。
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("key1", 1);
        Integer value = map.get("key1");
    }
}
  1. CopyOnWriteArrayList:线程安全的 List,在修改操作(如添加、删除元素)时,会创建一个新的数组来进行修改,读操作则直接读取旧数组,这样可以实现读写分离,提高并发性能。
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListExample {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        list.add("element1");
        String element = list.get(0);
    }
}

性能调优方向

减少锁竞争

  1. 锁粗化:如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中,此时可以将加锁的范围扩大到整个操作序列的外部,这就是锁粗化。例如:
public class LockCoarseningExample {
    private static final Object lock = new Object();

    public static void bad() {
        for (int i = 0; i < 1000; i++) {
            synchronized (lock) {
                // 一些简单操作
            }
        }
    }

    public static void good() {
        synchronized (lock) {
            for (int i = 0; i < 1000; i++) {
                // 同样的简单操作
            }
        }
    }
}

bad 方法中,每次循环都进行加锁解锁操作,而 good 方法将锁的范围扩大到循环外部,减少了锁竞争的次数。

  1. 锁细化:与锁粗化相反,锁细化是指将一个大的锁分解为多个小的锁,从而降低锁的粒度。以 ConcurrentHashMap 为例,它将整个哈希表分成多个段(Segment),每个段都有自己的锁。这样不同的线程可以同时访问不同的段,提高了并发性能。

  2. 使用读写锁:如果在系统中读操作远远多于写操作,可以使用读写锁(ReadWriteLock)。ReadWriteLock 允许多个线程同时进行读操作,但只允许一个线程进行写操作。

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private static ReadWriteLock lock = new ReentrantReadWriteLock();

    public static void read() {
        lock.readLock().lock();
        try {
            // 读操作
        } finally {
            lock.readLock().unlock();
        }
    }

    public static void write() {
        lock.writeLock().lock();
        try {
            // 写操作
        } finally {
            lock.writeLock().unlock();
        }
    }
}

优化线程池

  1. 合理设置线程池参数:线程池的核心参数包括 corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(线程存活时间)、unit(存活时间单位)和 workQueue(任务队列)。
    • corePoolSize:应根据任务类型和系统资源来设置。对于 CPU 密集型任务,一般设置为 CPU 核心数加 1;对于 I/O 密集型任务,可以适当增大,例如设置为 CPU 核心数的 2 倍。
    • maximumPoolSize:要考虑系统资源的承受能力,避免线程过多导致系统资源耗尽。
    • keepAliveTimeunit:控制非核心线程在任务队列空闲时的存活时间,适当调整可以减少线程的频繁创建和销毁。
    • workQueue:选择合适的任务队列,如 ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(无界队列)等。对于有界队列,如果任务提交速度过快,可能会导致任务拒绝,需要合理设置队列大小;无界队列虽然不会拒绝任务,但可能会导致内存耗尽。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        5, // corePoolSize
        10, // maximumPoolSize
        10, // keepAliveTime
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(100) // workQueue
);
  1. 使用合适的线程池类型
    • FixedThreadPool:固定大小的线程池,核心线程数和最大线程数相同,适用于任务数量已知且相对稳定的场景。
    • CachedThreadPool:可缓存的线程池,线程数会根据任务数量动态调整,适用于任务执行时间短且数量不确定的场景。
    • ScheduledThreadPool:用于执行定时任务或周期性任务。
// FixedThreadPool
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
// CachedThreadPool
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// ScheduledThreadPool
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

减少线程上下文切换

  1. 减少锁竞争:前面提到的减少锁竞争的方法,如锁粗化、锁细化、使用读写锁等,都可以间接减少线程上下文切换。因为锁竞争会导致线程阻塞,进而引发上下文切换。
  2. 使用无锁数据结构:在一些场景下,可以使用无锁数据结构来避免锁的使用,从而减少线程上下文切换。例如,ConcurrentLinkedQueue 是一个基于链表的无锁队列,在多线程环境下具有较高的性能。
import java.util.concurrent.ConcurrentLinkedQueue;

public class ConcurrentLinkedQueueExample {
    public static void main(String[] args) {
        ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
        queue.add(1);
        Integer element = queue.poll();
    }
}
  1. 调整线程优先级:合理调整线程优先级可以减少不必要的上下文切换。对于一些关键任务,可以适当提高其线程优先级,但要注意避免优先级反转的问题。
Thread highPriorityThread = new Thread(() -> {
    // 高优先级任务
});
highPriorityThread.setPriority(Thread.MAX_PRIORITY);
highPriorityThread.start();

性能调优工具

JDK 自带工具

  1. jstack:用于生成 Java 虚拟机当前时刻的线程快照。通过分析线程快照,可以定位线程死锁、线程长时间阻塞等问题。例如,在命令行中执行 jstack <pid><pid> 为 Java 进程的 ID),可以获取线程的堆栈信息。
  2. jstat:用于监视虚拟机各种运行状态信息,如类加载、内存、垃圾收集等。例如,使用 jstat -gc <pid> 1000 可以每 1000 毫秒打印一次垃圾收集的统计信息,帮助分析内存使用情况和垃圾收集性能。
  3. jconsole:图形化的监控工具,可以实时监控 Java 应用程序的内存、线程、类等运行状况。通过在命令行中执行 jconsole,然后连接到目标 Java 进程,即可进行监控。

第三方工具

  1. YourKit Java Profiler:功能强大的性能分析工具,能够详细分析 CPU、内存、线程等方面的性能问题。它可以深入到方法级别,展示方法的执行时间、调用次数等信息,帮助开发者快速定位性能瓶颈。
  2. VisualVM:集成了多个 JDK 自带工具的功能,并且提供了插件机制,可以扩展更多的功能。例如,可以通过安装插件来支持对远程 Java 应用的监控,以及对垃圾收集器的详细分析等。

实战案例分析

案例一:电商系统中的商品库存扣减

在一个电商系统中,有一个商品库存扣减的功能。多个用户可能同时下单购买同一件商品,需要保证库存扣减的线程安全性。

public class Stock {
    private int quantity;
    private static final Object lock = new Object();

    public Stock(int quantity) {
        this.quantity = quantity;
    }

    public void deduct(int amount) {
        synchronized (lock) {
            if (quantity >= amount) {
                quantity -= amount;
                System.out.println("Deducted " + amount + " units. Remaining stock: " + quantity);
            } else {
                System.out.println("Not enough stock.");
            }
        }
    }
}

在上述代码中,使用 synchronized 关键字来保证库存扣减的线程安全性。但随着并发量的增加,锁竞争可能会成为性能瓶颈。可以考虑使用 AtomicInteger 来优化:

import java.util.concurrent.atomic.AtomicInteger;

public class OptimizedStock {
    private AtomicInteger quantity;

    public OptimizedStock(int quantity) {
        this.quantity = new AtomicInteger(quantity);
    }

    public void deduct(int amount) {
        while (true) {
            int current = quantity.get();
            if (current < amount) {
                System.out.println("Not enough stock.");
                return;
            }
            if (quantity.compareAndSet(current, current - amount)) {
                System.out.println("Deducted " + amount + " units. Remaining stock: " + quantity.get());
                return;
            }
        }
    }
}

通过使用 AtomicIntegercompareAndSet 方法,实现了无锁的原子操作,提高了并发性能。

案例二:分布式系统中的任务调度

在一个分布式系统中,有一个任务调度模块,需要将大量的任务分配到多个工作节点执行。使用线程池来管理任务的执行。

public class TaskScheduler {
    private static final int CORE_POOL_SIZE = 10;
    private static final int MAX_POOL_SIZE = 50;
    private static final int QUEUE_CAPACITY = 1000;
    private static final long KEEP_ALIVE_TIME = 10;

    private ExecutorService executorService;

    public TaskScheduler() {
        executorService = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }

    public void submitTask(Runnable task) {
        executorService.submit(task);
    }

    public void shutdown() {
        executorService.shutdown();
    }
}

在这个案例中,合理设置了线程池的参数。但在实际运行中发现,任务执行时间较长,导致任务队列积压。经过分析,发现部分任务是 I/O 密集型任务,可以通过增加核心线程数来提高性能:

public class OptimizedTaskScheduler {
    private static final int CORE_POOL_SIZE = 20;
    private static final int MAX_POOL_SIZE = 50;
    private static final int QUEUE_CAPACITY = 1000;
    private static final long KEEP_ALIVE_TIME = 10;

    private ExecutorService executorService;

    public OptimizedTaskScheduler() {
        executorService = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }

    public void submitTask(Runnable task) {
        executorService.submit(task);
    }

    public void shutdown() {
        executorService.shutdown();
    }
}

通过将核心线程数从 10 增加到 20,提高了 I/O 密集型任务的处理能力,减少了任务队列的积压。

总结常见性能问题及解决方案

  1. 锁竞争导致性能下降:通过锁粗化、锁细化、使用读写锁等方法减少锁竞争,或者使用无锁数据结构来避免锁的使用。
  2. 线程池参数设置不合理:根据任务类型(CPU 密集型或 I/O 密集型)合理设置线程池的核心参数,如 corePoolSizemaximumPoolSize 等,选择合适的任务队列。
  3. 线程上下文切换频繁:减少锁竞争,使用无锁数据结构,合理调整线程优先级。
  4. 资源消耗过大:通过性能分析工具(如 JDK 自带工具、第三方工具)监控内存、CPU 等资源的使用情况,及时发现并解决内存泄漏、CPU 使用率过高等问题。

在 Java 并发编程中,性能调优是一个复杂而又关键的工作。需要开发者深入理解并发编程的原理,结合实际业务场景,灵活运用各种调优方法和工具,才能打造出高性能的并发应用程序。