Java多线程最佳实践

1,930 阅读20分钟

前言

这篇文章根据场景分类总结实践的最优做法以及注意事项, 不详细解释基本概念。 更灵活, 性能更好往往意味着代码复杂度的增加。 根据实际业务选择, 有时最简单的写法反而更合适当下的场景。

  • 2023-10-15 更新常用并发容器章节

线程池

创建

  • 线程池必须通过 ThreadPolExecutor 的构造函数来显式地声明
  • 构造时使用有界队列,控制线程创建数量, , 避免Executors创建线程池导致的因最大线程数过大(Integer.MAX_VALUE)或者使用无界队列导致的OOM
  • 不同业务使用不同线程池, 并且在构造时给我们的线程池命名, 赋予其业务含义
  • 应设置合适的核心线程数和最大线程数, 如果过小, 则会造成任务的阻塞堆积, 过大则会增加CPU上下文切换时间
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • *corePoolSize: 线程池的最小线程数, 又叫核心线程数,表示线程池中始终保持的线程数。即使线程是空闲的,也不会被销毁
  • *maximumPoolSize: 线程池所有线程(包含核心线程和非核心线程在内)的最大线程数,表示线程池允许创建的最大线程数。如果活动线程数达到最大线程数并且任务队列也已满,后续的任务会触发线程池的拒绝策略
  • keepAliveTime: 非核心线程(超出核心线程数的线程)在空闲状态下的最大存活时间。如果线程池中的线程数超过核心线程数,且空闲时间超过指定时间,这些非核心线程会被回收。
  • unit: keepAliveTime的时间单位。
  • *workQueue: 任务阻塞队列
  • threadFactory: 生产线程池的工厂
  • *handler: 拒绝策略
  • 重点需要关注的是corePoolSize,maximumPoolSize,workQueue队列长度, 以及handler拒绝策略, 这4个参数决定了线程池的运作模式
  • 假设corePoolSize6, maximumPoolSize10, workQueuesize2:
  • 当池内线程超过6时, 第7个任务将被放入workQueue
  • 当任务总数达到8时, 这时workQueue也已经满了, 此时将会根据maximumPoolSize创建新的线程, 这里大家应该发现了, workQueue不能设的太大, 否则线程池无法扩容
  • 当任务总数达到12时, 这时workQueue是满的, 而且池内的线程数量已经达到maximumPoolSize, 此时将会触发handler, 执行拒绝策略的逻辑

corePoolSize

有一个简单并且适用面比较广的公式:

  • CPU 密集型任务(N+1): 多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程。
  • N为逻辑CPU逻辑核心数, 可以用Runtime.getRuntime().availableProcessors()获取
  • 例如, 对于8c16t的机器, N为16

maximumPoolSize

对于I/O 密集型任务, 它的值取决于最大瞬时并发数

BlockingQueue

最常用的是LinkedBlockingQueue

  • 注意构造队列时传入容量, 防止无界队列

ThreadFactory

除了直接继承ThreadFactory实现外, 使用GuavaThreadFactoryBuilder会更方便

new ThreadFactoryBuilder()
        .setNameFormat("pool-name" + "-%d")
        .setDaemon(true)
        .setUncaughtExceptionHandler((t, e) -> {
            System.out.println(t.getName());
            e.printStackTrace();
        })
        .build();
  • setNameFormat(): 程池中的线程生成名称, pool-name-1, pool-name-2, pool-name-3, etc.
  • setDaemon(): 设置是否为守护线程
  • setUncaughtExceptionHandler(): 处理未捕获的异常

RejectedExecutionHandler

JDK提供了四种拒绝策略实现:

  • AbortPolicy: 默认策略, 抛出 RejectedExecutionException 异常。
  • CallerRunsPolicy:让提交任务的线程自己去执行该任务,即在调用线程中执行被拒绝的任务。
  • DiscardPolicy:默默丢弃被拒绝的任务,不抛出异常也不执行任务。
  • DiscardOldestPolicy:丢弃工作队列中最老的任务,然后重新尝试提交当前任务。

监控

