JUC面试问答

一、Java并发基础

1. 什么是Java中的线程?线程与进程有什么区别?

回答:

  • 线程(Thread)
    • 是操作系统能够进行运算调度的最小单位。
    • 线程是进程中的一个执行路径,共享进程的资源(如内存)。
    • Java中的Thread类和Runnable接口用于创建和管理线程。
  • 进程(Process)
    • 是具有一定独立功能的程序关于某数据集合上的一次运行活动。
    • 进程之间是相互独立的,每个进程有自己的内存空间。
    • 一个进程可以包含多个线程。

区别

  • 资源占用:进程拥有独立的内存空间,线程共享进程的内存。
  • 创建和销毁:线程的创建和销毁比进程更高效。
  • 通信方式:线程间通信(共享内存)比进程间通信(如管道、信号)更简单和高效。

2. Java中如何创建和启动一个线程?

回答:

Java中创建和启动线程有两种主要方式:

  1. 继承Thread

    public class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Thread is running.");
        }
    
        public static void main(String[] args) {
            MyThread thread = new MyThread();
            thread.start(); // 启动线程
        }
    }
    
  2. 实现Runnable接口

    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("Runnable is running.");
        }
    
        public static void main(String[] args) {
            Thread thread = new Thread(new MyRunnable());
            thread.start(); // 启动线程
        }
    }
    

推荐:实现Runnable接口更灵活,因为Java不支持多继承,可以同时继承其他类。

3. synchronized关键字的作用是什么?它是如何工作的?

回答:

  • 作用

    • synchronized关键字用于实现线程同步,防止多个线程同时访问共享资源导致数据不一致。
    • 它可以修饰方法或代码块,确保在同一时间只有一个线程执行被修饰的部分。
  • 工作原理

    • 每个对象都有一个内置的锁(monitor)。
    • 当一个线程进入synchronized方法或代码块时,会获取相应对象的锁。
    • 其他线程如果尝试进入同一锁的synchronized方法或代码块,会被阻塞,直到锁被释放。
  • 示例

    public class Counter {
        private int count = 0;
    
        // 同步方法
        public synchronized void increment() {
            count++;
        }
    
        // 同步代码块
        public void decrement() {
            synchronized(this) {
                count--;
            }
        }
    
        public int getCount() {
            return count;
        }
    }
    

注意

  • 使用synchronized会带来性能开销,应合理使用。
  • 避免锁的嵌套和死锁。

4. 什么是可重入锁(Reentrant Lock)?它与synchronized有何不同?

回答:

  • 可重入锁(Reentrant Lock)

    • 是指同一个线程可以多次获取同一把锁而不会被阻塞。
    • Java中通过java.util.concurrent.locks.ReentrantLock类实现。
  • synchronized的区别

    1. 功能丰富:

      • ReentrantLock提供了比synchronized更丰富的功能,如公平锁、可中断的锁获取、尝试锁定等。
    2. 灵活性:

      • ReentrantLock可以在不同的代码块之间共享锁。
    3. 性能:

      • 在某些场景下,ReentrantLock的性能可能优于synchronized,但现代JVM对synchronized进行了优化,两者性能差异已较小。
    4. 解锁机制:

      • ReentrantLock需要显式调用unlock()方法释放锁,通常在finally块中进行,以确保锁被释放。
      • synchronized自动释放锁。
  • 示例

    import java.util.concurrent.locks.ReentrantLock;
    
    public class ReentrantLockExample {
        private final ReentrantLock lock = new ReentrantLock();
        private int count = 0;
    
        public void increment() {
            lock.lock();
            try {
                count++;
            } finally {
                lock.unlock();
            }
        }
    
        public int getCount() {
            return count;
        }
    }
    

注意:使用ReentrantLock时必须确保在所有可能的执行路径上都释放锁,否则会导致死锁。

5. 线程的生命周期有哪些阶段?

回答:

线程的生命周期主要包括以下几个阶段:

  1. 新建(New)
    • 线程对象被创建,但尚未启动。
    • 通过new Thread()创建线程实例。
  2. 就绪(Runnable)
    • 线程已启动并等待CPU时间片执行。
    • 调用start()方法后,线程进入就绪状态。
  3. 运行(Running)
    • 线程获得CPU资源,开始执行run()方法中的代码。
  4. 阻塞(Blocked)
    • 线程因为等待锁或资源而暂时停止执行。
    • 例如,等待synchronized锁释放,或调用wait()sleep()等方法。
  5. 等待(Waiting)
    • 线程在等待另一个线程执行某个特定操作。
    • 通过Object.wait()Thread.join()Lockawait()等方法进入等待状态。
  6. 计时等待(Timed Waiting)
    • 线程在等待指定时间后会自动恢复就绪状态。
    • 通过Thread.sleep(long millis)Object.wait(long timeout)Lockawait(long time, TimeUnit unit)等方法进入计时等待状态。
  7. 终止(Terminated)
    • 线程执行完run()方法或被异常终止,进入终止状态。

状态转换图

New
 |
start()
 |
Runnable <----> Blocked
 |        |
run()    acquire lock
 |        |
Running  |
 |        |
finish() |
 |        |
Terminated

二、线程池(ThreadPool)

6. 线程池的作用是什么?为什么要使用线程池?

回答:

  • 作用

    • 管理和复用多个线程,避免频繁创建和销毁线程带来的性能开销。
    • 提供线程的生命周期管理,包括线程的创建、调度和销毁。
    • 控制并发执行的线程数量,避免系统过载。
  • 为什么要使用线程池

    1. 性能优化:

      • 减少线程创建和销毁的开销,提升性能。
    2. 资源管理:

      • 控制并发执行的线程数量,避免系统资源耗尽。
    3. 任务管理:

      • 提供任务排队和调度机制,合理分配任务到线程执行。
    4. 稳定性:

      • 通过线程池的参数配置(如核心线程数、最大线程数、队列容量等),提升系统的稳定性和可预测性。
    5. 便捷性:

      • 提供丰富的接口和工具类,简化多线程编程。

7. Java中如何创建一个线程池?请解释Executors类提供的不同类型的线程池。

回答:

Java中可以通过java.util.concurrent.Executors工厂类创建不同类型的线程池。以下是常见的线程池类型及其特点:

  1. Fixed Thread Pool(固定大小线程池)

    • 使用固定数量的线程执行任务。
    • 如果所有线程都在忙碌,任务会被放入等待队列。
    • 适用于负载稳定且任务数量已知的场景。

    创建方式

    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
    
  2. Cached Thread Pool(缓存线程池)

    • 根据需要创建新线程执行任务,空闲线程在一定时间后会被回收。
    • 适用于执行大量短期异步任务。
    • 可能创建无限数量的线程,需谨慎使用以避免资源耗尽。

    创建方式

    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    
  3. Single Thread Executor(单线程池)

    • 只有一个线程执行任务,任务按提交顺序依次执行。
    • 适用于需要保证任务按顺序执行的场景。

    创建方式

    ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    
  4. Scheduled Thread Pool(定时线程池)

    • 支持定时和周期性任务执行。
    • 适用于需要延迟执行或定期执行的任务。

    创建方式

    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
    
  5. Work Stealing Pool(工作窃取线程池,Java 8引入)

    • 基于ForkJoinPool实现,适用于处理大量的短期任务。
    • 线程可以窃取其他线程的任务,提高资源利用率。

    创建方式

    ExecutorService workStealingPool = Executors.newWorkStealingPool();
    

示例

ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
    final int index = i;
    executor.submit(() -> {
        System.out.println("Task " + index + " is running on " + Thread.currentThread().getName());
    });
}
executor.shutdown();

注意

  • 使用Executors工厂方法创建的线程池有一些缺陷(如无限线程池可能导致资源耗尽),在生产环境中更推荐使用ThreadPoolExecutor构造器进行自定义配置。

8. 什么是ThreadPoolExecutor?解释其主要参数。

回答:

ThreadPoolExecutor是Java中最灵活和强大的线程池实现类,位于java.util.concurrent包下。它提供了丰富的参数和方法,允许开发者根据具体需求自定义线程池行为。

主要参数

  1. corePoolSize(核心线程数)
    • 线程池中始终保持的最小线程数量。
    • 线程池启动时,会创建核心线程数的线程。
    • 即使线程处于空闲状态,也不会被回收,除非设置了allowCoreThreadTimeOut
  2. maximumPoolSize(最大线程数)
    • 线程池中允许的最大线程数量。
    • 当任务队列已满且核心线程都在忙碌时,线程池会创建新的非核心线程,直到达到最大线程数。
  3. keepAliveTime(线程空闲保持时间)
    • 非核心线程在空闲时等待新任务的最长时间。
    • 超过此时间,空闲的非核心线程会被回收。
  4. unit(时间单位)
    • keepAliveTime的时间单位,如TimeUnit.SECONDSTimeUnit.MILLISECONDS等。
  5. workQueue(任务队列)
    • 用于存储待执行任务的队列。
    • 常见实现类包括:
      • LinkedBlockingQueue:有界或无界的阻塞队列,适用于较多读操作。
      • SynchronousQueue:不存储元素的阻塞队列,适用于需要直接提交任务给线程执行的场景。
      • ArrayBlockingQueue:有界的阻塞队列,适用于固定容量的任务队列。
  6. threadFactory(线程工厂)
    • 用于创建新线程的工厂,允许自定义线程的创建方式,如设置线程名、守护线程等。
  7. handler(拒绝策略)
    • 当任务无法被执行时的处理策略。
    • 常见策略包括:
      • AbortPolicy(默认):抛出RejectedExecutionException
      • CallerRunsPolicy:由调用线程执行任务。
      • DiscardPolicy:静默丢弃任务。
      • DiscardOldestPolicy:丢弃队列中最旧的任务,尝试执行新任务。

构造示例

import java.util.concurrent.*;

public class CustomThreadPool {
    public static void main(String[] args) {
        int corePoolSize = 5;
        int maximumPoolSize = 10;
        long keepAliveTime = 60;
        TimeUnit unit = TimeUnit.SECONDS;
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
    
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            corePoolSize,
            maximumPoolSize,
            keepAliveTime,
            unit,
            workQueue,
            threadFactory,
            handler
        );
    
        // 提交任务
        for (int i = 0; i < 50; i++) {
            final int index = i;
            executor.execute(() -> {
                System.out.println("Task " + index + " is running on " + Thread.currentThread().getName());
            });
        }
    
        executor.shutdown();
    }
}

注意

  • 合理配置线程池参数(核心线程数、最大线程数、任务队列大小、拒绝策略等)对系统性能和稳定性至关重要。
  • 避免使用Executors工厂方法创建线程池,因为它们隐藏了线程池的具体参数配置,可能导致资源问题。

9. 什么是拒绝策略(Rejection Handler)?Java中有哪些常见的拒绝策略?

回答:

  • 拒绝策略(Rejection Handler)

    • 当线程池无法执行提交的任务时,ThreadPoolExecutor会调用拒绝策略处理该任务。
    • 任务无法执行的原因通常是线程池已达到最大线程数,并且任务队列已满。
  • 常见的拒绝策略

    1. AbortPolicy(默认):

      • 抛出RejectedExecutionException,拒绝任务的提交。
      RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
      
    2. CallerRunsPolicy

      • 由调用线程(提交任务的线程)执行该任务。
      • 不会抛出异常,适用于缓解任务提交速率过高的情况。
      RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
      
    3. DiscardPolicy

      • 静默丢弃被拒绝的任务,不抛出任何异常。
      • 适用于某些不关心丢失任务的场景。
      RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();
      
    4. DiscardOldestPolicy

      • 丢弃任务队列中最旧的未处理任务,然后尝试提交当前任务。
      • 适用于希望保留最新任务的场景。
      RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy();
      
  • 自定义拒绝策略

    • 通过实现RejectedExecutionHandler接口,可以定义自定义的拒绝策略。
    RejectedExecutionHandler customHandler = new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            // 自定义处理逻辑,如记录日志、重新提交任务等
            System.out.println("Task rejected: " + r.toString());
        }
    };
    

选择策略

  • AbortPolicy:适用于不允许任务丢失的场景。
  • CallerRunsPolicy:适用于希望减缓任务提交速度,并保持任务执行的场景。
  • DiscardPolicy:适用于任务丢失不会影响系统的场景。
  • DiscardOldestPolicy:适用于希望保留最新任务,丢弃最旧任务的场景。

10. 什么是ScheduledExecutorService?它有哪些常用的方法?

回答:

  • ScheduledExecutorService

    • ExecutorService的子接口,提供了定时和周期性任务执行的能力。
    • 支持在指定延迟后执行任务,或按照固定频率执行任务。
  • 常用方法

    1. schedule

      • 延迟指定时间后执行一个任务。
      ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
      scheduler.schedule(() -> System.out.println("Task executed after delay"), 5, TimeUnit.SECONDS);
      
    2. scheduleAtFixedRate

      • 按固定频率执行任务,任务之间的间隔时间是固定的,不考虑任务执行时间。
      scheduler.scheduleAtFixedRate(() -> System.out.println("Periodic task"), 0, 10, TimeUnit.SECONDS);
      
    3. scheduleWithFixedDelay

      • 按固定延迟执行任务,任务之间的间隔时间是固定的,任务执行完成后开始计时。
      scheduler.scheduleWithFixedDelay(() -> System.out.println("Periodic task with delay"), 0, 10, TimeUnit.SECONDS);
      
    4. shutdown

      • 关闭线程池,不再接受新任务,但会执行已提交的任务。
      scheduler.shutdown();
      
    5. shutdownNow

      • 立即关闭线程池,尝试停止所有正在执行的任务。
      scheduler.shutdownNow();
      
  • 示例

    import java.util.concurrent.*;
    
    public class ScheduledExecutorExample {
        public static void main(String[] args) {
            ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
    
            // 延迟3秒执行任务
            scheduler.schedule(() -> System.out.println("Task executed after 3 seconds"), 3, TimeUnit.SECONDS);
    
            // 每5秒执行一次任务,固定频率
            scheduler.scheduleAtFixedRate(() -> System.out.println("Fixed Rate Task"), 0, 5, TimeUnit.SECONDS);
    
            // 每5秒执行一次任务,固定延迟
            scheduler.scheduleWithFixedDelay(() -> System.out.println("Fixed Delay Task"), 0, 5, TimeUnit.SECONDS);
    
            // 让线程池运行一段时间后关闭
            scheduler.schedule(() -> {
                scheduler.shutdown();
                System.out.println("Scheduler shutdown.");
            }, 20, TimeUnit.SECONDS);
        }
    }
    

注意

  • 定时任务应合理配置延迟和频率,避免任务执行时间过长影响后续任务。
  • 使用shutdown()shutdownNow()方法正确关闭线程池,释放资源。

三、锁机制

11. 什么是锁(Lock)?Java中有哪些锁的实现?

