Java高手的30k之路|面试宝典|精通多线程(三)- 并发编程

高级并发工具

并发集合

之前发过

线程池

Executors类的工厂方法

Java中的Executors类提供了一些静态工厂方法,用于创建常见的线程池类型:

  1. newFixedThreadPool(int nThreads)

    • 创建一个固定大小的线程池。
    • 当线程数达到最大值时,新的任务将在队列中等待。
    • 适用于需要限制线程数量以满足资源管理的场景。
  2. newCachedThreadPool()

    • 创建一个可以根据需要创建新线程的线程池。
    • 如果线程池中有可用线程,将重用它们;否则,将创建新线程。
    • 适用于执行许多短期异步任务的场景。
  3. newSingleThreadExecutor()

    • 创建一个单线程的线程池。
    • 这个线程池保证所有任务在同一个线程中顺序执行。
    • 适用于需要保证任务顺序执行的场景。
  4. newScheduledThreadPool(int corePoolSize)

    • 创建一个支持定时和周期性任务执行的线程池。
    • 适用于需要在给定延迟后运行任务或定期执行任务的场景。
使用场景和示例代码
1. 固定大小的线程池(newFixedThreadPool)

适用场景:需要限制并发线程数量以控制资源使用。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        for (int i = 0; i < 5; i++) {
            final int index = i;
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " is handling task " + index);
            });
        }

        executor.shutdown();
    }
}
2. 可缓存的线程池(newCachedThreadPool)

适用场景:执行很多短期异步任务时,可以提高性能。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        
        for (int i = 0; i < 5; i++) {
            final int index = i;
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " is handling task " + index);
            });
        }

        executor.shutdown();
    }
}
3. 单线程的线程池(newSingleThreadExecutor)

适用场景:需要保证任务按顺序执行。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleThreadExecutorExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        
        for (int i = 0; i < 5; i++) {
            final int index = i;
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " is handling task " + index);
            });
        }

        executor.shutdown();
    }
}
4. 定时线程池(newScheduledThreadPool)

适用场景:需要定时或周期性执行任务。

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolExample {
    public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);

        executor.schedule(() -> {
            System.out.println("Task executed after 3 seconds");
        }, 3, TimeUnit.SECONDS);

        executor.scheduleAtFixedRate(() -> {
            System.out.println("Periodic task executed every 2 seconds");
        }, 1, 2, TimeUnit.SECONDS);

        executor.scheduleWithFixedDelay(() -> {
            System.out.println("Fixed delay task executed after previous task completes");
        }, 1, 2, TimeUnit.SECONDS);
    }
}

池化技术

学习思想

池化技术是一种提高系统性能和资源利用效率的常用方法,在Java高级开发中尤为重要。以下是池化技术相关的知识点及经典使用场景:

  1. 基本概念

    • 池化:将一组可以重复使用的资源(如线程、连接、对象等)维护在一个池中,避免频繁创建和销毁资源,提升性能和效率。
    • 资源池:管理和维护可复用资源的容器,如线程池、连接池、对象池等。
  2. 常见类型

    • 线程池(Thread Pool):复用一组线程来执行多个任务,减少线程创建和销毁的开销。
    • 连接池(Connection Pool):维护数据库连接的复用,避免频繁创建和关闭数据库连接。
    • 对象池(Object Pool):维护可复用对象的池,减少对象创建和销毁的开销。
  3. 优点

    • 性能提升:减少资源创建和销毁的频率,降低系统开销。
    • 资源复用:高效管理和复用资源,提高资源利用率。
    • 管理方便:统一管理资源的分配和回收,提升系统的可维护性。
  4. 实现机制

    • 资源初始化:在池创建时预先初始化一定数量的资源。
    • 资源分配:从池中获取资源供客户端使用。
    • 资源回收:使用完毕后,将资源归还池中以备复用。
    • 资源销毁:在资源不再需要时,释放其占用的系统资源。