可以通过一些手段来检测线程池的运行状态比如 SpringBoot 中的 Actuator 组件。 简单的场景,我们还可以利用 ThreadPoolExecutor 的相关 API 做一个简陋的监控。

  • getPoolSize():获取线程池当前的线程数(包括核心线程和非核心线程)
  • getActiveCount():获取活跃线程数,也就是正在执行任务的线程数量
  • getQueue():获取线程池中的阻塞队列, 队列长度即正在等待执行的任务数
  • getCompletedTaskCount(): 获取线程池已完成的任务数量
  • getLargestPoolSize():获取线程池曾经到过的最大工作线程数
  • getTaskCount():获取历史已完成以及正在执行的总的任务数量

除此之外, 还可以重写ThreadPoolExecutor中的钩子函数:

  • beforeExecute():Worker线程执行任务之前会调用的方法
  • afterExecute():Worker线程执行任务之后会调用的方法
  • terminated():当线程变为TERMINATED状态之调用的方法

比如, 我们可以用beforeExecuteafterExecute监听线程执行的时间,在afterExecuteterminated时释放资源。

动态管理

 ThreadPoolExecutor 提供了相关 API 在运行时动态调整线程池的核心线程数,最大线程数,最大存活时间和拒绝策略, 但任务阻塞队列的容量是final的, 如果想要调整的话, 可以参考ElasticSearch里面实现的ResizableBlockingQueue, 自己实现一个容量可变的容器

  • 需注意任务阻塞队列的容量从大减小可能会导致的问题 // TODO

使用

结合Spring Boot提供的@EnableAsync@Async注解, 使得线程池的使用更加方便

关闭

  • 当线程池不再需要使用时,应该显式地关闭线程池,释放线程资源。shutdown()shutdownNow()都可用于关闭线程池
  • 如有必要, 线程逻辑中应能正常响应中断信号, 用来做释放资源等操作

相同点:

  • 调用后都不会再接收新的任务, 如果调用了submit(), 会抛出RejectedExecutionException
  • 调用后都会立即返回, 等待不会造成阻塞, 如果希望阻塞等待可以调用awaitTermination()方法。

不同点:

  • shutdown()会将线程池状态设为SHUTDOWN, 调用后等待所有已经提交的任务执行完成, 包括正在执行的和在阻塞队列中等待执行的, 同时向空闲的核心线程发送interrupt信号
  • shutdownNow()会将线程池状态设为STOP, 调用后会向线程池中的所有线程发送interrupt信号, 最后返回阻塞队列中等待执行的任务(List<Runnable>)

一个典型的场景是调用shutdown()之后再调用awaitTermination()等待指定的时间, 超过时间后或发生异常时不再等待, 调用shutdownNow()向所有线程发送中断信号

ExecutorService es = Executors.newFixedThreadPool(10);
es.execute(new Thread(() - > {...}));
try {
    es.shutdown();
    if(!es.awaitTermination(5, TimeUnit.SECONDS)){
        // 到达5s指定时间,还有线程没执行完,不再等待
        es.shutdownNow();
    }
} catch (Throwable e) {
    es.shutdownNow();
}

可见性

  • volatile: 对于一个多线程共享的变量, 每次访问变量时,总是获取主内存的最新值, 且当某个线程在其本地内存副本中修改了该变量的值, 立刻回写到主内存

线程同步

原子操作

赋值不需要同步

  1. 基本类型(longdouble除外)赋值,例如:int n = m, longdouble是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把longdouble的赋值作为原子操作实现的。
  2. 引用类型赋值,例如:List<String> list = anotherList

Atomic 原子类

把单个变量引用或者值的变化封装为原子操作, 可分为4类

基本类型

使用原子的方式更新基本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

数组类型

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray:引用类型数组原子类

引用类型

  • AtomicReference:引用类型原子类
  • AtomicMarkableReference:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来。
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

ABA 问题不是必须解决的, 如果业务关注变量的值而不在意值变化的过程, 那就不需要处理。

对象的属性修改类型

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段

注意: 需要原子操作的属性必须用public volatile修饰

一般场景

使用synchronizedReentrantLock

  1. ReentrantLock性能更好, 提供了tryLock()方法限制了最大阻塞时间
  2. ReentrantLock更灵活, 临界区可跨多个代码块
  3. ReentrantLock更适用于顺序敏感的场景, synchronizedReentrantLock默认都是非公平锁, 但ReentrantLock可以在构造时new ReentrantLock(true)设置为公平锁