回答:

  • 锁(Lock)

    • 是一种用于控制多个线程对共享资源访问的同步机制。
    • 通过锁机制,确保在同一时间只有一个线程可以访问临界区,避免数据竞争和不一致。
  • Java中锁的实现

    1. 内置锁(Intrinsic Lock):

      • 通过synchronized关键字实现,隐式获取和释放锁。
      • 每个对象都有一个内置锁(monitor)。
    2. 显示锁(Explicit Lock):

      • 通过java.util.concurrent.locks.Lock接口及其实现类实现。
      • 提供更灵活的锁控制,如可中断的锁获取、非阻塞尝试锁定等。

      常见实现类:

      • ReentrantLock

        • 可重入锁,允许同一线程多次获取同一把锁。
        • 提供公平锁和非公平锁两种模式。
      • ReentrantReadWriteLock

        • 读写锁,允许多个读线程同时访问,写线程独占访问。
        • 适用于读多写少的场景。
      • StampedLock

        (Java 8引入):

        • 提供乐观读锁和悲观读锁,支持更高效的读写操作。
        • 适用于高并发读写场景。
      • Semaphore

        • 信号量,用于控制同时访问特定资源的线程数量。
      • CountDownLatch

        • 允许一个或多个线程等待,直到一组操作完成。
      • CyclicBarrier

        • 允许一组线程互相等待,直到达到一个共同的屏障点。
      • LockSupport

        • 提供底层的锁和同步机制,支持构建高级同步工具。

12. 什么是ReentrantLock?它有哪些特点和优势?

回答:

  • ReentrantLock

    • java.util.concurrent.locks包下的一个可重入锁实现,提供了比synchronized更高级的锁控制。
  • 特点

    1. 可重入性

      • 同一线程可以多次获取同一把锁而不会被阻塞,锁的获取计数会增加,释放时计数减少,直到计数为零锁被释放。
    2. 公平性

      • 提供公平锁和非公平锁两种模式。公平锁按照线程请求锁的顺序进行获取,非公平锁则允许线程抢占锁。

      • 公平锁通过构造器参数设置:

        ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
        ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁
        
    3. 可中断的锁获取

      • 线程在等待锁时可以响应中断,通过lockInterruptibly()方法实现。
      try {
          lock.lockInterruptibly();
          // 执行任务
      } catch (InterruptedException e) {
          // 处理中断
      } finally {
          lock.unlock();
      }
      
    4. 尝试锁定

      • 线程可以尝试获取锁,如果锁不可用可以选择不等待,通过tryLock()方法实现。
      if (lock.tryLock()) {
          try {
              // 执行任务
          } finally {
              lock.unlock();
          }
      } else {
          // 处理未能获取锁的情况
      }
      
    5. 条件变量

      • 提供多个条件变量(Condition)支持更灵活的线程间通信,类似于Object.wait()Object.notify()
      Condition condition = lock.newCondition();
      
  • 优势

    1. 更高的灵活性:

      • 提供可中断的锁获取、尝试锁定等功能,满足更复杂的同步需求。
    2. 公平性选择:

      • 允许选择公平锁或非公平锁,适应不同的应用场景。
    3. 条件变量支持:

      • 允许创建多个条件变量,支持更加精细的线程等待和通知机制。
    4. 性能优化:

      • 在高并发场景下,ReentrantLock可能比synchronized表现出更好的性能和可伸缩性。

示例

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock(); // 获取锁
        try {
            count++;
        } finally {
            lock.unlock(); // 确保锁被释放
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + example.getCount()); // 输出: Final count: 2000
    }
}

13. 什么是ReentrantReadWriteLock?它的工作原理是什么?

回答:

  • ReentrantReadWriteLock

    • java.util.concurrent.locks包下的一个读写锁实现,允许多个读线程并发访问,或一个写线程独占访问。
    • 实现了ReadWriteLock接口。
  • 工作原理

    1. 读锁(Read Lock):

      • 多个线程可以同时持有读锁,只要没有线程持有写锁。
      • 适用于读多写少的场景,提升并发性能。
    2. 写锁(Write Lock):

      • 只有一个线程可以持有写锁,同时阻塞所有读锁和其他写锁的获取。
      • 适用于需要独占访问的场景,确保数据一致性。
    3. 可重入性:

      • 同一线程可以多次获取读锁或写锁,且读锁和写锁之间也是可重入的。
    4. 锁降级和升级:

      • 锁降级:先获取写锁,再获取读锁,最后释放写锁,可以安全地降级为读锁。
      • 锁升级:先获取读锁,再尝试获取写锁,可能会导致死锁,因此不推荐。
  • 组成

    • ReadLock:表示读锁,通过readLock()方法获取。
    • WriteLock:表示写锁,通过writeLock()方法获取。
  • 示例

    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    public class ReadWriteLockExample {
        private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
        private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
        private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
        private int count = 0;
    
        public void increment() {
            writeLock.lock();
            try {
                count++;
            } finally {
                writeLock.unlock();
            }
        }
    
        public int getCount() {
            readLock.lock();
            try {
                return count;
            } finally {
                readLock.unlock();
            }
        }
    
        public static void main(String[] args) {
            ReadWriteLockExample example = new ReadWriteLockExample();
            Runnable writer = () -> {
                for (int i = 0; i < 1000; i++) {
                    example.increment();
                }
            };
            Runnable reader = () -> {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("Count: " + example.getCount());
                }
            };
    
            Thread t1 = new Thread(writer);
            Thread t2 = new Thread(reader);
            t1.start();
            t2.start();
        }
    }
    

注意

  • 锁升级可能导致死锁:从读锁尝试获取写锁,如果其他线程持有写锁,会导致死锁。因此,应避免锁升级。
  • 锁降级是安全的:从写锁获取读锁后,再释放写锁,可以安全地降级为读锁。

14. 什么是StampedLock?它与ReentrantReadWriteLock有何不同?

回答:

  • StampedLock

    • 是Java 8引入的一个锁实现,提供了一种基于乐观锁的读写锁机制。
    • 通过标记(stamp)机制管理锁状态,支持更高效的并发读写操作。
    • 提供了三种锁模式:写锁、悲观读锁和乐观读锁。
  • ReentrantReadWriteLock的区别

    1. 乐观锁支持:

      • StampedLock提供了乐观读锁,允许线程在没有实际加锁的情况下读取数据,并通过验证确保数据的一致性。
      • ReentrantReadWriteLock仅提供悲观读锁,读锁期间会阻塞写锁。
    2. 标记机制:

      • StampedLock使用标记(stamp)来表示锁的状态,允许更细粒度的锁控制。
    3. 不可重入:

      • StampedLock是不支持重入的,线程不能多次获取同一类型的锁。
      • ReentrantReadWriteLock支持重入。
    4. 性能:

      • 在读多写少的场景下,StampedLock由于乐观读锁的存在,可能比ReentrantReadWriteLock表现更好。
    5. 方法复杂度:

      • StampedLock提供了更多的控制方法,如tryOptimisticRead()validate()等,使用相对复杂。
  • 使用示例

    import java.util.concurrent.locks.StampedLock;
    
    public class StampedLockExample {
        private final StampedLock sl = new StampedLock();
        private int count = 0;
    
        public void increment() {
            long stamp = sl.writeLock();
            try {
                count++;
            } finally {
                sl.unlockWrite(stamp);
            }
        }
    
        public int getCount() {
            long stamp = sl.tryOptimisticRead();
            int currentCount = count;
            if (!sl.validate(stamp)) {
                stamp = sl.readLock();
                try {
                    currentCount = count;
                } finally {
                    sl.unlockRead(stamp);
                }
            }
            return currentCount;
        }
    
        public static void main(String[] args) {
            StampedLockExample example = new StampedLockExample();
            Runnable writer = () -> {
                for (int i = 0; i < 1000; i++) {
                    example.increment();
                }
            };
            Runnable reader = () -> {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("Count: " + example.getCount());
                }
            };
    
            Thread t1 = new Thread(writer);
            Thread t2 = new Thread(reader);
            t1.start();
            t2.start();
        }
    }
    

注意

  • 不可重入StampedLock不支持重入,如果需要重入功能,应使用ReentrantReadWriteLock
  • 复杂性StampedLock的使用相对复杂,需要谨慎处理标记(stamp)的获取和释放。

15. 什么是Condition?如何在ReentrantLock中使用它?

回答:

  • Condition

    • java.util.concurrent.locks包下的一个接口,提供了类似于Objectwait()notify()方法的功能,但更灵活。
    • 允许线程在某些条件下等待,并在条件满足时被唤醒。
    • 每个Lock可以创建多个Condition,提供了更细粒度的线程控制。
  • ReentrantLock中使用Condition

    步骤

    1. 创建ReentrantLockCondition

      ReentrantLock lock = new ReentrantLock();
      Condition condition = lock.newCondition();
      
    2. 线程等待条件

      lock.lock();
      try {
          while (!conditionMet) {
              condition.await(); // 释放锁并等待
          }
          // 条件满足,继续执行
      } catch (InterruptedException e) {
          // 处理中断
      } finally {
          lock.unlock();
      }
      
    3. 线程通知条件

      lock.lock();
      try {
          // 修改条件状态
          conditionMet = true;
          condition.signal(); // 唤醒一个等待的线程
          // 或者 condition.signalAll(); // 唤醒所有等待的线程
      } finally {
          lock.unlock();
      }
      
  • 示例:生产者-消费者模型

    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.ReentrantLock;
    import java.util.LinkedList;
    import java.util.Queue;
    
    public class ProducerConsumer {
        private final Queue<Integer> queue = new LinkedList<>();
        private final int MAX_SIZE = 5;
        private final ReentrantLock lock = new ReentrantLock();
        private final Condition notFull = lock.newCondition();
        private final Condition notEmpty = lock.newCondition();
    
        public void produce(int value) throws InterruptedException {
            lock.lock();
            try {
                while (queue.size() == MAX_SIZE) {
                    notFull.await();
                }
                queue.add(value);
                notEmpty.signal(); // 通知消费者
            } finally {
                lock.unlock();
            }
        }
    
        public int consume() throws InterruptedException {
            lock.lock();
            try {
                while (queue.isEmpty()) {
                    notEmpty.await();
                }
                int value = queue.poll();
                notFull.signal(); // 通知生产者
                return value;
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) {
            ProducerConsumer pc = new ProducerConsumer();
    
            // 生产者
            Runnable producer = () -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        pc.produce(i);
                        System.out.println("Produced: " + i);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            };
    
            // 消费者
            Runnable consumer = () -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        int value = pc.consume();
                        System.out.println("Consumed: " + value);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            };
    
            new Thread(producer).start();
            new Thread(consumer).start();
        }
    }
    

注意

  • 使用Condition时,必须在获取锁的情况下调用await()signal()
  • await()方法会释放锁,并在条件满足时重新获取锁后继续执行。

四、并发集合

16. Java中有哪些并发集合类?简要介绍它们的用途。

回答:

Java提供了多种线程安全的并发集合类,位于java.util.concurrent包下。这些集合类设计用于在高并发环境下安全、高效地处理数据。

  • ConcurrentHashMap
    • 线程安全的哈希表实现,支持高并发的读写操作。
    • 不允许null键和null值。
    • 适用于需要频繁读写的键值对存储,如缓存、共享数据存储等。
  • CopyOnWriteArrayList
    • 线程安全的List实现,基于写时复制(Copy-On-Write)策略。
    • 适用于读多写少的场景,如事件监听器列表、配置项集合等。
    • 写操作会创建数组的副本,读操作无需同步。
  • CopyOnWriteArraySet
    • 线程安全的Set实现,基于CopyOnWriteArrayList
    • 适用于读多写少的唯一元素集合。
  • ConcurrentLinkedQueue
    • 基于链表的非阻塞线程安全队列。
    • 适用于需要高并发读写的队列,如任务队列、消息队列等。
  • ConcurrentLinkedDeque
    • 基于链表的非阻塞线程安全双端队列。
    • 适用于需要在两端插入和删除元素的高并发场景。
  • BlockingQueue接口及其实现
    • 提供了阻塞的队列操作,适用于生产者-消费者模型。
    • 常见实现类:
      • ArrayBlockingQueue:有界阻塞队列,基于数组实现。
      • LinkedBlockingQueue:有界或无界阻塞队列,基于链表实现。
      • PriorityBlockingQueue:无界优先级阻塞队列,基于优先级堆实现。
      • DelayQueue:无界延迟阻塞队列,元素必须实现Delayed接口。
  • ConcurrentSkipListMapConcurrentSkipListSet
    • 基于跳表实现的线程安全有序映射和集合。
    • 适用于需要有序访问和高并发的场景。
  • BlockingDeque接口及其实现
    • 提供了阻塞的双端队列操作。
    • 常见实现类:
      • LinkedBlockingDeque:基于链表的双端阻塞队列。
      • ArrayDeque(非阻塞,需外部同步)。
  • ConcurrentSkipListSet
    • 基于ConcurrentSkipListMap实现的线程安全有序集合。
    • 适用于需要高并发且有序的唯一元素集合。

选择合适的并发集合

  • 根据具体的应用场景和性能需求,选择最适合的并发集合类,以实现高效、线程安全的数据处理。

17. 什么是ConcurrentHashMap?它的内部实现原理是什么?

回答:

  • ConcurrentHashMap

    • java.util.concurrent包下的一个线程安全的哈希表实现。
    • 提供高并发的读写操作,支持多线程同时访问和修改。
    • 不允许null键和null值。
  • 内部实现原理

    Java 7之前

    • 基于分段锁(Segment Locks)机制。
    • 将整个哈希表分为多个段(Segment),每个段是一个独立的哈希表,有自己的锁。
    • 读操作无需锁定,写操作只锁定对应的段,提高了并发性能。

    Java 8及之后

    • 取消了分段锁,采用更细粒度的锁机制,基于Node的锁定和CAS操作。

    • 采用链表和红黑树相结合的数据结构:

      • 链表:存储哈希冲突的键值对。
      • 红黑树:当链表长度超过一定阈值(默认为8),链表会转换为红黑树,提升查找性能。
    • 同步机制:

      • 使用CAS(Compare-And-Swap)操作实现无锁读操作。
      • 在写操作时,采用节点级别的锁定,保证高效的并发写入。
  • 关键特性

    • 高并发性能:读操作基本不需要锁定,写操作只锁定必要的部分。
    • 动态扩容:支持动态扩容,随着元素数量的增加自动调整容量。
    • 线程安全性:通过内置的同步机制,确保在多线程环境下的数据一致性。
  • 示例

    import java.util.concurrent.ConcurrentHashMap;
    
    public class ConcurrentHashMapExample {
        public static void main(String[] args) {
            ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
            map.put("A", 1);
            map.put("B", 2);
            map.put("C", 3);
    
            // 线程安全的操作
            map.forEach(1, (key, value) -> System.out.println(key + ": " + value));
    
            map.computeIfAbsent("D", key -> 4);
            map.computeIfPresent("A", (key, value) -> value + 10);
    
            System.out.println(map); // 输出: {A=11, B=2, C=3, D=4}
        }
    }
    

注意

  • 不允许null键和值:尝试插入null键或值会抛出NullPointerException
  • 并发级别:Java 8的ConcurrentHashMap通过无锁读操作和更细粒度的锁定机制,提供了更高的并发性能。

18. 什么是CopyOnWriteArrayList?它适用于哪些场景?