经典使用场景
  1. 线程池

    • 场景:并发任务执行,例如处理大量请求、后台任务调度。
    • 实现:使用Executors类创建不同类型的线程池(如固定大小线程池、缓存线程池、单线程池)。
    • 示例
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class ThreadPoolExample {
        public static void main(String[] args) {
            ExecutorService executor = Executors.newFixedThreadPool(5);
    
            for (int i = 0; i < 10; i++) {
                executor.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " is executing task.");
                });
            }
    
            executor.shutdown();
        }
    }
    
  2. 数据库连接池

    • 场景:数据库操作频繁的应用,例如Web应用、企业级应用。
    • 实现:使用数据库连接池(如HikariCP、C3P0、Druid)管理数据库连接。
    • 示例
    import com.zaxxer.hikari.HikariConfig;
    import com.zaxxer.hikari.HikariDataSource;
    
    import java.sql.Connection;
    import java.sql.ResultSet;
    import java.sql.Statement;
    
    public class ConnectionPoolExample {
        public static void main(String[] args) throws Exception {
            HikariConfig config = new HikariConfig();
            config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
            config.setUsername("user");
            config.setPassword("password");
    
            HikariDataSource dataSource = new HikariDataSource(config);
    
            try (Connection conn = dataSource.getConnection();
                 Statement stmt = conn.createStatement();
                 ResultSet rs = stmt.executeQuery("SELECT * FROM mytable")) {
    
                while (rs.next()) {
                    System.out.println(rs.getString("column1"));
                }
            }
        }
    }
    
  3. 对象池

    • 场景:高频次使用某些重资源对象,如大数据量处理中的对象重用。
    • 实现:使用第三方库(如Apache Commons Pool)或自行实现对象池。
    • 示例
    import org.apache.commons.pool2.ObjectPool;
    import org.apache.commons.pool2.impl.GenericObjectPool;
    
    public class ObjectPoolExample {
        public static void main(String[] args) throws Exception {
            ObjectPool<MyObject> pool = new GenericObjectPool<>(new MyObjectFactory());
    
            MyObject obj1 = pool.borrowObject();
            // 使用对象
            pool.returnObject(obj1);
        }
    }
    
    class MyObject {
        // 对象的具体实现
    }
    
    class MyObjectFactory extends BasePooledObjectFactory<MyObject> {
        @Override
        public MyObject create() throws Exception {
            return new MyObject();
        }
    
        @Override
        public PooledObject<MyObject> wrap(MyObject obj) {
            return new DefaultPooledObject<>(obj);
        }
    }
    

在Java高级开发面试中,深入了解ThreadPoolExecutor的配置与参数是一个重要的知识点。ThreadPoolExecutor是Java并发包(java.util.concurrent)中用于创建和管理线程池的核心类。它允许开发者灵活配置线程池的行为,以满足不同的应用需求。下面是ThreadPoolExecutor的配置和参数的详细说明:

ThreadPoolExecutor配置与参数

构造方法

ThreadPoolExecutor提供多个构造方法,最常用的是以下这个:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
参数解释
  1. corePoolSize(核心线程池大小):

    • 线程池中保持的线程数量,即使它们处于空闲状态。
    • 这些线程用于执行提交的任务,除非设置了allowCoreThreadTimeOut
  2. maximumPoolSize(最大线程池大小):

    • 线程池中允许的最大线程数量。
    • 当队列满时,如果线程数少于最大线程数,会创建新的线程。
  3. keepAliveTime(线程空闲保持时间):

    • 当线程数超过corePoolSize时,空闲线程的存活时间。
    • 超过这个时间的空闲线程将被终止。
  4. unit(时间单位):

    • keepAliveTime参数的时间单位。常用的时间单位包括TimeUnit.SECONDSTimeUnit.MILLISECONDS等。
  5. workQueue(工作队列):

    • 用于存放等待执行任务的队列。常用的队列实现包括:
      • SynchronousQueue:不存储任务,直接将任务交给线程执行。
      • LinkedBlockingQueue:一个基于链表的有界阻塞队列,按FIFO顺序保存任务。
      • ArrayBlockingQueue:一个基于数组的有界阻塞队列。
      • PriorityBlockingQueue:一个支持优先级的无界阻塞队列。
  6. threadFactory(线程工厂):

    • 用于创建新线程。可以通过实现ThreadFactory接口来自定义线程工厂,通常用于设置线程名称、是否为守护线程等属性。
  7. handler(拒绝策略):

    • 当线程池和队列都满时,执行被拒绝任务的处理程序。常用的拒绝策略包括:
      • AbortPolicy(默认):抛出RejectedExecutionException异常。
      • CallerRunsPolicy:调用运行该任务的线程来执行任务。
      • DiscardPolicy:丢弃任务,不抛出异常。
      • DiscardOldestPolicy:丢弃队列中最旧的任务,然后尝试重新提交任务。