读多写少

使用ReadWriteLockStampedLock, 将读锁和写锁分离, 提高读并发性能

  1. ReadWriteLock: 悲观, 把读写操作分别用读锁和写锁来加锁, 允许多个线程同时读(当有一个线程持有读锁, 其他线程也可以获取读锁, 这样就大大提高了并发读的执行效率), 但它只允许一个线程写入(当有一个线程持有写锁, 其他线程读锁和写锁都获取不到)
  2. StampedLock乐观, 它和ReadWriteLock相比,不同之处在于,读的过程中也允许获取写锁,这样一来,我们读的数据就可能不一致,但需要一点额外的代码来判断读的过程中是否有写入
public class Point {
    private final StampedLock stampedLock = new StampedLock();

    private double x;
    private double y;

    public void move(double deltaX, double deltaY) {
        long stamp = stampedLock.writeLock(); // 获取写锁
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            stampedLock.unlockWrite(stamp); // 释放写锁
        }
    }

    public double distanceFromOrigin() {
        long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
        // 注意下面两行代码不是原子操作
        // 假设x,y = (100,200)
        double currentX = x;
        // 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
        double currentY = y;
        // 此处已读取到y,如果没有写入,读取是正确的(100,200)
        // 如果有写入,读取是错误的(100,400)
        if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
            stamp = stampedLock.readLock(); // 获取一个悲观读锁
            try {
                currentX = x;
                currentY = y;
            } finally {
                stampedLock.unlockRead(stamp); // 释放悲观读锁
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

死锁

各线程获取可重入锁的顺序一定要相同

public void add(int m) {
    synchronized(lockA) { // 获得lockA的锁
        this.value += m;
        synchronized(lockB) { // 获得lockB的锁
            this.another += m;
        } // 释放lockB的锁
    } // 释放lockA的锁
}

public void dec(int m) {
    synchronized(lockB) { // 获得lockB的锁
        this.another -= m;
        synchronized(lockA) { // 获得lockA的锁
            this.value -= m;
        } // 释放lockA的锁
    } // 释放lockB的锁
}

对于上述代码,线程1和线程2如果分别执行add()dec()方法时, 执行到内层的synchronized时就会永远等待下去, 造成死锁

线程协同

等待-唤醒

使用synchronized+wait/notify, 或者ReentrantLock+Condition可以做到最细粒度的控制, 且进入WAITING状态会释放锁, 不会阻塞其他线程, 但代码较为繁琐

class TaskQueue {
    Queue<String> queue = new LinkedList<>();

    public synchronized void addTask(String s) {
        this.queue.add(s);
        this.notifyAll();
    }

    public synchronized String getTask() throws InterruptedException {
        while (queue.isEmpty()) {
            this.wait();
        }
        return queue.remove();
    }
}

ThreadLocal

  • ThreadLocal用于存放每个线程各自的变量, 本质上是以线程实例自身的弱引用作为key, 从一个全局的ThreadLocal.ThreadLocalMap(可以理解为一个Map<WeakReference<ThreadLocal<?>>, Object>)中取值

  • 需要注意的是, 当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去。因此保证, 当前线程执行的任务, 在结束时能移除ThreadLocal关联的值, 我们借助try (resource) {...}结构,可以这么写:

public class UserContext implements AutoCloseable {

    static final ThreadLocal<String> ctx = new ThreadLocal<>();

    public UserContext(String user) {
        ctx.set(user);
    }

    public static String currentUser() {
        return ctx.get();
    }

    @Override
    public void close() {
        ctx.remove();
    }
}

使用的时候:

try (var ctx = new UserContext("Bob")) {
    // 可任意调用UserContext.currentUser():
    String currentUser = UserContext.currentUser();
} 

InheritableThreadLocal(ITL)

各线程的ThreadLocal之间没有父子关系, 如果子线程想获取父线程ThreadLocal的值, 就需要用到ThreadLocal

TransmittableThreadLocal(TTL)

InheritableThreadLocal仍然有缺陷,一般我们做异步化处理都是使用的线程池,而InheritableThreadLocal是在new Thread中的init()方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题。

当然,有问题出现就会有解决问题的方案,阿里巴巴开源了一个TransmittableThreadLocal(TTL)组件, 用其包裹线程池实例即可实现父子线程的正确传值:

ExecutorService executorService = ...;
executorService = TtlExecutors.getTtlExecutorService(executorService);

还可以通过增加启动参数的方式以零侵入的方式实现, 这里不再详述, 项目在这里alibaba/transmittable-thread-local - GitHub

说到线程间的数据交换, 很自然地想到Exchanger, 虽然它在传递数据时不局限于父传子, 但各线程要在同步点等待, 显然性能较差

CompletableFuture

CompletableFuture在实践中较为常用, 下面详细说明

1. 实例方法

实例方法较多, 需要注意的是, 不带Aync的方法, 其执行者有可能是上文执行任务的子线程, 也有可能是调用方线程, 建议使用Aync版本的方法, 同时每一个步骤都指定线程池, 防止阻塞调用方线程, 确保任务由同一个线程中的线程执行, 消除执行者的不确定性

Actions supplied for dependent completions of non-async methods may be performed by the thread that completes the current CompletableFutureor by any other caller of a completion method.

单个异步任务完成后

  • thenApply: 对其结果执行Function<T,U>, 返回一个新CompletionStage
  • thenAccept: 对其结果执行Consumer
  • thenRun: 执行一个Runnable
  • thenCompose: 类似thenApply, 他们的回参类型都是CompletionStage, 但thenCompose执行的是不是一个普通的Function<T,U>, 而是Function<T,CompletionStage<U>>, 当现有的方法返回已经是一个CompletionStage时, 相比thenApply, thenCompose不会嵌套, 因此thenApply适合用来编写新的异步逻辑, 而thenCompose更适合用来串接多个已有的CompletableFuture
// 回调是普通方法
CompletableFuture<Integer> futureApply = CompletableFuture
                                     .supplyAsync(() -> 1)
                                     .thenApply(x -> x+1);
                                   
CompletableFuture<Integer> futureCompose = CompletableFuture
                              .supplyAsync(() -> 1)
                              .thenCompose(x -> CompletableFuture.supplyAsync(() -> x+1));
// 回调是已有的异步方法, thenApply会嵌套一层而thenCompose不会
public CompletableFuture<UserInfo> getUserInfo(userId)
public CompletableFuture<UserRating> getUserRating(UserInfo)

CompletableFuture<CompletableFuture<UserRating>> f =
    userInfo.thenApply(this::getUserRating);

CompletableFuture<UserRating> relevanceFuture =
    userInfo.thenCompose(this::getUserRating);

两个异步任务完成后

  • thenCombine: 对它们的结果执行BiFunction, 返回一个新结果
  • thenAcceptBoth: 对它们的结果执行BiConsumer
  • runAfterBoth: 执行一个Runnable
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20);

CompletableFuture<Integer> combinedFuture = future1.thenCombine(future2, (result1, result2) -> result1 + result2);

int combinedResult = combinedFuture.join(); // 或者使用 get() 方法获取结果

System.out.println(combinedResult); // 输出:30,因为 future1 返回 10,future2 返回 20,合并结果为 10 + 20 = 30

两个异步任务中的任意一个完成后

  • applyToEither: 对其结果后执行Function, 返回一个新结果, 不需要等待两个任务都完成
  • acceptEither: 对其结果执行Consumer
  • runAfterEither: 执行一个Runnable
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(2000); // 模拟任务1耗时2秒
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return 10;
});

CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(1000); // 模拟任务2耗时1秒
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return 20;
});

CompletableFuture<Integer> resultFuture = future1.applyToEither(future2, result -> result * 2);

int result = resultFuture.join(); // 或者使用 get() 方法获取结果

System.out.println(result); // 输出:40,因为 future2 先完成,结果为 20,应用 fn 函数得到 20 * 2 = 40

异常处理相关

  • exceptionally: 入参是一个Function, 当exceptionally前的异步操作抛出异常时,可以对这个异常进行处理,并返回一个新的CompletionStage
  • handle: 和exceptionally类似, 但入参是一个BiFunction, 因此异常和正常的情况可以处理, ,并返回一个新的CompletionStage, 传递给后面
  • whenComplete: 和handle类似, , 可以同时处理正常和异常的情况, 但入参是一个BiConsumer, Consumer是没有回参的, 所以whenComplete不产生新的异步结果
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    // Simulating an exception
    throw new RuntimeException("Oops, something went wrong!");
}).exceptionally(ex -> {
    // Handling the exception
    System.out.println("Caught exception: " + ex.getMessage());
    return 0; // Providing a default value
});

future.thenAccept(result -> {
    System.out.println("Final result: " + result);
});
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    return 10 / 2;
});

CompletableFuture<String> handledFuture = future.handle((result, ex) -> {
    if (ex != null) {
        return "Error: " + ex.getMessage();
    } else {
        return "Result: " + result;
    }
});