回答:

  • CopyOnWriteArrayList

    • java.util.concurrent包下的一个线程安全的List实现,基于写时复制(Copy-On-Write)策略。
    • 所有的可变操作(如addsetremove等)都会在内部复制一个新的数组,进行修改,然后替换原有的数组。
    • 迭代器是基于数组的快照,独立于后续的修改,不会抛出ConcurrentModificationException
  • 适用场景

    • 读多写少的场景:适用于读操作频繁,而写操作较少的环境,如事件监听器列表、配置项集合等。
    • 线程安全的迭代:在多线程环境下,允许线程安全地遍历集合,而不需要显式同步。
    • 不可变视图:提供了一种不可变的视图,适合需要安全共享数据的场景。
  • 优点

    • 高效的读操作:由于读操作无需锁定,且迭代器基于快照,读性能高。
    • 线程安全:通过写时复制机制,保证线程安全。
    • 避免锁竞争:不需要显式的同步机制,避免了锁竞争问题。
  • 缺点

    • 高开销的写操作:每次写操作都会复制整个数组,开销较大。
    • 内存消耗高:频繁的写操作会导致大量的数组复制,增加内存使用。
    • 不适用于写多读少的场景
  • 示例

    import java.util.concurrent.CopyOnWriteArrayList;
    
    public class CopyOnWriteArrayListExample {
        public static void main(String[] args) {
            CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
            list.add("A");
            list.add("B");
            list.add("C");
    
            // 迭代时可以安全地修改集合
            for (String s : list) {
                System.out.println(s);
                list.add("D"); // 不会影响当前迭代
            }
    
            System.out.println(list); // 输出: [A, B, C, D, D, D]
        }
    }
    

注意

  • 适用场景:应根据应用需求选择合适的并发集合,避免在写操作频繁的场景下使用CopyOnWriteArrayList
  • 内存与性能:在高并发写操作时,CopyOnWriteArrayList可能导致内存和性能问题。

19. 什么是BlockingQueue?Java中有哪些常见的BlockingQueue实现?

回答:

  • BlockingQueue

    • java.util.concurrent包下的一个接口,扩展了Queue接口,支持阻塞的插入和移除操作。
    • 适用于生产者-消费者模型,提供线程安全的任务队列。
  • 常见的BlockingQueue实现

    1. ArrayBlockingQueue

      • 基于数组实现的有界阻塞队列。
      • 需要在创建时指定容量。
      • FIFO(先进先出)顺序。
      BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
      
    2. LinkedBlockingQueue

      • 基于链表实现的可选有界或无界阻塞队列。
      • 默认情况下是无界的,但可以通过构造器指定容量。
      • FIFO顺序。
      BlockingQueue<String> queue = new LinkedBlockingQueue<>();
      BlockingQueue<String> boundedQueue = new LinkedBlockingQueue<>(100);
      
    3. PriorityBlockingQueue

      • 基于优先级堆实现的无界阻塞队列。
      • 元素按照自然顺序或指定的比较器排序。
      • 不保证FIFO顺序。
      BlockingQueue<Integer> priorityQueue = new PriorityBlockingQueue<>();
      
    4. DelayQueue

      • 无界阻塞队列,只有到期的元素才能被取出。
      • 元素必须实现Delayed接口。
      • 适用于需要延迟处理的任务,如定时任务。
      BlockingQueue<DelayedTask> delayQueue = new DelayQueue<>();
      
    5. SynchronousQueue

      • 无缓冲的阻塞队列,每个插入操作必须等待对应的移除操作,反之亦然。
      • 适用于需要直接传递任务的场景,如任务提交到线程池。
      BlockingQueue<Runnable> syncQueue = new SynchronousQueue<>();
      
    6. LinkedTransferQueue

      • 基于链表实现的无界阻塞队列,支持直接传输元素给消费者。
      • 提供了transfer方法,允许生产者等待消费者接收元素。
      BlockingQueue<String> transferQueue = new LinkedTransferQueue<>();
      

选择合适的BlockingQueue

  • 有界 vs 无界:根据应用场景选择有界队列以限制资源消耗,或无界队列以避免任务丢失。
  • FIFO vs 优先级:选择FIFO队列进行任务的顺序执行,或优先级队列根据任务的优先级执行。
  • 特殊需求:如需要延迟执行的任务,选择DelayQueue;需要直接传递任务,选择SynchronousQueue

20. 什么是ConcurrentLinkedQueue?它的适用场景是什么?

回答:

  • ConcurrentLinkedQueue

    • java.util.concurrent包下的一个基于链表的非阻塞线程安全队列。
    • 实现了Queue接口,支持高并发的无锁操作。
    • 采用了乐观锁策略,使用CAS(Compare-And-Swap)操作来保证线程安全。
    • 无界队列,理论上可以容纳任意数量的元素(受限于系统内存)。
  • 适用场景

    • 高并发环境:适用于多个线程同时进行入队和出队操作的场景,如任务调度、消息传递等。
    • 非阻塞需求:不需要阻塞等待队列状态变化,适用于快速处理任务的场景。
    • 轻量级应用:由于没有阻塞机制,适用于对性能要求较高、任务处理快速的应用。
  • 关键特性

    • 线程安全:通过无锁算法实现高并发读写操作。
    • 高性能:在高并发场景下表现优异,避免了锁竞争带来的性能瓶颈。
    • 无阻塞:入队和出队操作不会阻塞线程,适合快速处理任务。
  • 示例

    import java.util.concurrent.ConcurrentLinkedQueue;
    
    public class ConcurrentLinkedQueueExample {
        public static void main(String[] args) {
            ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
            queue.add("A");
            queue.add("B");
            queue.add("C");
    
            // 多线程访问
            Runnable producer = () -> {
                for (int i = 0; i < 5; i++) {
                    queue.add("P" + i);
                    System.out.println("Produced: P" + i);
                }
            };
    
            Runnable consumer = () -> {
                for (int i = 0; i < 5; i++) {
                    String value = queue.poll();
                    if (value != null) {
                        System.out.println("Consumed: " + value);
                    }
                }
            };
    
            Thread t1 = new Thread(producer);
            Thread t2 = new Thread(consumer);
            t1.start();
            t2.start();
        }
    }
    

注意

  • 元素顺序ConcurrentLinkedQueue遵循FIFO顺序。
  • 不支持阻塞操作:如果需要阻塞等待元素,使用BlockingQueue的实现类,如ConcurrentLinkedDequeLinkedBlockingQueue等。

五、原子变量

21. 什么是原子变量(Atomic Variables)?它们的作用是什么?

回答:

  • 原子变量(Atomic Variables)

    • java.util.concurrent.atomic包下的一组类,提供了对单个变量的原子操作,支持无锁的线程安全编程。
    • 通过硬件级别的原子指令(如CAS操作)实现高效的并发操作。
  • 作用

    • 无锁编程:避免使用传统的锁机制,减少锁竞争和上下文切换开销,提升性能。
    • 简化同步:提供了一种更简单、直观的方式来实现线程安全的变量操作。
    • 原子性保证:确保对变量的操作是不可分割的,防止数据竞争和不一致。
  • 常见的原子变量类

    • AtomicIntegerAtomicLongAtomicBoolean:支持基本数据类型的原子操作。
    • AtomicReference<V>AtomicStampedReference<V>:支持引用类型的原子操作。
    • AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray<V>:支持数组类型的原子操作。
  • 示例

    import java.util.concurrent.atomic.AtomicInteger;
    
    public class AtomicIntegerExample {
        private AtomicInteger count = new AtomicInteger(0);
    
        public void increment() {
            count.incrementAndGet(); // 原子递增
        }
    
        public int getCount() {
            return count.get();
        }
    
        public static void main(String[] args) {
            AtomicIntegerExample example = new AtomicIntegerExample();
            Runnable task = () -> {
                for (int i = 0; i < 1000; i++) {
                    example.increment();
                }
            };
    
            Thread t1 = new Thread(task);
            Thread t2 = new Thread(task);
            t1.start();
            t2.start();
    
            try {
                t1.join();
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("Final count: " + example.getCount()); // 输出: Final count: 2000
        }
    }
    

注意

  • CAS操作:原子变量类主要依赖于CAS(Compare-And-Swap)操作实现原子性。
  • ABA问题:在某些情况下,CAS操作可能会遇到ABA问题,AtomicStampedReference等类提供了解决方案。
  • 与锁的选择:原子变量适用于简单的原子操作,复杂的同步需求仍需使用锁机制。

22. 解释AtomicInteger的常用方法,如incrementAndGetcompareAndSet

回答:

  • AtomicInteger

    • java.util.concurrent.atomic包下的一个类,提供对int类型的原子操作。
  • 常用方法

    1. incrementAndGet()

      • 原子地将当前值加1,并返回更新后的值。
      • 等价于addAndGet(1)
      AtomicInteger atomicInt = new AtomicInteger(0);
      int newValue = atomicInt.incrementAndGet(); // newValue = 1
      
    2. decrementAndGet()

      • 原子地将当前值减1,并返回更新后的值。
      int newValue = atomicInt.decrementAndGet(); // newValue = 0
      
    3. getAndIncrement()

      • 原子地将当前值加1,但返回的是加1前的值。
      int oldValue = atomicInt.getAndIncrement(); // oldValue = 0, atomicInt = 1
      
    4. getAndDecrement()

      • 原子地将当前值减1,但返回的是减1前的值。
      int oldValue = atomicInt.getAndDecrement(); // oldValue = 1, atomicInt = 0
      
    5. compareAndSet(int expect, int update)

      • 如果当前值等于expect,则原子地将其设置为update,并返回true
      • 否则,不做任何操作,返回false
      boolean success = atomicInt.compareAndSet(0, 100); // success = true, atomicInt = 100
      
    6. get()

      • 返回当前的值。
      int currentValue = atomicInt.get(); // currentValue = 100
      
    7. set(int newValue)

      • 设置为新值。
      atomicInt.set(200); // atomicInt = 200
      
    8. addAndGet(int delta)

      • 原子地将当前值加上delta,并返回更新后的值。
      int newValue = atomicInt.addAndGet(50); // newValue = 250
      
    9. getAndAdd(int delta)

      • 原子地将当前值加上delta,但返回的是加上delta前的值。
      int oldValue = atomicInt.getAndAdd(50); // oldValue = 250, atomicInt = 300
      
  • 使用场景

    • 实现高效的计数器。
    • 管理并发环境下的共享状态。
    • 无需使用锁的简单同步场景。

示例

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        int newCount = count.incrementAndGet();
        System.out.println("Incremented count: " + newCount);
    }

    public void conditionalUpdate(int expected, int newValue) {
        boolean updated = count.compareAndSet(expected, newValue);
        System.out.println("Conditional update from " + expected + " to " + newValue + ": " + updated);
    }

    public static void main(String[] args) {
        AtomicIntegerDemo demo = new AtomicIntegerDemo();
        demo.increment(); // Incremented count: 1
        demo.conditionalUpdate(1, 10); // Conditional update from 1 to 10: true
        demo.conditionalUpdate(5, 20); // Conditional update from 5 to 20: false
    }
}

输出

Incremented count: 1
Conditional update from 1 to 10: true
Conditional update from 5 to 20: false

23. 什么是AtomicReference?它的应用场景是什么?

回答:

  • AtomicReference<V>

    • java.util.concurrent.atomic包下的一个类,提供了对引用类型的原子操作。
    • 支持原子地获取、设置和更新对象引用。
  • 应用场景

    1. 非原子类型的原子操作:

      • 当需要对对象引用进行原子更新时,使用AtomicReference可以避免使用显式的同步。
    2. 实现无锁算法:

      • 在构建复杂的并发数据结构或算法时,使用AtomicReference可以实现无锁的线程安全操作。
    3. 状态管理:

      • 管理共享对象的状态变化,如状态机、配置对象的动态更新等。
    4. 引用更新:

      • 在多线程环境下,安全地更新共享的对象引用。
  • 常用方法

    1. get()

      • 返回当前的引用。
      AtomicReference<String> atomicRef = new AtomicReference<>("Initial");
      String value = atomicRef.get(); // "Initial"
      
    2. set(V newValue)

      • 设置为新引用。
      atomicRef.set("Updated");
      
    3. compareAndSet(V expect, V update)

      • 如果当前引用等于expect,则原子地将其设置为update,并返回true
      boolean success = atomicRef.compareAndSet("Updated", "Final");
      
    4. getAndSet(V newValue)

      • 原子地设置为新引用,并返回旧引用。
      String oldValue = atomicRef.getAndSet("New Value");
      
    5. updateAndGetgetAndUpdate

      • 通过函数更新引用。
      atomicRef.updateAndGet(s -> s + "!");
      
  • 示例

    import java.util.concurrent.atomic.AtomicReference;
    
    public class AtomicReferenceExample {
        private AtomicReference<String> atomicString = new AtomicReference<>("Hello");
    
        public void updateString(String newValue) {
            boolean updated = atomicString.compareAndSet("Hello", newValue);
            if (updated) {
                System.out.println("String updated to: " + newValue);
            } else {
                System.out.println("Update failed.");
            }
        }
    
        public String getString() {
            return atomicString.get();
        }
    
        public static void main(String[] args) {
            AtomicReferenceExample example = new AtomicReferenceExample();
            example.updateString("World"); // String updated to: World
            example.updateString("Java");  // Update failed.
            System.out.println(example.getString()); // World
        }
    }
    

注意

  • CAS操作的ABA问题:在某些场景下,AtomicReference的CAS操作可能会遇到ABA问题,即对象从A变为B再变回A,导致CAS成功但实际状态已变化。可以使用AtomicStampedReference来解决。

六、Fork/Join框架

24. 什么是Fork/Join框架?它的主要组件是什么?

回答:

  • Fork/Join框架

    • 是Java 7引入的一个框架,旨在利用多核处理器的优势,通过任务分解(Fork)和结果合并(Join)来并行处理大任务。
    • 适用于可以递归分解为更小任务的计算密集型任务,如排序、矩阵运算等。
  • 主要组件

    1. ForkJoinPool

      • java.util.concurrent包下的一个特殊线程池,专门用于执行ForkJoinTask
      • 使用工作窃取(Work Stealing)算法,允许空闲线程从繁忙线程的任务队列中窃取任务,提高资源利用率。
    2. ForkJoinTask<V>

      • 是所有Fork/Join任务的基类,定义了任务的基本操作。
      • 两个主要的子类:
        • RecursiveAction:不返回结果的任务。
        • RecursiveTask<V>:返回结果的任务。
    3. 工作窃取算法(Work Stealing):

      • 当一个线程的任务队列为空时,它可以从其他线程的队列中窃取任务,保持线程池的活跃性和高效性。
  • 工作流程

    1. 将大任务分解为小任务(Fork)。
    2. 小任务被提交到ForkJoinPool的工作线程执行。
    3. 等待所有小任务完成(Join)。
    4. 合并小任务的结果,得到最终结果。
  • 示例

    import java.util.concurrent.ForkJoinPool;
    import java.util.concurrent.RecursiveTask;
    
    public class ForkJoinExample {
        // 计算数组元素的和
        static class SumTask extends RecursiveTask<Long> {
            private static final int THRESHOLD = 1000;
            private final int[] array;
            private final int start;
            private final int end;
    
            public SumTask(int[] array, int start, int end) {
                this.array = array;
                this.start = start;
                this.end = end;
            }
    
            @Override
            protected Long compute() {
                if (end - start <= THRESHOLD) {
                    long 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(); // 异步执行左半部分
                    long rightResult = rightTask.compute(); // 同步执行右半部分
                    long leftResult = leftTask.join(); // 等待左半部分结果
                    return leftResult + rightResult;
                }
            }
        }
    
        public static void main(String[] args) {
            int[] array = new int[10000];
            for (int i = 0; i < array.length; i++) {
                array[i] = i + 1;
            }
    
            ForkJoinPool pool = new ForkJoinPool();
            SumTask task = new SumTask(array, 0, array.length);
            long result = pool.invoke(task);
            System.out.println("Sum: " + result); // 输出: Sum: 50005000
        }
    }
    