示例代码

下面是一个ThreadPoolExecutor的配置示例,展示了如何创建和使用一个线程池:

import java.util.concurrent.*;

public class ThreadPoolExecutorExample {
    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++) {
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " is executing task.");
                try {
                    Thread.sleep(2000);  // Simulate task execution
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }
    }
}
重要概念
  1. 线程池大小设置

    • corePoolSizemaximumPoolSize应根据任务的特性和系统资源合理设置。
    • 可以通过CPU密集型任务(线程数设置为CPU核数+1)和IO密集型任务(线程数可以设置为更大的值)来优化线程池大小。
  2. 任务队列选择

    • 任务队列的选择对线程池行为有重要影响,例如LinkedBlockingQueue适合大多数常规任务,SynchronousQueue适合高并发短任务。
  3. 拒绝策略选择

    • 需要根据实际应用场景选择合适的拒绝策略,避免系统过载时任务丢失或异常抛出。
  4. 线程工厂自定义

    • 通过自定义线程工厂,可以设置线程名称、是否为守护线程、优先级等属性,有助于调试和监控。

学习ThreadPoolExecutor设计

ThreadPoolExecutor 构造函数的关键因素和设计考量:

关键因素和设计考量

  1. 线程池大小配置

    • 核心线程数 (corePoolSize):线程池中保持活跃的线程数量,即使它们是空闲的。
    • 最大线程数 (maximumPoolSize):线程池中允许的最大线程数量。当工作队列已满时,若核心线程已全部占用且未达到最大线程数,则创建新的线程来处理任务。
    • 合理设置:需要根据任务类型(CPU密集型、I/O密集型)和系统资源进行合理设置。比如,CPU密集型任务通常设置为CPU核心数+1,而I/O密集型任务则可以设置更多。
  2. 线程存活时间 (keepAliveTime) 和时间单位 (unit)

    • 当线程数超过核心线程数时,超过此存活时间的空闲线程将被终止。该时间单位通常为秒、毫秒等。
    • 长时间空闲处理:设置一个合适的存活时间,可以在任务高峰期过后释放不必要的资源,避免系统资源浪费。
  3. 工作队列 (workQueue)

    • 用于存放等待执行任务的队列。常见的有:
      • 无界队列 (LinkedBlockingQueue):没有大小限制,但可能导致任务积压,内存耗尽。
      • 有界队列 (ArrayBlockingQueue):有大小限制,适用于控制资源的应用场景。
      • 优先级队列 (PriorityBlockingQueue):按任务优先级执行。
      • 同步队列 (SynchronousQueue):每个插入操作都要等待一个删除操作,适用于高并发短任务。
    • 选择合适的队列:需要根据应用场景选择合适的工作队列,以平衡性能和资源使用。
  4. 线程工厂 (threadFactory)

    • 用于创建新线程。可以通过实现 ThreadFactory 接口来自定义线程工厂,通常用于设置线程名称、优先级、是否为守护线程等属性。
    • 自定义线程工厂:有助于调试、监控和管理线程。
  5. 拒绝策略 (handler)

    • 当线程池和工作队列都满时,处理新任务的策略。常见的拒绝策略包括:
      • AbortPolicy:直接抛出 RejectedExecutionException 异常。
      • CallerRunsPolicy:由调用线程处理该任务。
      • DiscardPolicy:直接丢弃任务,不抛出异常。
      • DiscardOldestPolicy:丢弃队列中最旧的任务,然后尝试重新提交。
    • 合理的拒绝策略:根据实际需求选择合适的拒绝策略,以避免系统过载和任务丢失。
  6. 线程安全

    • 确保线程池的内部操作是线程安全的,避免线程之间的竞争和数据不一致问题。
    • 锁机制与并发控制:使用合适的锁机制(如 ReentrantLocksynchronized)和并发控制技术(如CAS操作、条件变量)来保证线程安全。
示例设计