handledFuture.thenAccept(result -> {
    System.out.println(result);
});
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    return 10 / 2;
});

future.whenComplete((result, ex) -> {
    if (ex != null) {
        System.out.println("Exception occurred: " + ex.getMessage());
    } else {
        System.out.println("Result: " + result);
    }
});

其它实例方法

Future接口下

  • get():调用方线程阻塞, 等待异步任务完成后获取结果
  • get(long timeout, TimeUnit unit):同get(),但只等待指定的时间;
  • cancel(boolean mayInterruptIfRunning):取消当前任务;
  • isDone():判断任务是否已完成。
  • join(): 调用方线程阻塞, 等待异步任务完成

CompletableFuture类下

  • getNow(T valueIfAbsent): 不会阻塞调用方线程, 如果任务已完成则返回结果, 否则返回给定的缺省值
  • complete(T value)/completeExceptionally(Throwable ex): 直接手动完成异步任务, 返回给定的正常或异常结果, 如果在调用该方法之前已经有一个结果(包括正常结果或异常),则该方法不会生效, 这个方法可以用于模拟异步任务的完成,并将结果传递给等待该任务的其他部分。
  • obtrudeValue(T value)/obtrudeException(Throwable ex): 类似complete(T value)/completeExceptionally(Throwable ex), 但会无视之前的结果, 强制替换为给定的值

备注

  • Future接口下的方法get()CompletableFuture类下的join()方法的区别在于get()会抛出checked exception, 需要try...catch...手动处理异常, 而join()不需要, 发生异常时join()会抛出一个checked CompletionException, CompletionException中包裹着真正的异常信息
  • 对于异常的处理, 更好的方式还是在get()或者join()之前就调用exceptionally

2. 静态方法

  • runAync/supplyAsync: 执行异步任务, 可指定提交的线程池, 默认使用ForkJoinPool.commonPool()
  • anyOf/allOf: applyToEither/applyToBoth,acceptEither/acceptBoth,runAfterEither/runAfterBoth的强化版, 可以组合2个以上的CompletableFuture
  • completedFuture: 创建一个已完成的任务, 并指定它的返回值, 可用于在异步调用链中返回常量

3. 属性

  • isCompletedExceptionally:如果异步任务异常结束时返回true, 未完成或已正常完成返回false

其它协同工具

CountDownLatch

  • CountDownLatch阻塞主线程, 等待指定数量的线程完成后, 执行指定的逻辑, 基本上可以被静态方法CompletableFuture.allOf()取代

CyclicBarrier

  • 可循环屏障, 各子线程可设置同步点, 运行到同步点时CyclicBarrier会阻塞子线程, 类似一道屏障拦截在多个线程上, 屏障本身包含一段逻辑, 线程经过屏障时会等待, 所有线程都通过屏障时, 执行屏障逻辑, 各子线程也继续往下执行
  • CyclicBarrier不会阻塞主线程

Exchanger

  • CyclicBarrier类似, 不过Exchanger的各子线程在到达同步点时, 可以获取其他线程的数据, 而不是执行屏障逻辑
  • Exchanger不会阻塞主线程

Phaser

  • 更加强大和灵活的屏障, 相比CyclicBarrier,可以实现多段同步, 而且可以动态地注册和取消注册线程

Semaphore

  • Semaphore用于限制同一时间并发访问的线程数量

大任务分割