注意

  • Fork/Join框架适用于计算密集型任务,不适用于IO密集型任务。
  • 合理划分任务的粒度,避免任务过小导致过多的任务调度开销,或任务过大导致并行度不足。

25. Fork/Join框架的工作窃取(Work Stealing)算法是如何实现的?

回答:

  • 工作窃取(Work Stealing)算法

    • 是Fork/Join框架中提高并发性能和资源利用率的关键机制。
    • 主要思想是让空闲的线程主动从其他繁忙线程的任务队列中“窃取”任务,以保持线程池中所有线程的活跃性。
  • 实现原理

    1. 双端队列(Deque):

      • 每个工作线程拥有一个双端队列,用于存储其任务。
      • 线程以LIFO(后进先出)的方式从队列的尾部提交和执行任务。
    2. 任务提交和执行:

      • 当一个线程创建子任务时,会将子任务“fork”到其自己的任务队列的尾部。
      • 线程会优先从自己的队列的尾部获取任务执行。
    3. 任务窃取:

      • 当一个线程的任务队列为空时,它会尝试从其他线程的队列的头部窃取任务。
      • 窃取的任务是其他线程最早提交的任务,有助于保持任务的分布均匀。
    4. 避免竞争:

      • 由于一个线程只从自己队列的尾部提交和获取任务,而窃取线程从其他线程的头部窃取任务,减少了并发访问同一队列的冲突。
      • 使用无锁的算法和CAS操作实现高效的队列操作。
  • 优势

    • 高效的任务分配:通过工作窃取,确保所有线程都能持续工作,避免空闲。
    • 负载均衡:自动分配任务到空闲线程,平衡各线程的负载。
    • 提高资源利用率:最大化利用多核处理器的并行能力。
  • 示例

    • 在前述的ForkJoinExample中,ForkJoinPool内部采用了工作窃取算法,自动管理任务的分配和执行。

注意

  • 任务粒度:合理划分任务的粒度,避免任务过大或过小影响工作窃取效率。
  • 避免死锁:确保任务的分解和合并不会导致线程间的死锁。

七、Java 8新特性与并发

26. 什么是CompletableFuture?它如何改进了异步编程?

回答:

  • CompletableFuture

    • 是Java 8引入的java.util.concurrent包下的一个类,代表了一个可以在未来完成的异步计算。
    • 提供了丰富的API来处理异步操作的结果,如回调、组合、异常处理等。
    • 实现了CompletionStage接口,支持链式调用和函数式编程。
  • 改进点

    1. 灵活的回调机制:

      • 提供了多种回调方法,如thenApplythenAcceptthenRun等,简化了异步操作的处理。
    2. 任务组合:

      • 支持任务的组合执行,如thenCombinethenComposeallOfanyOf等,方便构建复杂的异步流程。
    3. 异常处理:

      • 提供了exceptionallyhandle等方法,简化了异步任务中的异常处理。
    4. 非阻塞:

      • 通过异步执行,不阻塞主线程,提高资源利用率。
    5. 与Stream API集成:

      • 可以与Java 8的Stream API结合,构建高效的并行和异步处理流程。
  • 示例

    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.ExecutionException;
    
    public class CompletableFutureExample {
        public static void main(String[] args) throws InterruptedException, ExecutionException {
            // 创建一个异步任务
            CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "Hello";
            });
    
            // 处理异步任务的结果
            CompletableFuture<String> greetingFuture = future.thenApply(name -> name + ", World!");
    
            // 打印结果
            System.out.println(greetingFuture.get()); // 输出: Hello, World!
        }
    }
    

注意

  • 线程池选择CompletableFuture默认使用ForkJoinPool.commonPool(),可通过CompletableFuture.supplyAsync的第二个参数指定自定义线程池。
  • 异常处理:需要正确处理异步任务中的异常,避免未捕获的异常影响程序。

27. Stream API如何与并发编程结合使用?请举例说明。

回答:

  • Stream API与并发编程的结合

    • Java 8的Stream API提供了对集合数据的高效处理方式,包括串行流和并行流。
    • 并行流(Parallel Streams):通过利用多核处理器,实现数据的并行处理,提升性能。
    • 内部通过ForkJoinPool执行任务,自动分解和合并任务。
  • 使用并行流的示例

    import java.util.Arrays;
    import java.util.List;
    
    public class ParallelStreamExample {
        public static void main(String[] args) {
            List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    
            // 串行流
            long startTime = System.currentTimeMillis();
            long sumSerial = numbers.stream()
                                    .mapToLong(Integer::longValue)
                                    .sum();
            long endTime = System.currentTimeMillis();
            System.out.println("Serial Sum: " + sumSerial + ", Time: " + (endTime - startTime) + "ms");
    
            // 并行流
            startTime = System.currentTimeMillis();
            long sumParallel = numbers.parallelStream()
                                      .mapToLong(Integer::longValue)
                                      .sum();
            endTime = System.currentTimeMillis();
            System.out.println("Parallel Sum: " + sumParallel + ", Time: " + (endTime - startTime) + "ms");
        }
    }
    
  • 示例说明

    • 串行流:按顺序处理元素,适用于小规模数据或不需要并行的场景。
    • 并行流:自动将数据分解为多个子任务,利用多线程并行处理,适用于大规模数据和计算密集型任务。
  • 注意事项

    1. 线程安全:

      • 确保在并行流中操作的数据结构是线程安全的,或避免共享可变状态。
    2. 性能评估:

      • 并行流并不总是比串行流快,取决于数据规模、任务复杂度和硬件资源。
    3. 可分性:

      • 数据处理任务应具备良好的可分性,才能充分利用并行流的优势。
    4. 线程池限制:

      • 并行流默认使用ForkJoinPool.commonPool(),在某些场景下可能需要自定义线程池。
  • 高级示例:并行流结合自定义线程池

    import java.util.Arrays;
    import java.util.List;
    import java.util.concurrent.ForkJoinPool;
    
    public class CustomThreadPoolParallelStream {
        public static void main(String[] args) throws InterruptedException {
            List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
            
            ForkJoinPool customThreadPool = new ForkJoinPool(4); // 创建自定义线程池
            
            try {
                customThreadPool.submit(() -> {
                    long sum = numbers.parallelStream()
                                      .mapToLong(Integer::longValue)
                                      .sum();
                    System.out.println("Parallel Sum with Custom ThreadPool: " + sum);
                }).get();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                customThreadPool.shutdown();
            }
        }
    }
    

总结Stream API通过并行流提供了简洁、高效的并行数据处理方式,但需要注意线程安全和性能评估,合理选择使用场景。

28. 什么是Phaser?它与CountDownLatchCyclicBarrier有什么区别?

回答:

  • Phaser

    • java.util.concurrent包下的一个同步工具类,支持可变数量的参与者。
    • 提供比CountDownLatchCyclicBarrier更灵活的同步机制,适用于多阶段的同步控制。
    • 允许动态注册和注销参与者,适应参与者数量的变化。
  • CountDownLatchCyclicBarrier的区别

    1. CountDownLatch

      • 用于等待一组线程完成操作。
      • 计数器只允许减到零一次,不能重用。
      • 参与者数量固定,不能动态变化。
      CountDownLatch latch = new CountDownLatch(3);
      
    2. CyclicBarrier

      • 用于让一组线程互相等待,直到所有线程都达到某个共同点。
      • 计数器可以重用,适用于多个周期性的同步点。
      • 参与者数量固定。
      CyclicBarrier barrier = new CyclicBarrier(3);
      
    3. Phaser

      • 综合了CountDownLatchCyclicBarrier的特性,支持多个阶段的同步。
      • 允许动态注册和注销参与者,参与者数量可以在运行时变化。
      • 支持灵活的阶段控制,如分阶段的任务执行。
      Phaser phaser = new Phaser(1); // 初始注册1个参与者
      
  • 使用场景

    • CountDownLatch:等待固定数量的线程完成初始化或某个任务。
    • CyclicBarrier:在多个线程之间同步执行多个阶段的任务。
    • Phaser:在复杂的多阶段、多动态参与者的同步场景下使用。
  • 示例

    Phaser的基本使用

    import java.util.concurrent.Phaser;
    
    public class PhaserExample {
        public static void main(String[] args) {
            Phaser phaser = new Phaser(1); // 主线程注册
    
            for (int i = 0; i < 3; i++) {
                final int taskId = i;
                phaser.register(); // 动态注册参与者
                new Thread(() -> {
                    System.out.println("Task " + taskId + " is running.");
                    phaser.arriveAndAwaitAdvance(); // 到达并等待其他参与者
                    System.out.println("Task " + taskId + " is completed.");
                    phaser.arriveAndDeregister(); // 到达并注销
                }).start();
            }
    
            phaser.arriveAndAwaitAdvance(); // 主线程等待所有参与者到达
            System.out.println("All tasks are completed.");
            phaser.arriveAndDeregister(); // 主线程注销
        }
    }
    

输出

Task 0 is running.
Task 1 is running.
Task 2 is running.
All tasks are completed.
Task 0 is completed.
Task 1 is completed.
Task 2 is completed.

总结Phaser提供了灵活的多阶段同步机制,适用于复杂的并发任务控制,相比于CountDownLatchCyclicBarrier更具扩展性和灵活性。


八、阻塞队列与生产者-消费者

29. 解释生产者-消费者模型,并说明如何使用BlockingQueue实现它。

回答:

  • 生产者-消费者模型

    • 是一种经典的多线程设计模式,涉及两个角色:生产者和消费者。
    • 生产者负责生成数据或任务,并将其放入共享的缓冲区(队列)。
    • 消费者从缓冲区中取出数据或任务,并进行处理。
    • 目标:解耦生产者和消费者,使其能够独立运行,且在速度不匹配时保持系统稳定。
  • 使用BlockingQueue实现生产者-消费者模型

    步骤

    1. 选择合适的BlockingQueue实现:如ArrayBlockingQueueLinkedBlockingQueue等,根据需求选择有界或无界队列。
    2. 创建生产者和消费者线程
      • 生产者:生成任务,调用put()方法将任务放入队列。
      • 消费者:调用take()方法从队列中取出任务并处理。
    3. 启动线程
      • 启动多个生产者和消费者线程,实现并发生产和消费。
  • 示例

    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.BlockingQueue;
    
    public class ProducerConsumerDemo {
        public static void main(String[] args) {
            BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5); // 有界队列
    
            // 生产者
            Runnable producer = () -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        queue.put(i);
                        System.out.println("Produced: " + i);
                        Thread.sleep(100); // 模拟生产时间
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            };
    
            // 消费者
            Runnable consumer = () -> {
                for (int i = 0; i < 10; i++) {
                    try {
                        int value = queue.take();
                        System.out.println("Consumed: " + value);
                        Thread.sleep(150); // 模拟消费时间
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            };
    
            new Thread(producer).start();
            new Thread(consumer).start();
        }
    }
    

输出

Produced: 0
Consumed: 0
Produced: 1
Produced: 2
Consumed: 1
Produced: 3
Produced: 4
Consumed: 2
Produced: 5
Consumed: 3
Produced: 6
Produced: 7
Consumed: 4
Produced: 8
Consumed: 5
Produced: 9
Consumed: 6
Consumed: 7
Consumed: 8
Consumed: 9

注意

  • 队列容量:有界队列可以防止生产者过快生成任务导致内存溢出;无界队列可以防止生产者阻塞,但可能导致内存占用过高。
  • 线程同步BlockingQueue内部已实现线程同步,生产者和消费者无需显式同步。

30. 什么是LinkedBlockingQueueArrayBlockingQueue?它们的区别是什么?

回答:

  • LinkedBlockingQueue

    • 基于链表实现的阻塞队列。
    • 可以是有界队列或无界队列。
    • 默认容量为Integer.MAX_VALUE(无界)。
    • 适用于生产者和消费者速度不匹配且需要高吞吐量的场景。

    特点

    • 支持公平性,可以设置为FIFO顺序。
    • 内部通过两个ReentrantLock锁控制puttake操作,提高并发性能。

    创建方式

    // 无界
    BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
    
    // 有界
    BlockingQueue<Integer> boundedQueue = new LinkedBlockingQueue<>(100);
    
  • ArrayBlockingQueue

    • 基于数组实现的有界阻塞队列。
    • 需要在创建时指定固定的容量。
    • 适用于需要严格控制队列大小的场景,如限流、资源控制等。

    特点

    • FIFO顺序。
    • 内部使用一个ReentrantLock锁控制所有操作,简单但锁竞争可能更高。

    创建方式

    BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(100);
    
  • 主要区别

    特性LinkedBlockingQueueArrayBlockingQueue
    内部结构链表数组
    队列容量可有界或无界(默认无界)有界,必须在创建时指定容量
    并发性能双锁机制,读写锁分离,较高的并发性能单锁机制,锁竞争可能更高
    是否允许null元素不允许不允许
    FIFO保证
    公平性可配置(默认非公平)可配置(默认非公平)
    适用场景生产者和消费者速度不匹配,高吞吐量场景需要严格控制队列大小,限流、资源控制等场景

选择建议

  • LinkedBlockingQueue:适用于生产者和消费者速度不匹配、需要高吞吐量的场景。
  • ArrayBlockingQueue:适用于需要严格控制队列容量、限流和资源控制的场景。

九、同步工具类

31. 解释CountDownLatch及其使用场景。