以下是一个简化版的线程池实现示例,包含上述设计因素:

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class CustomThreadPoolExecutor {

    private final int corePoolSize;
    private final int maximumPoolSize;
    private final long keepAliveTime;
    private final TimeUnit unit;
    private final BlockingQueue<Runnable> workQueue;
    private final ThreadFactory threadFactory;
    private final RejectedExecutionHandler handler;
    private final AtomicInteger activeThreads = new AtomicInteger(0);

    public CustomThreadPoolExecutor(int corePoolSize,
                                    int maximumPoolSize,
                                    long keepAliveTime,
                                    TimeUnit unit,
                                    BlockingQueue<Runnable> workQueue,
                                    ThreadFactory threadFactory,
                                    RejectedExecutionHandler handler) {
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.keepAliveTime = keepAliveTime;
        this.unit = unit;
        this.workQueue = workQueue;
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

    public void execute(Runnable command) {
        if (command == null) throw new NullPointerException();
        
        if (activeThreads.get() < corePoolSize) {
            if (addWorker(command, true)) return;
        }
        
        if (workQueue.offer(command)) {
            // Task accepted by queue
        } else if (activeThreads.get() < maximumPoolSize && addWorker(command, false)) {
            // Task accepted by new worker
        } else {
            handler.rejectedExecution(command, this);
        }
    }

    private boolean addWorker(Runnable firstTask, boolean core) {
        // Simplified worker creation and start
        Thread thread = threadFactory.newThread(() -> {
            try {
                while (firstTask != null || (firstTask = workQueue.poll(keepAliveTime, unit)) != null) {
                    try {
                        firstTask.run();
                    } finally {
                        firstTask = null;
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                activeThreads.decrementAndGet();
            }
        });
        if (thread != null) {
            activeThreads.incrementAndGet();
            thread.start();
            return true;
        }
        return false;
    }

    public void shutdown() {
        // Shutdown logic
    }

    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
        // Termination logic
        return true;
    }

    // Additional methods...
}
其他语言的实现

在其他编程语言中,如Python、C++、JavaScript等,可以基于上述设计原则和关键因素实现类似的线程池。以下是一些实现要点:

  1. 线程池大小:配置核心线程数和最大线程数。
  2. 线程存活时间:配置空闲线程的存活时间。
  3. 工作队列:选择适合的队列类型。
  4. 线程工厂:提供自定义线程创建方式。
  5. 拒绝策略:实现任务拒绝处理逻辑。
  6. 线程安全:确保多线程操作的安全性。

ThreadLocal

ThreadLocal 是 Java 中提供的一种机制,用于在同一个线程中存储和访问全局变量。每个线程可以通过 ThreadLocal 维护自己独立的变量副本,从而避免了多线程环境下的线程安全问题。

作用和使用场景

  1. 线程独立变量:每个线程都有自己的变量副本,互不干扰。
  2. 线程安全:避免了使用同步机制,保证线程安全。
  3. 上下文信息传递:适用于传递与线程关联的上下文信息,如用户会话、事务等。

常见使用场景

  1. 用户会话信息:在Web应用中,存储每个用户的会话信息。
  2. 数据库连接:在数据库操作中,存储每个线程独立的数据库连接。
  3. 事务管理:在事务处理时,存储与当前线程关联的事务信息。

基本使用方法

以下是 ThreadLocal 的基本使用示例:

public class ThreadLocalExample {
    // 创建一个 ThreadLocal 变量
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        Runnable task = () -> {
            // 获取当前线程的变量值
            int value = threadLocal.get();
            System.out.println(Thread.currentThread().getName() + " initial value: " + value);

            // 设置当前线程的变量值
            threadLocal.set(value + 1);
            System.out.println(Thread.currentThread().getName() + " updated value: " + threadLocal.get());
        };

        // 启动多个线程
        Thread thread1 = new Thread(task, "Thread-1");
        Thread thread2 = new Thread(task, "Thread-2");
        thread1.start();
        thread2.start();
    }
}

在上述代码中,每个线程都有自己的 ThreadLocal 变量副本,不会相互影响。

深入理解

  1. 内存泄漏问题

    • 强引用与弱引用ThreadLocal 采用弱引用存储键值,当 ThreadLocal 实例没有强引用时,可以被垃圾回收。但是,ThreadLocal 的值是强引用,可能导致内存泄漏。
    • 清理机制:在每次调用 get(), set(), 或 remove() 方法时,ThreadLocal 会尝试清理已经被回收的键对应的值。
    • 最佳实践:使用 remove() 方法在任务完成后显式地删除变量,避免内存泄漏。
    public class ThreadLocalExample {
        private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
    
        public static void main(String[] args) {
            try {
                int value = threadLocal.get();
                System.out.println("Initial value: " + value);
                threadLocal.set(value + 1);
                System.out.println("Updated value: " + threadLocal.get());
            } finally {
                threadLocal.remove(); // 避免内存泄漏
            }
        }
    }
    
  2. 实现原理

    • 内部结构ThreadLocal 的变量保存在当前线程的 ThreadLocalMap 中,该映射是当前线程私有的。
    • ThreadLocalMap:内部使用 Entry 类存储键值对,Entry 继承自 WeakReference,键是弱引用,值是强引用。
  3. 适用场景

    • 短生命周期变量:适用于存储生命周期较短的变量,如临时的上下文信息、用户会话数据等。
    • 线程隔离的数据:需要在线程之间隔离的数据,如用户请求的处理、数据处理上下文等。

面试回答示例

在面试中,关于 ThreadLocal 的问题可以这样回答:

ThreadLocal 是 Java 中提供的一种机制,用于在同一个线程中存储和访问全局变量。每个线程都拥有自己独立的变量副本,避免了多线程环境下的线程安全问题。ThreadLocal 的典型使用场景包括用户会话信息存储、数据库连接管理和事务处理等。在使用 ThreadLocal 时,需要注意内存泄漏问题,最好在任务完成后显式地调用 remove() 方法进行清理。ThreadLocal 通过在每个线程中维护一个私有的 ThreadLocalMap 实现,其键是弱引用,可以被垃圾回收,但其值是强引用,可能导致内存泄漏,因此需要谨慎使用。

ThreadLocal in Tomcat

Tomcat 是一个多线程的 Java Web 容器,ThreadLocal 在其中用于保存线程私有的数据,避免了同步开销,并且能够在同一请求的不同处理阶段共享数据。以下是一些典型的应用场景:

1. 数据库连接管理

在 Tomcat 中,一个常见的应用场景是通过 ThreadLocal 来管理数据库连接。每个请求线程会从连接池中获取一个数据库连接,并存储在 ThreadLocal 中,这样在请求处理的不同阶段都可以方便地访问到该连接。

public class DatabaseConnectionManager {
    private static ThreadLocal<Connection> connectionHolder = ThreadLocal.withInitial(() -> {
        try {
            // 假设DataSource是已经配置好的数据源
            return DataSource.getConnection();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    });

    public static Connection getConnection() {
        return connectionHolder.get();
    }

    public static void closeConnection() {
        try {
            connectionHolder.get().close();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            connectionHolder.remove();
        }
    }
}
2. 用户会话管理

在 Tomcat 中,通过 ThreadLocal 可以方便地管理每个线程独立的用户会话信息。这种方式可以避免将会话信息传递给每个方法,简化代码。

public class UserSession {
    private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();

    public static void setUser(User user) {
        userThreadLocal.set(user);
    }

    public static User getUser() {
        return userThreadLocal.get();
    }

    public static void clear() {
        userThreadLocal.remove();
    }
}
3. 日志记录

在处理请求时,通过 ThreadLocal 可以将一些上下文信息(如请求ID、用户ID等)保存起来,方便日志记录时使用。

public class LoggingContext {
    private static ThreadLocal<String> requestId = ThreadLocal.withInitial(() -> UUID.randomUUID().toString());

    public static String getRequestId() {
        return requestId.get();
    }

    public static void clear() {
        requestId.remove();
    }
}
4. 事务管理

在分布式事务处理中,可以使用 ThreadLocal 存储当前线程的事务状态,避免事务状态在方法调用之间传递。

public class TransactionManager {
    private static ThreadLocal<Transaction> transactionHolder = new ThreadLocal<>();

    public static void startTransaction() {
        Transaction tx = new Transaction();
        transactionHolder.set(tx);
    }

    public static Transaction getCurrentTransaction() {
        return transactionHolder.get();
    }

    public static void endTransaction() {
        Transaction tx = transactionHolder.get();
        if (tx != null) {
            tx.commit();
        }
        transactionHolder.remove();
    }
}
5. 内存泄漏问题

在 Tomcat 中使用 ThreadLocal 时要特别注意内存泄漏问题。Tomcat 的线程池会重用线程,如果没有清理 ThreadLocal 中的变量,会导致数据在不同请求之间泄漏,甚至导致内存泄漏。因此,在每次请求处理完毕后,应该显式调用 remove() 方法清理 ThreadLocal 变量。

为什么使用到ThreadLocal的时候都是静态值,静态变量是全局唯一,不会在多线程环境出问题吗?
ThreadLocal 是一个用于在多个线程之间提供独立变量副本的类。每个线程可以独立地改变其副本,而不影响其他线程的副本。这使得 ThreadLocal 特别适合在多线程环境中使用,而不需要显式的同步。

静态变量与 ThreadLocal
在 Java 中,静态变量是所有实例共享的。但是,ThreadLocal 提供了一种机制,使得每个线程都可以拥有自己的独立变量副本,而不是共享静态变量。这就解释了为什么 ThreadLocal 的实例通常声明为静态变量的原因。

public class MyClass {
    private static ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);

    public void someMethod() {
        int value = threadLocalValue.get();
        // Do something with value
        threadLocalValue.set(value + 1);
    }
}

为什么使用静态变量

  1. 方便访问:将 ThreadLocal 声明为静态变量,使得它可以在类的不同方法中方便地访问,而不需要每次都传递 ThreadLocal 实例。
  2. 全局唯一:静态变量在类加载时初始化,并且在整个应用程序的生命周期内都只有一个实例。通过使用静态 ThreadLocal 变量,可以确保每个线程在整个应用程序中都使用相同的 ThreadLocal 实例,避免了在不同类或方法中重复创建 ThreadLocal 实例的麻烦。

静态 ThreadLocal 变量不会共享的问题
虽然 ThreadLocal 变量本身是静态的,但它所管理的值(即每个线程的副本)并不是静态的。每个线程都有一个独立的副本,这些副本之间是完全隔离的。因此,不同线程对 ThreadLocal 变量的访问是线程安全的,每个线程只能看到自己独立的副本。

ThreadLocal get源码

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
	
	/**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals; //每个线程都有这个变量
    }

Future和FutureTask

Future 接口

  1. 概述

    Future 接口是 Java 中用于表示异步计算结果的接口。它提供了一种获取异步计算结果的方式,允许检查计算是否完成、等待计算完成以及获取计算的结果。

  2. 基本方法

    • boolean cancel(boolean mayInterruptIfRunning): 尝试取消任务的执行。
    • boolean isCancelled(): 如果任务在取消前完成,则返回 true。
    • boolean isDone(): 如果任务已完成,则返回 true。
    • V get() throws InterruptedException, ExecutionException: 获取计算的结果,如果计算尚未完成,则阻塞当前线程直到计算完成。
  3. 示例

    ExecutorService executor = Executors.newFixedThreadPool(1);
    
    // 提交一个异步任务,并获得 Future 对象
    Future<String> futureResult = executor.submit(() -> {
        Thread.sleep(2000); // 模拟耗时操作
        return "Hello, Future!";
    });
    
    // 检查任务是否完成
    if (!futureResult.isDone()) {
        System.out.println("Task is still running...");
    }
    
    // 等待任务完成,并获取结果
    try {
        String result = futureResult.get(); // 阻塞直到任务完成
        System.out.println("Task result: " + result);
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
    
    executor.shutdown();
    

FutureTask 类

  1. 概述

    FutureTask 类实现了 RunnableFuture 接口,是 Future 接口的一个实现,同时也是 Runnable 接口的实现。它允许将 FutureRunnable 结合使用,用于异步执行任务和获取结果。

  2. 特性

    • 可以由 Executor 执行 FutureTask
    • 可以手动创建和执行。
    • 可以用作 Runnable 传递给 Thread 对象执行。
  3. 示例

    // 创建一个 FutureTask
    FutureTask<String> futureTask = new FutureTask<>(() -> {
        Thread.sleep(3000); // 模拟耗时操作
        return "Hello, FutureTask!";
    });
    
    // 使用 Executor 执行 FutureTask
    ExecutorService executor = Executors.newFixedThreadPool(1);
    executor.execute(futureTask);
    
    // 获取任务结果
    try {
        String result = futureTask.get(); // 阻塞直到任务完成
        System.out.println("FutureTask result: " + result);
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
    
    executor.shutdown();
    

典型面试问题和回答示例

  1. 什么是 Future 接口?

    Future 接口是 Java 中用于异步计算结果的接口,它提供了一种非阻塞获取任务执行结果的机制。

  2. FutureTask 的作用是什么?

    FutureTask 类实现了 Future 接口和 Runnable 接口,允许将 FutureRunnable 结合使用,用于异步执行任务和获取结果。

  3. 如何使用 FutureFutureTask 执行异步任务?

    可以通过 ExecutorService 提交 CallableRunnable 对象创建 FutureFutureTask 实例,然后通过 get() 方法获取计算结果。

CompletableFuture

CompletableFuture 是 Java 8 引入的一个类,它提供了异步编程的强大功能,允许你以非阻塞的方式执行任务并处理它们的结果。

CompletableFuture是Future的实现类

  1. 创建和完成 CompletableFuture

    • CompletableFuture 可以通过静态工厂方法创建,例如 CompletableFuture.supplyAsync()CompletableFuture.runAsync()
    • 可以手动完成 CompletableFuture,使用 complete 方法。
  2. 链式调用和组合

    • thenApply:在前一个任务完成后,使用其结果执行一个函数。
    • thenAccept:在前一个任务完成后,使用其结果执行一个消费者(不返回结果)。
    • thenRun:在前一个任务完成后,执行一个无参的 Runnable(不使用结果)。
    • thenCombine:组合两个 CompletableFuture 的结果。
    • thenCompose:将一个 CompletableFuture 的结果作为另一个异步操作的输入。
  3. 异常处理

    • exceptionally:处理异常并返回默认值。
    • handle:处理正常结果和异常。
    • whenComplete:在 CompletableFuture 完成时执行回调,无论是正常完成还是异常完成。
  4. 并行执行

    • allOf:等待多个 CompletableFuture 全部完成。
    • anyOf:等待任意一个 CompletableFuture 完成。

使用示例

创建和完成 CompletableFuture
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // 执行一些异步操作
    return "Hello, World!";
});

future.thenAccept(result -> {
    // 处理结果
    System.out.println(result);
});
异常处理
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    if (Math.random() > 0.5) {
        throw new RuntimeException("Something went wrong!");
    }
    return "Success!";
});

future.exceptionally(ex -> {
    // 处理异常
    System.out.println("Error: " + ex.getMessage());
    return "Default Value";
}).thenAccept(result -> {
    // 处理结果
    System.out.println(result);
});
链式调用和组合
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");

CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (s1, s2) -> s1 + " " + s2);
combinedFuture.thenAccept(result -> {
    // 输出 "Hello World"
    System.out.println(result);
});
并行执行
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task 1");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task 2");
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> "Task 3");

CompletableFuture<Void> allOf = CompletableFuture.allOf(future1, future2, future3);
allOf.thenRun(() -> {
    // 所有任务完成后执行
    System.out.println("All tasks completed.");
});

典型面试问题和回答示例

  1. 什么是 CompletableFuture

    CompletableFuture 是 Java 8 中引入的一个类,用于处理异步编程。它提供了非阻塞的方法来执行任务并处理它们的结果,使代码更简洁和可读。

  2. 如何处理 CompletableFuture 的异常?

    可以使用 exceptionally 方法来处理异常并提供默认值,或者使用 handle 方法来处理正常结果和异常。此外,可以使用 whenComplete 方法在 CompletableFuture 完成时执行回调,无论是否发生异常。

  3. 如何组合多个 CompletableFuture

    可以使用 thenCombine 方法组合两个 CompletableFuture 的结果,使用 allOf 方法等待多个 CompletableFuture 全部完成,或者使用 anyOf 方法等待任意一个 CompletableFuture 完成。

同步辅助类

下面的类来自包 java.util.concurrent

在 Java 的并发编程中,java.util.concurrent 包提供了一些强大的工具类来简化多线程开发。这些工具类可以帮助你协调多个线程的操作、控制线程间的通信和同步。下面是对 CountDownLatchCyclicBarrierSemaphoreBlockingQueue 等类的详细介绍:

1. CountDownLatch

CountDownLatch 是一种同步辅助类,用于使一个或多个线程等待,直到其他线程完成一组操作。其核心思想是有一个计数器,线程可以在这个计数器上等待,而其他线程可以减少这个计数器的值。

主要方法:

  • countDown(): 递减计数器的值。
  • await(): 使当前线程等待,直到计数器的值变为零。

使用示例:

CountDownLatch latch = new CountDownLatch(3);

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        try {
            // 执行任务
        } finally {
            latch.countDown();
        }
    }).start();
}

latch.await(); // 等待所有任务完成
System.out.println("所有任务已完成");

2. CyclicBarrier

CyclicBarrier 是一种同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点。它与 CountDownLatch 不同的是,CyclicBarrier 在所有线程都到达屏障点后可以重新使用。

主要方法:

  • await(): 使当前线程在屏障点等待,直到所有线程都到达屏障点。

使用示例:

int parties = 3;
CyclicBarrier barrier = new CyclicBarrier(parties, () -> {
    System.out.println("所有线程到达屏障点");
});

for (int i = 0; i < parties; i++) {
    new Thread(() -> {
        try {
            // 执行任务
            barrier.await();
            // 执行屏障后的任务
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }).start();
}

3. Semaphore

Semaphore 是一种计数信号量,用于控制同时访问特定资源的线程数量。它通过允许一定数量的许可来限制并发访问。

主要方法:

  • acquire(): 获取一个许可,如果没有可用的许可,则阻塞。
  • release(): 释放一个许可,增加可用许可的数量。

使用示例:

int permits = 5;
Semaphore semaphore = new Semaphore(permits);

for (int i = 0; i < 10; i++) {
    new Thread(() -> {
        try {
            semaphore.acquire();
            // 执行需要控制并发访问的任务
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
        }
    }).start();
}

4. BlockingQueue

BlockingQueue 是一种支持线程安全操作的队列,它在插入和移除元素时可以自动阻塞或唤醒线程。常用实现类有 ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueue 等。

主要方法:

  • put(E e): 将元素插入队列,如果队列已满则阻塞。
  • take(): 从队列中取出一个元素,如果队列为空则阻塞。

使用示例:

BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10);

new Thread(() -> {
    try {
        queue.put(1);
        // 插入更多元素
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

new Thread(() -> {
    try {
        Integer value = queue.take();
        System.out.println("取出元素: " + value);
        // 取出更多元素
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

Exchanger

作用: Exchanger类提供了一个同步点,允许两个线程在同一个时间点上交换数据。每个线程在调用exchange()方法时会被阻塞,直到另一个线程也调用了同样的方法,然后两者交换数据并继续执行。
主要方法:

  • exchange(V x): 在此方法上调用的线程将自己的数据x与另一个线程交换,如果没有另一个线程调用exchange,则会一直阻塞。
  • exchange(V x, long timeout, TimeUnit unit): 允许设置超时时间,如果在指定时间内没有另一个线程到达交换点,则会抛出超时异常。

应用场景: 可用于遗传算法、流水线设计、以及其他需要线程间数据交换的场景。

Phaser

作用: Phaser类提供了更灵活的线程同步方式,支持多阶段并发任务的控制和同步。
主要方法:

  • register(): 向Phaser注册一个参与者(线程)。
  • arrive(): 表示一个线程已经到达了同步点,会阻塞直到所有注册的参与者都到达。
  • arriveAndAwaitAdvance(): 表示一个线程到达同步点并等待其他参与者到达。
  • arriveAndDeregister(): 表示一个线程到达同步点后注销自己,不再参与后续的同步操作。
  • awaitAdvance(int phase): 在指定阶段等待,直到Phaser进入下一个阶段。

应用场景: 适合需要分阶段并发控制的复杂算法和流程,如分布式任务协调、多阶段数据处理等

总结

  • CountDownLatch: 用于一个或多个线程等待其他线程完成操作。
  • CyclicBarrier: 用于一组线程互相等待,直到所有线程到达屏障点。
  • Semaphore: 用于控制同时访问资源的线程数量。
  • BlockingQueue: 用于线程安全地插入和移除元素,支持阻塞操作。
  • Exchanger: 用于允许两个线程在同一时间点交换数据
  • Phaser: 用于多阶段并发任务控制和同步
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值