ForkJoinPool线程池可以把一个大任务递归地分拆成小任务并行执行,任务类必须继承自RecursiveTaskRecursiveAction, 但代码较为繁琐, 简单场景推荐使用进一步封装的parallelStream()

应使用parallelStream(), 它的性能优于stream().parallel()

常用并发容器

阻塞

虽然性能不如非阻塞容器, 阻塞容器也有其适用的场景, 比如:

  • 线程池的任务队列
  • 消息队列使用的的生产者-消费者模式
  • 限流控制

List, Map, Set - Collections.synchronizedList/Map/Set

  • 使用装饰器产生阻塞容器, 内部实现就是在原集合的各方法上加上synchronized, 注意使用迭代器遍历时还是要加synchronized, 以List为例:
List<String> elements = Collections.synchronizedList(new ArrayList<>());
synchronized (elements) {
            for (String element : elements) {
               ...
            }
}

Queue - LinkedBlockingQueue

  • JDK没有提供Collections.synchronizedQueue装饰器, 我们可以用LinkedBlockingQueue, 其内部用ReentrantLock实现, 在构造时, 通常会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE

非阻塞

非阻塞容器提供更好的并发性能

List - CopyOnWriteArrayList

CopyOnWriteArrayList 线程安全的核心在于其采用了 写时复制(Copy-On-Write)  策略,当需要修改( addsetremove 等操作) 时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。

Map, Set - ConcurrentHashMap

在 JDK1.7 的时候, ConcurrentHashMap使用分段锁(Segment), 当一个线程需要修改某个段的数据时,只需要获取该段对应的锁,而不会影响其他段的操作。这样可以减小锁的粒度,从而提高并发性能。

到了 JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment 的概念,并发控制使用 synchronizedCAS 实现。虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。

如果需要Set类型的容器, 可以调用ConcurrentHashMap.keySet()获取

Queue - ConcurrentLinkedQueue

ConcurrentLinkedQueue 内部使用 CAS 实现, 代码我们就不分析了。

容器内并发场景

// TODO

附录

Java内存模型(Java Memory Model)

image.png

线程状态

state-machine-example-java-6-thread-states.png

注意Blocked和Waiting的区别:

  • Blocked: 处于锁竞争状态, 但未获取到锁
  • Waiting: 等待其他线程执行完成, 会将持有的锁释放

线程池状态

image.png

  1. 线程池创建后处于RUNNING状态。

  2. 调用shutdown()方法后处于SHUTDOWN状态,线程池不能接受新的任务,清除一些空闲worker,不会等待阻塞队列的任务完成。

  3. 调用shutdownNow()方法后处于STOP状态,线程池不能接受新的任务,中断所有线程,阻塞队列中没有被执行的任务全部丢弃。此时,poolsize = 0,阻塞队列的size = 0

  4. 当所有的任务已终止,ctl记录的任务数量为0,线程池会变为TIDYING状态。接着会执行terminated()函数。

  5. 线程池处在TIDYING状态时,执行完terminated()方法之后,就会由 TIDYING变为TERMINATED状态。

ThreadPoolExecutor中有一个控制状态的属性叫ctl,它是一个AtomicInteger类型的变量。线程池状态就是通过AtomicInteger类型的成员变量ctl来获取的。

线程池任务提交流程

image.png

CAS - Compare and Swap

以原子类AtomicIntegerincrementAndGet()方法为例,它使用基于CAS算法的自旋锁(Spin Lock)实现, 可用代码简单描述为:

public int incrementAndGet(AtomicInteger var) {
    int prev, next;
    do {
        prev = var.get();
        next = prev + 1;
    } while (!var.compareAndSet(prev, next));
    return next;
}

如果AtomicInteger的当前值是prev,那么就更新为next,返回true。如果AtomicInteger的当前值不是prev,就什么也不干,返回false。配合do ... while循环,假设有其他线程修改了AtomicInteger的值,会再走一次do里面的逻辑, 重新get()最新的值, 保证最终的结果是正确的。

那么这里的compareAndSet()方法是怎么获取到最新的当前值的呢? 其实, CAS在JDK中是通过 C++ 内联汇编的形式实现, 通过 JNI 调用的。