回答:

  • CountDownLatch

    • java.util.concurrent包下的一个同步工具类,允许一个或多个线程等待,直到其他线程完成一组操作。
    • 通过一个计数器控制,计数器的初始值由创建时指定,每调用一次countDown()方法,计数器减1。
    • 当计数器达到零时,等待的线程被唤醒。
  • 使用场景

    1. 等待多个线程完成初始化:

      • 主线程等待多个子线程完成初始化工作后继续执行。
    2. 并行测试:

      • 在测试中,等待多个测试步骤完成后进行断言或结果验证。
    3. 任务同步:

      • 多个任务完成后,触发后续的操作。
  • 特点

    • 一次性使用CountDownLatch不能重用,计数器只能从初始值减到零一次。
    • 不可中断:等待线程可以被中断,抛出InterruptedException
  • 示例:等待多个线程完成任务后继续执行

    import java.util.concurrent.CountDownLatch;
    
    public class CountDownLatchExample {
        public static void main(String[] args) throws InterruptedException {
            int numberOfWorkers = 3;
            CountDownLatch latch = new CountDownLatch(numberOfWorkers);
    
            // 创建并启动工作线程
            for (int i = 0; i < numberOfWorkers; i++) {
                final int workerId = i;
                new Thread(() -> {
                    System.out.println("Worker " + workerId + " started.");
                    try {
                        Thread.sleep(1000 + workerId * 500); // 模拟工作
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                    System.out.println("Worker " + workerId + " finished.");
                    latch.countDown(); // 完成任务,计数器减1
                }).start();
            }
    
            // 主线程等待所有工作线程完成
            System.out.println("Main thread waiting for workers to finish.");
            latch.await();
            System.out.println("All workers finished. Main thread proceeding.");
        }
    }
    

输出

Worker 0 started.
Worker 1 started.
Worker 2 started.
Main thread waiting for workers to finish.
Worker 0 finished.
Worker 1 finished.
Worker 2 finished.
All workers finished. Main thread proceeding.

注意

  • 一次性使用:如果需要重复使用同步工具,考虑使用CyclicBarrierPhaser
  • 防止线程泄漏:确保所有工作线程最终都会调用countDown(),否则主线程会一直等待。

32. 什么是CyclicBarrier?它如何与CountDownLatch不同?

回答:

  • CyclicBarrier

    • java.util.concurrent包下的一个同步工具类,允许一组线程互相等待,直到所有线程都达到某个共同的屏障点。
    • 通过一个计数器控制,计数器的初始值由创建时指定,每个线程调用await()方法后,计数器减1。
    • 当计数器达到零时,所有等待的线程被同时唤醒,并可以继续执行。
    • 计数器可以重用,因此CyclicBarrier是可循环使用的。
  • CountDownLatch的区别

    特性CountDownLatchCyclicBarrier
    重用性不能重用,一旦计数器到零后不可再次使用可重用,多次使用同一个屏障点
    参与者注册需要在创建时指定固定数量,不能动态变化可以动态注册和注销参与者(Java 9及以上)
    额外动作不支持,当计数器到零后无额外动作支持,当所有线程到达屏障点后可以执行一个回调
    使用场景等待固定数量的线程完成某个操作同步多个线程在某个屏障点
  • 使用场景

    1. 多阶段任务同步:

      • 一组线程需要在多个阶段同步执行,如分阶段计算、分阶段汇总等。
    2. 集体行动:

      • 需要一组线程在共同的屏障点等待,协调集体行动的场景。
    3. 测试同步:

      • 测试中,模拟多个线程同时到达某个点。
  • 示例

    import java.util.concurrent.BrokenBarrierException;
    import java.util.concurrent.CyclicBarrier;
    
    public class CyclicBarrierExample {
        public static void main(String[] args) {
            int numberOfParties = 3;
            CyclicBarrier barrier = new CyclicBarrier(numberOfParties, () -> {
                System.out.println("All parties have arrived at the barrier. Proceeding...");
            });
    
            Runnable task = () -> {
                try {
                    String threadName = Thread.currentThread().getName();
                    System.out.println(threadName + " is performing some work.");
                    Thread.sleep((long) (Math.random() * 1000));
                    System.out.println(threadName + " has arrived at the barrier.");
                    barrier.await(); // 等待其他线程
                    System.out.println(threadName + " is proceeding after the barrier.");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            };
    
            // 启动线程
            for (int i = 0; i < numberOfParties; i++) {
                new Thread(task, "Thread-" + i).start();
            }
    
            // 重用屏障
            new Thread(task, "Thread-3").start();
            new Thread(task, "Thread-4").start();
            new Thread(task, "Thread-5").start();
        }
    }
    

输出

Thread-0 is performing some work.
Thread-1 is performing some work.
Thread-2 is performing some work.
Thread-1 has arrived at the barrier.
Thread-0 has arrived at the barrier.
Thread-2 has arrived at the barrier.
All parties have arrived at the barrier. Proceeding...
Thread-1 is proceeding after the barrier.
Thread-0 is proceeding after the barrier.
Thread-2 is proceeding after the barrier.
Thread-3 is performing some work.
Thread-4 is performing some work.
Thread-5 is performing some work.
Thread-3 has arrived at the barrier.
Thread-4 has arrived at the barrier.
Thread-5 has arrived at the barrier.
All parties have arrived at the barrier. Proceeding...
Thread-3 is proceeding after the barrier.
Thread-5 is proceeding after the barrier.
Thread-4 is proceeding after the barrier.

注意

  • 异常处理:在使用CyclicBarrier时,需处理BrokenBarrierExceptionInterruptedException
  • 可重用性:在屏障被释放后,可以再次使用同一个CyclicBarrier进行同步。

33. 什么是Semaphore?它的常见应用场景是什么?

回答:

  • Semaphore

    • java.util.concurrent包下的一个同步工具类,基于信号量的概念。
    • 控制同时访问特定资源的线程数量。
    • 信号量维护一个计数器,表示可用的许可证数量。
  • 工作原理

    • 许可获取:

      • 线程调用acquire()方法获取一个许可证,如果许可证数量为零,线程会被阻塞,直到有许可证可用。
    • 许可释放:

      • 线程调用release()方法释放一个许可证,唤醒被阻塞的线程。
    • 计数器:

      • 初始许可数量由构造方法指定。
      • Semaphore可以是公平的(按照线程请求许可证的顺序)或非公平的(允许线程抢占许可证)。
  • 应用场景

    1. 资源限制:

      • 控制对有限资源(如数据库连接、文件句柄)的并发访问数量。
    2. 流量控制:

      • 限制系统的并发请求量,防止过载。
    3. 信号控制:

      • 实现线程间的信号传递,如事件触发、同步操作等。
    4. 实现生产者-消费者模式:

      • 控制生产者和消费者的并发关系,确保数据一致性。
  • 示例

    import java.util.concurrent.Semaphore;
    
    public class SemaphoreExample {
        public static void main(String[] args) {
            // 创建一个Semaphore,最多允许3个线程同时访问
            Semaphore semaphore = new Semaphore(3);
    
            Runnable task = () -> {
                String threadName = Thread.currentThread().getName();
                try {
                    System.out.println(threadName + " is attempting to acquire a permit.");
                    semaphore.acquire(); // 获取许可证
                    System.out.println(threadName + " has acquired a permit.");
                    Thread.sleep(2000); // 模拟工作
                    System.out.println(threadName + " is releasing a permit.");
                    semaphore.release(); // 释放许可证
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            };
    
            // 启动5个线程
            for (int i = 0; i < 5; i++) {
                new Thread(task, "Thread-" + i).start();
            }
        }
    }
    

输出

Thread-0 is attempting to acquire a permit.
Thread-0 has acquired a permit.
Thread-1 is attempting to acquire a permit.
Thread-1 has acquired a permit.
Thread-2 is attempting to acquire a permit.
Thread-2 has acquired a permit.
Thread-3 is attempting to acquire a permit.
Thread-4 is attempting to acquire a permit.
Thread-0 is releasing a permit.
Thread-3 has acquired a permit.
Thread-1 is releasing a permit.
Thread-4 has acquired a permit.
Thread-2 is releasing a permit.
Thread-3 is releasing a permit.
Thread-4 is releasing a permit.

注意

  • 公平性Semaphore可以通过构造方法设置为公平模式(按照线程请求顺序获取许可证)。

    Semaphore fairSemaphore = new Semaphore(3, true);
    
  • 许可证管理:确保每次acquire()对应一次release(),避免许可证泄漏。


十、线程安全与同步

34. 什么是volatile关键字?它如何确保变量的可见性?

回答:

  • volatile关键字

    • 是Java中的一个修饰符,用于声明变量的可见性和禁止指令重排序。
    • 适用于变量在多线程环境下被多个线程读取和写入,但不涉及复合操作(如自增)。
  • 确保可见性

    • 内存可见性volatile变量的写操作会立即刷新到主内存,读操作会从主内存读取最新的值。
    • 禁止指令重排序:编译器和处理器不会对volatile变量的读写操作进行重排序,保证程序执行的顺序性。
  • 适用场景

    • 标记位(flag),用于控制线程的停止或启动。
    • 单一读写操作的共享变量,不涉及复合操作。
  • 示例

    public class VolatileExample {
        private volatile boolean flag = false;
    
        public void setFlag() {
            flag = true;
        }
    
        public void checkFlag() {
            while (!flag) {
                // 等待flag被设置为true
            }
            System.out.println("Flag is true, proceeding...");
        }
    
        public static void main(String[] args) {
            VolatileExample example = new VolatileExample();
    
            // 线程A:检查flag
            new Thread(() -> example.checkFlag(), "Thread-A").start();
    
            // 线程B:设置flag
            new Thread(() -> {
                try {
                    Thread.sleep(1000); // 模拟工作
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                example.setFlag();
                System.out.println("Flag has been set to true.");
            }, "Thread-B").start();
        }
    }
    

输出

Flag has been set to true.
Flag is true, proceeding...

注意

  • 不适用于复合操作volatile不能保证复合操作(如count++)的原子性,需结合原子变量或锁机制。
  • 性能影响volatile会引入一定的内存屏障,可能影响性能,应合理使用。

35. 解释乐观锁和悲观锁的区别。Java中如何实现乐观锁?

回答:

  • 乐观锁(Optimistic Lock)

    • 假设多个线程不会同时修改同一资源,允许并发访问。
    • 在提交修改时,检查是否有其他线程已修改资源,如果有,则回滚或重试。
    • 不阻塞线程,适用于读多写少的场景。
    • 常用实现方式:基于版本号的CAS(Compare-And-Swap)操作。
  • 悲观锁(Pessimistic Lock)

    • 假设多个线程可能会同时修改同一资源,阻塞其他线程的访问。
    • 通过加锁机制,确保同一时间只有一个线程能修改资源。
    • 适用于写操作频繁或竞争激烈的场景。
    • 常用实现方式:synchronizedReentrantLock等显式锁。
  • 区别

    特性乐观锁悲观锁
    假设并发冲突较少,允许并发访问并发冲突较多,阻塞其他线程访问
    性能在读多写少场景下性能较高在高竞争场景下性能较低,存在锁竞争
    实现方式CAS操作,基于版本号检查显式加锁(synchronizedReentrantLock
    复杂性需要版本管理,处理回滚或重试逻辑相对简单,通过锁机制控制访问
  • Java中实现乐观锁

    使用AtomicReference实现简单的乐观锁

    import java.util.concurrent.atomic.AtomicReference;
    
    public class OptimisticLockExample {
        static class SharedResource {
            private final AtomicReference<Integer> value = new AtomicReference<>(0);
    
            public void increment() {
                while (true) {
                    Integer current = value.get();
                    Integer newValue = current + 1;
                    if (value.compareAndSet(current, newValue)) {
                        break;
                    }
                    // CAS失败,重试
                }
            }
    
            public int getValue() {
                return value.get();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            SharedResource resource = new SharedResource();
    
            Runnable task = () -> {
                for (int i = 0; i < 1000; i++) {
                    resource.increment();
                }
            };
    
            Thread t1 = new Thread(task);
            Thread t2 = new Thread(task);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
    
            System.out.println("Final value: " + resource.getValue()); // 输出: Final value: 2000
        }
    }
    

注意

  • 适用场景:乐观锁适用于读多写少、冲突概率低的场景。
  • 复杂性:实现乐观锁需要管理版本号和处理冲突回滚,逻辑相对复杂。

36. 如何避免死锁?请举例说明。

回答:

  • 死锁

    • 是指两个或多个线程在执行过程中,因为争夺资源而造成互相等待,导致程序无法继续执行。
  • 避免死锁的方法

    1. 避免嵌套锁:

      • 尽量减少持有多个锁的情况,避免线程在持有一个锁的同时申请另一个锁。
    2. 锁的顺序:

      • 所有线程获取多个锁时,遵循相同的锁获取顺序,避免循环等待。
    3. 使用定时锁:

      • 使用带有超时机制的锁获取方法,如tryLock(long time, TimeUnit unit),避免无限等待。
    4. 使用LocktryLock方法:

      • 通过尝试获取锁,如果无法获取,则放弃并采取其他措施,避免进入等待状态。
    5. 减少锁的粒度:

      • 尽量细化锁的范围,减少锁的持有时间,降低锁竞争和死锁风险。
    6. 使用锁的层次结构:

      • 设定锁的层次结构,确保线程按照层次顺序获取锁,避免循环依赖。
  • 示例:死锁的场景与避免方法

    死锁示例

    public class DeadlockDemo {
        private final Object lock1 = new Object();
        private final Object lock2 = new Object();
    
        public void methodA() {
            synchronized (lock1) {
                System.out.println("Thread A acquired lock1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                synchronized (lock2) {
                    System.out.println("Thread A acquired lock2");
                }
            }
        }
    
        public void methodB() {
            synchronized (lock2) {
                System.out.println("Thread B acquired lock2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                synchronized (lock1) {
                    System.out.println("Thread B acquired lock1");
                }
            }
        }
    
        public static void main(String[] args) {
            DeadlockDemo demo = new DeadlockDemo();
            new Thread(demo::methodA).start();
            new Thread(demo::methodB).start();
        }
    }
    

    输出

    Thread A acquired lock1
    Thread B acquired lock2
    // 线程A等待获取lock2,线程B等待获取lock1,形成死锁
    

    避免死锁

    通过锁顺序避免

    public class DeadlockAvoidance {
        private final Object lock1 = new Object();
        private final Object lock2 = new Object();
    
        public void methodA() {
            synchronized (lock1) {
                System.out.println("Thread A acquired lock1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                synchronized (lock2) {
                    System.out.println("Thread A acquired lock2");
                }
            }
        }
    
        public void methodB() {
            synchronized (lock1) { // 按相同顺序获取锁
                System.out.println("Thread B acquired lock1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                synchronized (lock2) {
                    System.out.println("Thread B acquired lock2");
                }
            }
        }
    
        public static void main(String[] args) {
            DeadlockAvoidance demo = new DeadlockAvoidance();
            new Thread(demo::methodA).start();
            new Thread(demo::methodB).start();
        }
    }
    

    输出

    Thread A acquired lock1
    Thread B acquired lock1 // 线程B等待锁1释放
    Thread A acquired lock2
    Thread B acquired lock2
    

总结:通过合理的锁设计和同步策略,可以有效避免死锁的发生,确保多线程程序的稳定性和可靠性。

37. 解释ReadWriteLock的工作机制,并说明它的优势。

回答:

  • ReadWriteLock

    • java.util.concurrent.locks包下的一个接口,定义了读写锁的行为。
    • 允许多个线程同时读取共享资源,但在写入时独占访问,保证数据一致性。
  • 工作机制

    1. 读锁(Read Lock):

      • 多个线程可以同时持有读锁,只要没有线程持有写锁。
      • 适用于读多写少的场景,提高并发性能。
    2. 写锁(Write Lock):

      • 只有一个线程可以持有写锁,并且在持有写锁时,所有其他线程的读锁和写锁请求都会被阻塞。
      • 适用于需要独占访问的写操作,确保数据一致性。
    3. 锁获取顺序:

      • 当有线程持有写锁时,后续的读锁和写锁请求会被阻塞。
      • 当有线程持有读锁时,写锁请求会被阻塞,但新读锁请求可能被允许(取决于具体实现和配置)。
  • 优势

    1. 提高并发性能:

      • 在读多写少的场景下,允许多个线程并发读操作,减少锁竞争,提高性能。
    2. 数据一致性:

      • 写锁确保写操作的独占性,防止数据被同时修改,保证数据一致性。
    3. 灵活的锁控制:

      • 通过读锁和写锁的分离,提供更细粒度的锁控制,适应不同的访问需求。
  • 实现类

    • ReentrantReadWriteLock

      • 提供了可重入的读写锁实现,支持公平锁和非公平锁。
    • StampedLock

      (Java 8引入):

      • 提供了读写锁的另一种实现,支持乐观读锁,适用于高并发读写场景。
  • 示例

    import java.util.concurrent.locks.ReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    public class ReadWriteLockExample {
        private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
        private int count = 0;
    
        public void write() {
            rwLock.writeLock().lock();
            try {
                count++;
                System.out.println(Thread.currentThread().getName() + " incremented count to " + count);
            } finally {
                rwLock.writeLock().unlock();
            }
        }
    
        public void read() {
            rwLock.readLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + " read count as " + count);
            } finally {
                rwLock.readLock().unlock();
            }
        }
    
        public static void main(String[] args) {
            ReadWriteLockExample example = new ReadWriteLockExample();
    
            // 创建读线程
            Runnable readTask = () -> {
                for (int i = 0; i < 5; i++) {
                    example.read();
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            };
    
            // 创建写线程
            Runnable writeTask = () -> {
                for (int i = 0; i < 5; i++) {
                    example.write();
                    try {
                        Thread.sleep(150);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            };
    
            // 启动线程
            new Thread(readTask, "Reader-1").start();
            new Thread(readTask, "Reader-2").start();
            new Thread(writeTask, "Writer-1").start();
        }
    }
    

输出

Reader-1 read count as 0
Reader-2 read count as 0
Writer-1 incremented count to 1
Reader-1 read count as 1
Reader-2 read count as 1
Writer-1 incremented count to 2
Reader-1 read count as 2
Reader-2 read count as 2
Writer-1 incremented count to 3
Reader-1 read count as 3
Reader-2 read count as 3
Writer-1 incremented count to 4
Reader-1 read count as 4
Reader-2 read count as 4
Writer-1 incremented count to 5

注意

  • 公平性ReentrantReadWriteLock支持公平锁和非公平锁,通过构造器参数设置:

    ReadWriteLock rwLock = new ReentrantReadWriteLock(true); // 公平锁
    
  • 读锁和写锁的互斥:当写锁被持有时,所有读锁和写锁的获取请求都会被阻塞,直到写锁被释放。


十一、线程池的高级配置与优化

38. 如何自定义一个线程池?解释ThreadPoolExecutor构造函数的各个参数。

回答:

  • 自定义线程池

    • 通过直接创建ThreadPoolExecutor实例,传入自定义的参数,实现灵活的线程池配置。
    • 适用于需要精细控制线程池行为的场景,避免使用Executors工厂方法带来的潜在问题(如无限线程池、无界队列等)。
  • ThreadPoolExecutor构造函数

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
    
  • 参数详解

    1. corePoolSize(核心线程数)
      • 线程池中始终保持的最小线程数量。
      • 即使线程处于空闲状态,也不会被回收,除非调用allowCoreThreadTimeOut(true)
    2. maximumPoolSize(最大线程数)
      • 线程池中允许的最大线程数量。
      • 当任务队列已满且核心线程数已被占用时,线程池会创建新的线程,直到达到最大线程数。
    3. keepAliveTime(线程空闲保持时间)
      • 超过核心线程数的线程在空闲时保持的最长时间,超过后会被回收。
      • 单位由unit参数指定。
    4. unit(时间单位)
      • 指定keepAliveTime的时间单位,如TimeUnit.SECONDSTimeUnit.MILLISECONDS等。
    5. workQueue(任务队列)
      • 用于存储待执行任务的阻塞队列。
      • 常见实现类包括:
        • LinkedBlockingQueue:有界或无界的阻塞队列,适用于任务数不固定的场景。
        • ArrayBlockingQueue:有界的阻塞队列,适用于固定容量的任务队列。
        • SynchronousQueue:无缓冲的阻塞队列,每个插入操作必须等待对应的移除操作。
        • PriorityBlockingQueue:基于优先级堆的无界阻塞队列。
    6. threadFactory(线程工厂)
      • 用于创建新线程的工厂,允许自定义线程的创建方式,如设置线程名、守护线程等。
      • 通过Executors.defaultThreadFactory()或自定义实现。
    7. handler(拒绝策略)
      • 当线程池无法接受新任务时的处理策略。
      • 常见策略包括:
        • AbortPolicy(默认):抛出RejectedExecutionException
        • CallerRunsPolicy:由调用线程执行任务。
        • DiscardPolicy:静默丢弃任务。
        • DiscardOldestPolicy:丢弃队列中最旧的任务,尝试执行新任务。
  • 示例:自定义ThreadPoolExecutor

    import java.util.concurrent.*;
    
    public class CustomThreadPoolExecutor {
        public static void main(String[] args) {
            int corePoolSize = 5;
            int maximumPoolSize = 10;
            long keepAliveTime = 60;
            TimeUnit unit = TimeUnit.SECONDS;
            BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
            ThreadFactory threadFactory = Executors.defaultThreadFactory();
            RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
    
            ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler
            );
    
            // 提交任务
            for (int i = 0; i < 50; i++) {
                final int taskId = i;
                executor.execute(() -> {
                    System.out.println("Task " + taskId + " is running on " + Thread.currentThread().getName());
                    try {
                        Thread.sleep(1000); // 模拟任务执行
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
    
            // 关闭线程池
            executor.shutdown();
            try {
                if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                    executor.shutdownNow();
                }
            } catch (InterruptedException e) {
                executor.shutdownNow();
            }
        }
    }
    

注意

  • 合理配置参数:根据应用需求,合理配置核心线程数、最大线程数、任务队列大小等参数,避免资源浪费或系统过载。
  • 拒绝策略选择:根据任务的重要性和系统要求,选择合适的拒绝策略,避免任务丢失或系统异常。

39. 解释ForkJoinPoolThreadPoolExecutor的区别。

回答:

  • ForkJoinPool

    • 专为执行ForkJoinTask(如RecursiveTaskRecursiveAction)设计的线程池,实现了工作窃取(Work Stealing)算法。
    • 适用于分治算法和递归任务,利用多核处理器的优势,实现高效的并行计算。
    • 线程池中的线程通常为工作线程,专注于执行分解后的子任务。
    • 通过ForkJoinPool.commonPool()可以获取一个共享的全局线程池。
  • ThreadPoolExecutor

    • 是Java中最通用的线程池实现,支持多种任务类型和调度策略。
    • 适用于处理各种类型的任务,如IO密集型、计算密集型等。
    • 不具备工作窃取机制,适用于独立的任务执行。
    • 提供了丰富的构造器参数,允许自定义线程池行为。
  • 主要区别

    特性ForkJoinPoolThreadPoolExecutor
    任务类型专为ForkJoinTask设计,适用于分治和递归任务适用于各种类型的RunnableCallable任务
    工作窃取机制是,支持工作窃取(Work Stealing)否,基于提交队列的调度策略
    线程复用与创建使用工作线程,动态创建和复用线程通过核心线程数和最大线程数控制线程的创建与复用
    适用场景计算密集型、可分解的并行任务IO密集型、短期任务、固定任务量的场景
    提供的API特定于ForkJoinTask的API,如fork()join()通用的ExecutorService API
    线程池的特性内部使用工作窃取算法,优化多核处理器利用率提供灵活的任务队列和拒绝策略
  • 示例

    使用ForkJoinPool

    import java.util.concurrent.ForkJoinPool;
    import java.util.concurrent.RecursiveTask;
    
    public class ForkJoinVsThreadPool {
        // ForkJoinTask实现
        static class SumTask extends RecursiveTask<Long> {
            private static final int THRESHOLD = 1000;
            private final int[] array;
            private final int start;
            private final int end;
    
            public SumTask(int[] array, int start, int end) {
                this.array = array;
                this.start = start;
                this.end = end;
            }
    
            @Override
            protected Long compute() {
                if (end - start <= THRESHOLD) {
                    long sum = 0;
                    for (int i = start; i < end; i++) {
                        sum += array[i];
                    }
                    return sum;
                } else {
                    int mid = (start + end) / 2;
                    SumTask left = new SumTask(array, start, mid);
                    SumTask right = new SumTask(array, mid, end);
                    left.fork(); // 异步执行左半部分
                    long rightResult = right.compute(); // 同步执行右半部分
                    long leftResult = left.join(); // 等待左半部分结果
                    return leftResult + rightResult;
                }
            }
        }
    
        public static void main(String[] args) throws Exception {
            int[] array = new int[10000];
            for (int i = 0; i < array.length; i++) {
                array[i] = i + 1;
            }
    
            ForkJoinPool forkJoinPool = new ForkJoinPool();
            SumTask task = new SumTask(array, 0, array.length);
            long result = forkJoinPool.invoke(task);
            System.out.println("ForkJoinPool Sum: " + result); // 输出: ForkJoinPool Sum: 50005000
    
            // 使用ThreadPoolExecutor实现相同功能
            ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(4);
            Future<Long> future = executor.submit(() -> {
                long sum = 0;
                for (int num : array) {
                    sum += num;
                }
                return sum;
            });
            System.out.println("ThreadPoolExecutor Sum: " + future.get()); // 输出: ThreadPoolExecutor Sum: 50005000
            executor.shutdown();
        }
    }
    

总结ForkJoinPool适用于可分解的并行计算任务,利用工作窃取机制提升多核处理器的利用率;而ThreadPoolExecutor则更通用,适用于各种类型的并发任务。

40. 解释FutureCompletableFuture的区别。

回答:

  • Future<V>

    • java.util.concurrent包下的一个接口,表示异步计算的结果。

    • 提供了获取结果、取消任务、检查任务是否完成等方法。

    • 主要方法:

      • get():等待任务完成并获取结果。
      • cancel(boolean mayInterruptIfRunning):尝试取消任务。
      • isDone():检查任务是否完成。
      • isCancelled():检查任务是否被取消。
    • 限制:

      • 不支持链式调用和回调机制。
      • 一旦创建,不能手动完成结果。
      • 只适用于单一的异步结果处理。
  • CompletableFuture<V>

    • java.util.concurrent包下的一个类,实现了FutureCompletionStage接口,提供了更强大的异步编程能力。
    • 支持链式调用、回调、任务组合、异常处理等功能。
    • 提供了多种方法来构建和管理复杂的异步任务流程。
    • 主要方法
      • supplyAsync(Supplier<U> supplier):异步执行提供者的任务,并返回结果。
      • thenApply(Function<? super T, ? extends U> fn):在任务完成后,应用函数处理结果。
      • thenAccept(Consumer<? super T> action):在任务完成后,接受结果执行操作。
      • thenRun(Runnable action):在任务完成后,执行操作,不使用结果。
      • exceptionally(Function<Throwable, ? extends T> fn):处理任务中的异常。
      • handle(BiFunction<? super T, Throwable, ? extends U> fn):同时处理结果和异常。
      • allOf(CompletableFuture<?>... cfs):等待所有给定的CompletableFuture完成。
      • anyOf(CompletableFuture<?>... cfs):等待任意一个CompletableFuture完成。
  • 区别

    特性FutureCompletableFuture
    继承/实现接口类,实现了FutureCompletionStage接口
    回调机制不支持支持多种回调和链式调用方法
    任务组合不支持支持组合多个异步任务,如thenCombinethenCompose
    手动完成不能手动完成(由线程池完成)支持手动完成,通过complete()completeExceptionally()方法
    异常处理通过get()抛出异常提供专门的异常处理方法,如exceptionally()handle()
    取消任务支持cancel()方法支持cancel()方法,但更多地用于任务的结果处理
  • 示例

    使用Future

    import java.util.concurrent.*;
    
    public class FutureExample {
        public static void main(String[] args) throws InterruptedException, ExecutionException {
            ExecutorService executor = Executors.newFixedThreadPool(2);
    
            Future<Integer> future = executor.submit(() -> {
                Thread.sleep(1000);
                return 42;
            });
    
            System.out.println("Result: " + future.get()); // 阻塞等待结果
            executor.shutdown();
        }
    }
    

    使用CompletableFuture

    import java.util.concurrent.CompletableFuture;
    
    public class CompletableFutureExample {
        public static void main(String[] args) throws InterruptedException {
            CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                return 42;
            });
    
            future.thenApply(result -> "Result is " + result)
                  .thenAccept(System.out::println);
    
            // 主线程等待异步任务完成
            Thread.sleep(2000);
        }
    }
    

总结CompletableFuture通过提供丰富的API和功能,极大地简化了异步编程,支持复杂的任务组合和结果处理;而Future则适用于简单的异步结果获取,但缺乏灵活性和功能。


十二、同步原语与信号量

41. 解释Semaphore的工作机制,并说明其常见用途。

回答:

  • Semaphore

    • java.util.concurrent包下的一个同步工具类,基于信号量(Semaphore)的概念。
    • 用于控制同时访问特定资源的线程数量。
  • 工作机制

    1. 许可(Permit):

      • Semaphore维护一个许可计数器,表示可以同时访问的线程数量。
      • 初始许可数量在创建时指定。
    2. 获取许可:

      • 线程调用acquire()方法获取一个许可,如果许可数量大于零,许可数量减1,线程继续执行。
      • 如果许可数量为零,线程会被阻塞,直到有许可可用。
    3. 释放许可:

      • 线程调用release()方法释放一个许可,许可数量加1,唤醒等待的线程。
    4. 公平性:

      • Semaphore可以设置为公平模式或非公平模式。
      • 公平模式下,线程按照请求许可的顺序获取许可。
      • 非公平模式下,线程可能会抢占许可,增加吞吐量。
  • 常见用途

    1. 限流:

      • 控制系统中并发访问某个资源(如数据库连接、API调用)的线程数量,防止过载。
    2. 资源池管理:

      • 管理有限数量的资源,如连接池、线程池中的任务槽。
    3. 实现信号:

      • 通过控制许可数量,实现线程间的信号传递,如同步多个线程的执行。
    4. 读写控制:

      • 在某些场景下,使用信号量控制读写线程的并发访问。
  • 示例:限流控制,最多允许3个线程同时访问资源

    import java.util.concurrent.Semaphore;
    
    public class SemaphoreExample {
        private static final int MAX_CONCURRENT_ACCESS = 3;
        private final Semaphore semaphore = new Semaphore(MAX_CONCURRENT_ACCESS);
    
        public void accessResource(String threadName) {
            try {
                System.out.println(threadName + " is attempting to acquire a permit.");
                semaphore.acquire(); // 获取许可
                System.out.println(threadName + " has acquired a permit.");
                // 模拟资源访问
                Thread.sleep(2000);
                System.out.println(threadName + " is releasing the permit.");
                semaphore.release(); // 释放许可
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    
        public static void main(String[] args) {
            SemaphoreExample example = new SemaphoreExample();
    
            Runnable task = () -> example.accessResource(Thread.currentThread().getName());
    
            // 启动5个线程,超过许可数量的线程会被阻塞
            for (int i = 0; i < 5; i++) {
                new Thread(task, "Thread-" + i).start();
            }
        }
    }
    

输出

Thread-0 is attempting to acquire a permit.
Thread-0 has acquired a permit.
Thread-1 is attempting to acquire a permit.
Thread-1 has acquired a permit.
Thread-2 is attempting to acquire a permit.
Thread-2 has acquired a permit.
Thread-3 is attempting to acquire a permit.
Thread-4 is attempting to acquire a permit.
Thread-0 is releasing the permit.
Thread-3 has acquired a permit.
Thread-1 is releasing the permit.
Thread-4 has acquired a permit.
Thread-2 is releasing the permit.
Thread-3 is releasing the permit.
Thread-4 is releasing the permit.

注意

  • 许可数量控制Semaphore通过许可数量限制并发访问,确保资源不会被过度占用。
  • 公平性选择:根据需求选择公平模式或非公平模式,平衡吞吐量和公平性。

42. 什么是CountDownLatch?它的使用场景是什么?

回答:

  • CountDownLatch

    • java.util.concurrent包下的一个同步工具类,允许一个或多个线程等待,直到其他线程完成一组操作。
    • 通过一个计数器控制,计数器的初始值由创建时指定,每调用一次countDown()方法,计数器减1。
    • 当计数器达到零时,等待的线程被唤醒。
  • 使用场景

    1. 等待多个线程完成初始化:

      • 主线程等待多个子线程完成初始化工作后继续执行。
    2. 并行测试:

      • 在测试中,等待多个测试步骤完成后进行断言或结果验证。
    3. 任务同步:

      • 多个任务完成后,触发后续的操作。
    4. 启动信号:

      • 控制多个线程在收到启动信号后同时开始执行。
  • 示例:等待多个线程完成任务后继续执行

    import java.util.concurrent.CountDownLatch;
    
    public class CountDownLatchExample {
        public static void main(String[] args) throws InterruptedException {
            int numberOfWorkers = 3;
            CountDownLatch latch = new CountDownLatch(numberOfWorkers);
    
            // 创建并启动工作线程
            for (int i = 0; i < numberOfWorkers; i++) {
                final int workerId = i;
                new Thread(() -> {
                    System.out.println("Worker " + workerId + " started.");
                    try {
                        Thread.sleep(1000 + workerId * 500); // 模拟工作
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                    System.out.println("Worker " + workerId + " finished.");
                    latch.countDown(); // 完成任务,计数器减1
                }).start();
            }
    
            // 主线程等待所有工作线程完成
            System.out.println("Main thread waiting for workers to finish.");
            latch.await();
            System.out.println("All workers finished. Main thread proceeding.");
        }
    }
    

输出

Worker 0 started.
Worker 1 started.
Worker 2 started.
Main thread waiting for workers to finish.
Worker 0 finished.
Worker 1 finished.
Worker 2 finished.
All workers finished. Main thread proceeding.

注意

  • 一次性使用CountDownLatch不能重用,计数器只能从初始值减到零一次。
  • 无法添加新的参与者:在初始化后,无法动态增加计数器。

十三、线程安全的设计模式

43. 什么是双重检查锁定(Double-Check Locking)?它如何在Java中实现?

回答:

  • 双重检查锁定(Double-Check Locking)

    • 是一种优化的同步机制,旨在减少在多线程环境下获取锁的开销。
    • 常用于单例模式的懒加载,实现线程安全的单例实例创建。
  • 工作原理

    1. 第一次检查:

      • 在同步块外检查实例是否已创建,如果已创建,则直接返回,避免获取锁的开销。
    2. 加锁和第二次检查:

      • 如果实例未创建,进入同步块,再次检查实例是否已创建。
      • 如果仍未创建,则创建实例。
  • 实现方式

    示例

    public class Singleton {
        private static volatile Singleton instance;
    
        private Singleton() {
            // 私有构造函数,防止外部实例化
        }
    
        public static Singleton getInstance() {
            if (instance == null) { // 第一次检查
                synchronized (Singleton.class) { // 加锁
                    if (instance == null) { // 第二次检查
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    
  • 关键点

    • volatile修饰:

      • instance变量必须声明为volatile,确保可见性和禁止指令重排序,防止创建部分初始化的实例。
    • 双重检查:

      • 同步块外和内部都进行实例检查,确保线程安全且提高性能。
  • 优缺点

    • 优点:

      • 高效,避免不必要的锁定操作。
      • 线程安全,确保单例实例的唯一性。
    • 缺点:

      • 实现复杂,易出错。
      • 需要volatile支持,Java 5及以上版本。

注意

  • 确保volatile:在Java 5及以上版本,通过volatile关键字修饰实例变量,确保双重检查锁定的正确性。

  • 替代方案:使用静态内部类或枚举实现单例,简化实现且天然线程安全。

    静态内部类示例

    public class Singleton {
        private Singleton() {
            // 私有构造函数
        }
    
        private static class SingletonHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
    
        public static Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
    

    枚举实现

    public enum Singleton {
        INSTANCE;
    
        public void someMethod() {
            // 方法实现
        }
    }
    

44. 解释ReadWriteLock的优势,并举例说明如何使用它。

回答:

  • ReadWriteLock的优势

    1. 提高并发性:

      • 允许多个线程同时读取共享资源,提升读操作的并发性能。
    2. 优化资源利用:

      • 当读操作远多于写操作时,使用ReadWriteLock可以减少读操作之间的阻塞,优化资源利用率。
    3. 数据一致性:

      • 写锁确保独占访问,防止数据被多个线程同时修改,保证数据一致性。
    4. 灵活的锁控制:

      • 通过读锁和写锁的分离,提供更细粒度的锁控制,适应不同的访问需求。
  • 使用示例:使用ReentrantReadWriteLock实现线程安全的共享资源访问

    import java.util.concurrent.locks.ReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    public class SharedData {
        private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
        private int data = 0;
    
        // 写操作
        public void write(int value) {
            rwLock.writeLock().lock();
            try {
                data = value;
                System.out.println(Thread.currentThread().getName() + " wrote data: " + data);
            } finally {
                rwLock.writeLock().unlock();
            }
        }
    
        // 读操作
        public int read() {
            rwLock.readLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + " read data: " + data);
                return data;
            } finally {
                rwLock.readLock().unlock();
            }
        }
    
        public static void main(String[] args) {
            SharedData sharedData = new SharedData();
    
            // 创建多个读线程
            Runnable readTask = () -> {
                for (int i = 0; i < 5; i++) {
                    sharedData.read();
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            };
    
            // 创建写线程
            Runnable writeTask = () -> {
                for (int i = 0; i < 5; i++) {
                    sharedData.write(i);
                    try {
                        Thread.sleep(150);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            };
    
            // 启动线程
            new Thread(readTask, "Reader-1").start();
            new Thread(readTask, "Reader-2").start();
            new Thread(writeTask, "Writer-1").start();
        }
    }
    

输出

Reader-1 read data: 0
Reader-2 read data: 0
Writer-1 wrote data: 0
Reader-1 read data: 0
Reader-2 read data: 0
Writer-1 wrote data: 1
Reader-1 read data: 1
Reader-2 read data: 1
Writer-1 wrote data: 2
Reader-1 read data: 2
Reader-2 read data: 2
Writer-1 wrote data: 3
Reader-1 read data: 3
Reader-2 read data: 3
Writer-1 wrote data: 4
Reader-1 read data: 4
Reader-2 read data: 4

注意

  • 锁降级:在读写锁的使用中,可以先获取写锁,然后获取读锁,最后释放写锁,实现锁的降级。
  • 避免锁升级:不要在持有读锁的情况下尝试获取写锁,以防死锁。

十四、总结与最佳实践

45. 什么是并发编程中的“锁粗化”(Lock Coarsening)?它的优势是什么?

回答:

  • 锁粗化(Lock Coarsening)

    • 是一种优化技术,旨在通过减少锁的获取和释放次数,降低锁操作的开销。
    • 将多个连续的锁操作合并为一个更大的锁操作,减少锁的粒度。
  • 工作原理

    • 当多个相邻的代码块需要获取同一把锁时,编译器或JIT优化器会将这些锁操作合并为一个更大的锁区域。
    • 这样可以减少频繁的锁获取和释放,提高性能。
  • 优势

    1. 减少锁操作开销:

      • 合并锁操作,减少锁获取和释放的次数,降低性能开销。
    2. 提高代码执行效率:

      • 通过减少同步操作的频率,提升代码的整体执行效率。
    3. 简化锁管理:

      • 通过锁粗化,避免锁的嵌套和频繁切换,提高锁管理的简洁性。
  • 示例

    未锁粗化的代码

    public class LockCoarseningExample {
        private final Object lock = new Object();
        private int count = 0;
    
        public void increment() {
            synchronized (lock) {
                count++;
            }
        }
    
        public void decrement() {
            synchronized (lock) {
                count--;
            }
        }
    
        public static void main(String[] args) {
            LockCoarseningExample example = new LockCoarseningExample();
            example.increment();
            example.decrement();
        }
    }
    

    锁粗化后的代码

    public class LockCoarseningExample {
        private final Object lock = new Object();
        private int count = 0;
    
        public void modify() {
            synchronized (lock) { // 合并多个锁操作为一个锁操作
                count++;
                count--;
            }
        }
    
        public static void main(String[] args) {
            LockCoarseningExample example = new LockCoarseningExample();
            example.modify();
        }
    }
    
  • 注意

    • 锁粗化通常由编译器或JIT优化器自动完成,开发者无需手动干预。
    • 在设计并发程序时,合理划分锁的粒度,结合锁粗化优化,提升程序性能。

46. 如何选择合适的并发集合类?

回答:

选择合适的并发集合类需要根据具体的应用场景、并发需求和性能要求来决定。以下是一些常见的选择标准和建议:

  • 集合类型

    1. Map接口

      • ConcurrentHashMap

        • 适用于高并发的键值对存储,支持快速读写。
        • 不允许null键和null值。
      • ConcurrentSkipListMap

        • 适用于需要有序键的高并发场景。
        • 支持范围查询和有序操作。
      • Collections.synchronizedMap(new HashMap<>())

        • 适用于低并发或小规模的线程安全需求。
        • 读写操作需要手动同步。
    2. List接口

      • CopyOnWriteArrayList

        • 适用于读多写少的场景,提供线程安全的List实现。
        • 写操作开销较大,不适用于频繁修改的场景。
      • Collections.synchronizedList(new ArrayList<>())

        • 适用于低并发或小规模的线程安全需求。
        • 需要在迭代时手动同步。
      • CopyOnWriteArraySet

        • 适用于读多写少的唯一元素集合。
    3. Queue接口

      • ConcurrentLinkedQueue

        • 适用于高并发的无界非阻塞队列。
        • 支持快速的并发读写操作。
      • LinkedBlockingQueue

        • 适用于生产者-消费者模型,支持有界或无界阻塞队列。
        • 支持线程阻塞等待任务的入队和出队。
      • ArrayBlockingQueue

        • 适用于需要固定容量的阻塞队列,控制资源的并发访问。
      • PriorityBlockingQueue

        • 适用于需要按优先级排序的阻塞队列。
      • SynchronousQueue

        • 适用于直接传递任务给消费者,常用于线程池的工作队列。
    4. Deque接口

      • ConcurrentLinkedDeque

        • 适用于高并发的无界双端队列。
      • LinkedBlockingDeque

        • 适用于需要阻塞双端队列的生产者-消费者模型。
  • 其他考虑因素

    • 有界 vs 无界:

      • 根据任务量和资源限制选择有界或无界的集合实现,避免资源耗尽或任务丢失。
    • 有序性:

      • 选择支持有序操作的集合,如ConcurrentSkipListMapPriorityBlockingQueue等,满足特定的顺序需求。
    • 性能需求:

      • 对于高并发读写,选择非阻塞的集合实现,如ConcurrentHashMapConcurrentLinkedQueue等。
      • 对于生产者-消费者模式,选择阻塞队列,如LinkedBlockingQueueArrayBlockingQueue等。
    • 线程安全性:

      • 确保所选集合类本身是线程安全的,或通过同步包装器进行保护。
  • 示例选择

    1. 高并发键值对存储,无需有序:

      • 使用ConcurrentHashMap
    2. 高并发键值对存储,需要有序:

      • 使用ConcurrentSkipListMap
    3. 读多写少的线程安全列表:

      • 使用CopyOnWriteArrayList
    4. 生产者-消费者模型,固定容量队列:

      • 使用ArrayBlockingQueue
    5. 需要按优先级处理的任务队列:

      • 使用PriorityBlockingQueue
    6. 高并发双端队列:

      • 使用ConcurrentLinkedDeque

总结:根据应用场景、并发需求和性能要求,选择最适合的并发集合类,以实现高效、线程安全的数据处理。

47. 什么是Exchanger?它的典型应用场景是什么?

回答:

  • Exchanger<V>

    • java.util.concurrent包下的一个同步工具类,用于在两个线程之间交换数据。
    • 每个参与交换的线程调用exchange(V x)方法,将自己的数据与对方线程的数据交换。
    • 线程在交换点会阻塞,直到两个线程都到达交换点并完成数据交换。
  • 工作机制

    • 当一个线程调用exchange()方法时,它会等待另一个线程也调用exchange()
    • 两个线程一旦都到达交换点,会交换它们传递的数据,并继续执行。
  • 典型应用场景

    1. 双线程数据交换:

      • 两个线程需要互相传递数据,如生产者与消费者之间的双向数据交换。
    2. 协作计算:

      • 两个线程需要在某个阶段交换中间结果,协同完成任务。
    3. 同步通信:

      • 在并行计算中,多个线程需要在特定阶段同步并交换数据,以保持协作的一致性。
  • 示例:两个线程交换数据

    import java.util.concurrent.Exchanger;
    
    public class ExchangerExample {
        public static void main(String[] args) {
            Exchanger<String> exchanger = new Exchanger<>();
    
            // 线程A:发送数据
            new Thread(() -> {
                String data = "Data from Thread A";
                try {
                    System.out.println("Thread A sending: " + data);
                    String received = exchanger.exchange(data);
                    System.out.println("Thread A received: " + received);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }, "Thread-A").start();
    
            // 线程B:发送数据
            new Thread(() -> {
                String data = "Data from Thread B";
                try {
                    System.out.println("Thread B sending: " + data);
                    String received = exchanger.exchange(data);
                    System.out.println("Thread B received: " + received);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }, "Thread-B").start();
        }
    }
    

输出

Thread A sending: Data from Thread A
Thread B sending: Data from Thread B
Thread A received: Data from Thread B
Thread B received: Data from Thread A

注意

  • 线程配对Exchanger只支持两个线程之间的数据交换,无法同时与多个线程交换。
  • 超时控制:可以使用带有超时参数的exchange(V x, long timeout, TimeUnit unit)方法,避免线程无限等待。

48. 解释CountDownLatchCyclicBarrier的区别及其各自的应用场景。

回答:

  • CountDownLatch

    • java.util.concurrent包下的一个同步工具类,允许一个或多个线程等待,直到其他线程完成一组操作。
    • 计数器一次性,从初始值减到零后不可重用。

    特点

    • 计数器在初始化后无法修改。
    • 适用于等待固定数量的事件或任务完成。
  • CyclicBarrier

    • java.util.concurrent包下的一个同步工具类,允许一组线程互相等待,直到所有线程都到达某个共同的屏障点。
    • 计数器可以重用,适用于多阶段的同步控制。

    特点

    • 计数器可以重用,支持多次等待和同步。
    • 可以设置一个回调,当所有线程到达屏障点时执行。
  • 主要区别

    特性CountDownLatchCyclicBarrier
    重用性不可重用,一次性可重用,多次使用同一个屏障点
    参与者注册在创建时指定固定数量,不能动态变化在创建时指定固定数量,可在运行时动态注册
    额外动作不支持,只有等待和计数支持,当所有线程到达屏障点时可执行回调
    使用场景等待多个线程完成初始化或任务同步多个线程在多个阶段的任务执行
  • 应用场景

    1. CountDownLatch
      • 等待多个线程完成:主线程等待多个子线程完成初始化或任务后继续执行。
      • 并行测试:等待测试中的多个步骤完成后进行结果验证。
      • 启动信号:一个线程等待其他线程准备完毕后一起开始执行。
    2. CyclicBarrier
      • 多阶段任务同步:在多个阶段的并行任务中,等待所有线程完成一个阶段后再继续下一阶段。
      • 并行计算:在并行计算中,等待所有线程完成某个计算步骤后进行汇总。
      • 集体行动:一组线程在某个共同点同步执行集体行动。
  • 示例

    CountDownLatch示例

    import java.util.concurrent.CountDownLatch;
    
    public class CountDownLatchDemo {
        public static void main(String[] args) throws InterruptedException {
            int numberOfWorkers = 3;
            CountDownLatch latch = new CountDownLatch(numberOfWorkers);
    
            for (int i = 0; i < numberOfWorkers; i++) {
                final int workerId = i;
                new Thread(() -> {
                    System.out.println("Worker " + workerId + " is doing work.");
                    try {
                        Thread.sleep(1000 + workerId * 500);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                    System.out.println("Worker " + workerId + " finished work.");
                    latch.countDown();
                }).start();
            }
    
            System.out.println("Main thread waiting for workers to finish.");
            latch.await();
            System.out.println("All workers have finished. Main thread proceeding.");
        }
    }
    

    CyclicBarrier示例

    import java.util.concurrent.CyclicBarrier;
    import java.util.concurrent.BrokenBarrierException;
    
    public class CyclicBarrierDemo {
        public static void main(String[] args) {
            int numberOfParties = 3;
            CyclicBarrier barrier = new CyclicBarrier(numberOfParties, () -> {
                System.out.println("All parties have arrived at the barrier. Proceeding...");
            });
    
            for (int i = 0; i < numberOfParties; i++) {
                final int threadId = i;
                new Thread(() -> {
                    try {
                        System.out.println("Thread " + threadId + " is performing part 1.");
                        Thread.sleep(1000 + threadId * 500);
                        System.out.println("Thread " + threadId + " is waiting at the barrier.");
                        barrier.await(); // 等待所有线程到达
                        System.out.println("Thread " + threadId + " is performing part 2.");
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        }
    }
    

输出

Worker 0 is doing work.
Worker 1 is doing work.
Worker 2 is doing work.
Main thread waiting for workers to finish.
Worker 0 finished work.
Worker 1 finished work.
Worker 2 finished work.
All workers have finished. Main thread proceeding.
Thread 0 is performing part 1.
Thread 1 is performing part 1.
Thread 2 is performing part 1.
Thread 0 is waiting at the barrier.
Thread 1 is waiting at the barrier.
Thread 2 is waiting at the barrier.
All parties have arrived at the barrier. Proceeding...
Thread 0 is performing part 2.
Thread 1 is performing part 2.
Thread 2 is performing part 2.

注意

  • 可重用性CyclicBarrier在所有线程到达屏障点后,可以继续使用,适用于多阶段同步;CountDownLatch一旦计数为零,无法重用。
  • 异常处理:在使用CyclicBarrier时,需处理BrokenBarrierExceptionInterruptedException

49. 什么是Phaser?它与CyclicBarrierCountDownLatch有何不同?

回答:

  • Phaser

    • java.util.concurrent包下的一个同步工具类,支持多阶段的同步控制。
    • 可以动态地注册和注销参与者,适用于可变数量的线程和多阶段任务。
    • 继承自ForkJoinTask,与ForkJoinPool紧密集成。
  • CyclicBarrierCountDownLatch的区别

    特性CountDownLatchCyclicBarrierPhaser
    重用性不可重用,一次性可重用,多次使用同一个屏障点可重用,多阶段同步,动态参与者
    参与者注册固定数量,创建时指定固定数量,创建时指定可动态注册和注销参与者,适应运行时变化
    额外动作不支持,只有等待和计数支持,当所有线程到达屏障点时可执行回调支持,当阶段完成时可执行回调
    多阶段控制不支持支持一个屏障点支持多个阶段的同步控制
    适用场景等待固定数量的线程完成某个操作同步一组线程在某个屏障点多阶段任务同步,动态参与者,多线程间的复杂同步需求
  • 优势

    • 灵活性Phaser支持动态注册和注销参与者,适用于参与者数量不固定的场景。
    • 多阶段同步:可以控制多个阶段的同步点,适用于多阶段的任务执行。
    • 适应性强:支持灵活的线程同步,适用于复杂的并发任务控制。
  • 使用示例:多阶段任务同步

    import java.util.concurrent.Phaser;
    
    public class PhaserDemo {
        public static void main(String[] args) {
            Phaser phaser = new Phaser(1); // 注册主线程
    
            int numberOfParties = 3;
            for (int i = 0; i < numberOfParties; i++) {
                phaser.register(); // 动态注册参与者
                final int taskId = i;
                new Thread(() -> {
                    System.out.println("Task " + taskId + " is performing Phase 1.");
                    try {
                        Thread.sleep(1000 + taskId * 500);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                    phaser.arriveAndAwaitAdvance(); // 到达阶段1并等待其他参与者
    
                    System.out.println("Task " + taskId + " is performing Phase 2.");
                    try {
                        Thread.sleep(1000 + taskId * 500);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                    phaser.arriveAndDeregister(); // 到达阶段2并注销
                }, "Thread-" + i).start();
            }
    
            // 等待所有参与者到达阶段1
            phaser.arriveAndAwaitAdvance();
            System.out.println("All tasks have completed Phase 1.");
    
            // 等待所有参与者到达阶段2并注销
            phaser.arriveAndAwaitAdvance();
            System.out.println("All tasks have completed Phase 2. Phaser terminating.");
            phaser.arriveAndDeregister(); // 主线程注销
        }
    }
    

输出

Task 0 is performing Phase 1.
Task 1 is performing Phase 1.
Task 2 is performing Phase 1.
All tasks have completed Phase 1.
Task 0 is performing Phase 2.
Task 1 is performing Phase 2.
Task 2 is performing Phase 2.
All tasks have completed Phase 2. Phaser terminating.

注意

  • 动态参与者Phaser允许在运行时动态注册和注销参与者,适应线程数量的变化。
  • 阶段控制:通过多个arriveAndAwaitAdvance()调用,实现多阶段的同步控制。

50. 解释StampedLock的工作机制及其优缺点。

回答:

  • StampedLock

    • java.util.concurrent.locks包下的一个锁实现,Java 8引入。
    • 提供了一种更加灵活和高效的读写锁机制,支持乐观读锁。
    • 适用于高并发读写场景,提升读操作的性能。
  • 工作机制

    1. 锁模式:

      • 写锁(Write Lock):

        • 独占锁,只有一个线程可以持有。
        • 阻塞所有读锁和其他写锁的获取。
      • 悲观读锁(Read Lock):

        • 多个线程可以同时持有读锁,前提是没有线程持有写锁。
      • 乐观读锁(Optimistic Read):

        • 线程尝试在无需锁定的情况下读取数据,通过验证确保读取过程中数据未被修改。
        • 适用于读多写少的场景,提升读操作的性能。
    2. 标记(Stamp):

      • 每次获取锁时,返回一个stamp值,代表锁的状态。
      • 通过stamp进行锁的释放和验证。
    3. 锁获取与释放:

      • 写锁:

        • 使用writeLock()方法获取写锁,返回stamp
        • 使用unlockWrite(stamp)方法释放写锁。
      • 悲观读锁:

        • 使用readLock()方法获取读锁,返回stamp
        • 使用unlockRead(stamp)方法释放读锁。
      • 乐观读锁:

        • 使用tryOptimisticRead()方法获取乐观读锁,返回stamp
        • 使用validate(stamp)方法验证读取过程中数据是否被修改。
  • 优点

    1. 高效的读操作:

      • 乐观读锁允许在无需锁定的情况下进行读取,减少了锁的开销,提升了读操作的性能。
    2. 灵活的锁模式:

      • 提供了写锁、悲观读锁和乐观读锁,适应不同的并发需求。
    3. 减少锁竞争:

      • 乐观读锁在读多写少的场景下,显著减少锁竞争,提高并发性能。
  • 缺点

    1. 不可重入:

      • StampedLock不支持重入,即同一线程无法多次获取同一种锁。
    2. 复杂性:

      • 使用StampedLock需要管理stamp值,增加了编程复杂度。
    3. ABA问题:

      • 乐观读锁可能受到ABA问题的影响,需谨慎处理。
    4. 不支持条件变量:

      • StampedLock不支持条件变量(Condition),无法实现更复杂的线程间通信。
  • 示例

    import java.util.concurrent.locks.StampedLock;
    
    public class StampedLockExample {
        private final StampedLock sl = new StampedLock();
        private int count = 0;
    
        // 写操作
        public void increment() {
            long stamp = sl.writeLock();
            try {
                count++;
                System.out.println(Thread.currentThread().getName() + " incremented count to " + count);
            } finally {
                sl.unlockWrite(stamp);
            }
        }
    
        // 悲观读操作
        public int pessimisticRead() {
            long stamp = sl.readLock();
            try {
                System.out.println(Thread.currentThread().getName() + " read count as " + count);
                return count;
            } finally {
                sl.unlockRead(stamp);
            }
        }
    
        // 乐观读操作
        public int optimisticRead() {
            long stamp = sl.tryOptimisticRead();
            int currentCount = count;
            if (!sl.validate(stamp)) {
                stamp = sl.readLock();
                try {
                    currentCount = count;
                } finally {
                    sl.unlockRead(stamp);
                }
            }
            System.out.println(Thread.currentThread().getName() + " optimistically read count as " + currentCount);
            return currentCount;
        }
    
        public static void main(String[] args) throws InterruptedException {
            StampedLockExample example = new StampedLockExample();
    
            // 创建写线程
            Runnable writer = () -> {
                for (int i = 0; i < 5; i++) {
                    example.increment();
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            };
    
            // 创建读线程
            Runnable reader = () -> {
                for (int i = 0; i < 5; i++) {
                    example.optimisticRead();
                    example.pessimisticRead();
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            };
    
            new Thread(writer, "Writer").start();
            new Thread(reader, "Reader-1").start();
            new Thread(reader, "Reader-2").start();
        }
    }
    

输出

Writer incremented count to 1
Reader-1 optimistically read count as 1
Reader-2 optimistically read count as 1
Reader-1 read count as 1
Reader-2 read count as 1
Writer incremented count to 2
Reader-1 optimistically read count as 2
Reader-2 optimistically read count as 2
Reader-1 read count as 2
Reader-2 read count as 2
...

总结StampedLock通过支持乐观读锁和更细粒度的锁控制,提供了高效的并发读写能力,适用于读多写少的高并发场景,但使用时需注意其复杂性和不可重入性。

51. 什么是FutureTask?它如何与CallableRunnable结合使用?

回答:

  • FutureTask<V>

    • java.util.concurrent包下的一个类,实现了RunnableFuture<V>接口,结合了RunnableFuture的功能。
    • 代表一个可取消的异步计算任务,能够执行CallableRunnable任务,并返回结果或状态。
  • CallableRunnable的结合使用

    • Callable<V>

      • java.util.concurrent包下的一个接口,代表一个可以返回结果并可能抛出异常的任务。
    • Runnable

      • java.lang包下的一个接口,代表一个不返回结果、不抛出异常的任务。
    • FutureTask的构造方法:

      • 可以接收Callable<V>Runnable和一个结果对象。

    示例

    import java.util.concurrent.*;
    
    public class FutureTaskExample {
        public static void main(String[] args) throws InterruptedException, ExecutionException {
            // 使用Callable构造FutureTask
            Callable<Integer> callable = () -> {
                Thread.sleep(1000);
                return 42;
            };
            FutureTask<Integer> futureTask = new FutureTask<>(callable);
    
            // 启动线程执行FutureTask
            new Thread(futureTask).start();
    
            // 主线程执行其他操作
            System.out.println("Main thread is doing other work.");
    
            // 获取FutureTask的结果,可能会阻塞
            int result = futureTask.get();
            System.out.println("Result from FutureTask: " + result);
    
            // 使用Runnable构造FutureTask
            Runnable runnable = () -> System.out.println("Runnable task executed.");
            FutureTask<Void> runnableTask = new FutureTask<>(runnable, null);
    
            // 启动线程执行FutureTask
            new Thread(runnableTask).start();
    
            // 等待Runnable任务完成
            runnableTask.get();
            System.out.println("Runnable task completed.");
        }
    }
    

输出

Main thread is doing other work.
Runnable task executed.
Runnable task completed.
Result from FutureTask: 42

注意

  • 任务取消:可以通过futureTask.cancel(true)取消任务,true表示允许中断正在执行的任务。
  • 结果获取:调用get()方法获取任务结果,会阻塞直到任务完成;也可以使用带超时参数的get(long timeout, TimeUnit unit)方法。
  • 与线程池结合FutureTask可以提交到ExecutorService,用于获取异步任务的结果。

52. 如何实现线程间的协作?请举例说明。

回答:

  • 线程间的协作

    • 通过共享变量、同步工具类和通信机制,让多个线程协调完成复杂的任务。
    • 主要方法包括:
      • 使用锁(synchronizedReentrantLock)同步访问共享资源。
      • 使用条件变量(Conditionwait()notify())实现线程间通信。
      • 使用同步工具类(CountDownLatchCyclicBarrierSemaphoreExchanger)控制线程同步。
      • 使用并发集合和原子变量,实现高效的线程间数据共享。
  • 示例:生产者-消费者模型实现线程间协作

    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.LinkedBlockingQueue;
    
    public class ProducerConsumerCollaboration {
        private static final int BUFFER_SIZE = 5;
        private final BlockingQueue<Integer> buffer = new LinkedBlockingQueue<>(BUFFER_SIZE);
    
        // 生产者
        class Producer implements Runnable {
            private final int id;
    
            public Producer(int id) {
                this.id = id;
            }
    
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        int item = id * 100 + i;
                        buffer.put(item); // 生产并放入缓冲区,可能阻塞
                        System.out.println("Producer " + id + " produced: " + item);
                        Thread.sleep((long) (Math.random() * 500));
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    
        // 消费者
        class Consumer implements Runnable {
            private final int id;
    
            public Consumer(int id) {
                this.id = id;
            }
    
            @Override
            public void run() {
                try {
                    while (true) {
                        int item = buffer.take(); // 消费并取出缓冲区,可能阻塞
                        System.out.println("Consumer " + id + " consumed: " + item);
                        Thread.sleep((long) (Math.random() * 1000));
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    
        public void start() {
            // 启动生产者线程
            for (int i = 1; i <= 2; i++) {
                new Thread(new Producer(i), "Producer-" + i).start();
            }
    
            // 启动消费者线程
            for (int i = 1; i <= 3; i++) {
                new Thread(new Consumer(i), "Consumer-" + i).start();
            }
        }
    
        public static void main(String[] args) {
            ProducerConsumerCollaboration pc = new ProducerConsumerCollaboration();
            pc.start();
        }
    }
    

输出示例

Producer 1 produced: 100
Producer 2 produced: 200
Consumer 1 consumed: 100
Producer 1 produced: 101
Consumer 2 consumed: 200
Producer 2 produced: 201
Consumer 3 consumed: 101
...

注意

  • 阻塞行为BlockingQueueput()take()方法会根据队列的状态自动阻塞和唤醒线程,实现生产者和消费者的协作。
  • 线程管理:在实际应用中,可以使用ExecutorService管理生产者和消费者线程,简化线程创建和管理。
  • 终止条件:示例中的消费者线程为无限循环,实际应用中需要设计合理的终止条件,如使用特殊的“结束”信号或中断机制。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愤怒的代码

如果您有受益,欢迎打赏博主😊

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值