Loading

13-JUC(下)

1. 同步器

1.1 CountDownLatch

  • 当一个或多个线程调用 await() 时,这些线程会阻塞;
  • 其它线程调用 countDown() 会将计数器减 1(调用该方法的线程不会阻塞);
  • 当计数器的值变为 0 时(减少计数),因 await() 阻塞的线程会被唤醒,继续执行。
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // [减少计数] 让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒
        CountDownLatch cd = new CountDownLatch(6);

        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "被吃掉 ...");
                cd.countDown();
            }, String.valueOf(i)).start();
        }

        cd.await(); // 等待计数器归零,然后再向下执行。
        System.out.println(Thread.currentThread().getName() + "吃饱了 ...");
    }
}

1.2 CyclicBarrier

CyclicBarrier 的字面意思是可循环(Cyclic) 使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫“同步点”)时被阻塞(线程进入屏障是通过 await() 方法),直到最后一个线程到达屏障时,屏障才会开门。只有等屏障开了,所有被屏障拦截的线程才会继续干活。

示例代码:

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("七龙珠集齐!召唤神龙!");
        });

        for (int i = 1; i <= 7; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"星龙珠被收集");
                try {
                    cyclicBarrier.await(); // 线程阻塞
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"星龙珠合成...");
            }, String.valueOf(i)).start();
        }
    }
}

打印结果:

2星龙珠被收集
5星龙珠被收集
4星龙珠被收集
1星龙珠被收集
3星龙珠被收集
7星龙珠被收集
6星龙珠被收集
七龙珠集齐!召唤神龙!
6星龙珠合成...
4星龙珠合成...
5星龙珠合成...
2星龙珠合成...
7星龙珠合成...
3星龙珠合成...
1星龙珠合成...

1.3 Semaphore

信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。在信号量上我们定义 2 种操作:

  • acquire(获取):当一个线程调用 acquire 操作时,它要么通过成功获取信号量(信号量减 1),要么一直等下去,直到有线程释放信号量或超时。
  • release(释放):实际上会将信号量的值加 1,然后唤醒等待的线程。

示例代码:

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 3 个停车位
        Semaphore sp = new Semaphore(3);
        // 6 辆汽车
        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                try {
                    sp.acquire();
                    System.out.println(Thread.currentThread().getName() + "号车驶入停车位");
                    TimeUnit.SECONDS.sleep(3);
                    System.out.println(Thread.currentThread().getName() + "号车驶出停车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    sp.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}

打印结果:

1号车驶入停车位
3号车驶入停车位
2号车驶入停车位
1号车驶出停车位
3号车驶出停车位
4号车驶入停车位
2号车驶出停车位
6号车驶入停车位
5号车驶入停车位
6号车驶出停车位
5号车驶出停车位
4号车驶出停车位

2. 读写锁

class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();
    
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
    public void put(String key, Object value) {
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始写入 ...");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入完毕 ...");
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public Object get(String key) {
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始读入 ...");
            Object value = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读入完毕 ...");
            return value;
        } finally {
            lock.readLock().unlock();
        }
        
    }
}

2. CAS

2.1 说明

CAS 的全程为 Compare-And-Swap,它是一条 CPU 并发原语。功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程的原子性的。

CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类中的各个方法。

UnSafe 是 CAS 的核心类,由于 Java 方法无法直接访问底层 OS,需要通过本地(native)方法来访问,UnSafe 相当于一个后门,基于该类可以直接操作特定内存的数据。UnSafe 类存在于 sun.misc 包种,其内部方法操作可以像 C 的指针一样直接操作内存,因为 Java 中 CAS 操作的执行依赖于 UnSafe 类的方法。

注意 UnSafe 类中的所有方法都是 native 修饰的,也就是说 UnSafe 类中的方法都直接调用 OS 底层资源执行相应的任务。

调用 UnSafe 类中的 CAS 方法,JVM 会帮我们实现出 CAS 汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于 CAS 是一种系统原语,原语属于 OS 用语范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS 是一条 CPU 的原子命令,不会造成所谓的数据不一致问题。

CAS 比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止。

CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改的更改值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。

2.2 ABA

public class ABATest {
  public static void main(String[] args) throws InterruptedException {
    Integer res = 1101;
    Integer aba = 1024;
    final AtomicInteger atomicInteger = new AtomicInteger(res);
    new Thread(() -> {
      atomicInteger.compareAndSet(res, aba);
      atomicInteger.compareAndSet(aba, res);
    }).start();
    TimeUnit.SECONDS.sleep(1);
    atomicInteger.compareAndSet(res, 67);
    // currentValue=67
    System.out.println("currentValue=" + atomicInteger.get());

    // ------------------------- ABA 解决方案 -------------------------

    AtomicStampedReference<Integer> stampedRef = new AtomicStampedReference<>(res, 1);
    int stamp = stampedRef.getStamp();
    new Thread(() -> {
      stampedRef.compareAndSet(res, aba, stampedRef.getStamp(), stampedRef.getStamp() + 1);
      stampedRef.compareAndSet(aba, res, stampedRef.getStamp(), stampedRef.getStamp() + 1);
    }).start();
    TimeUnit.SECONDS.sleep(1);
    stampedRef.compareAndSet(res, 67, stamp, stamp + 1);
    // currentValue=1101, currentVersion=3
    System.out.println("currentValue=" + stampedRef.getReference() + ", currentVersion=" + stampedRef.getStamp());
  }
}

3. 不同维度锁分类

3.1 读写锁

class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();

    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始写入 ...");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入完毕 ...");
        } finally {
            lock.writeLock().unlock();
        }
    }

    public Object get(String key) {
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始读入 ...");
            Object value = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读入完毕 ...");
            return value;
        } finally {
            lock.readLock().unlock();
        }

    }
}

3.2 公平/非公平锁

⽣活中,排队讲求先来后到视为“公平”。程序中的公平性也是符合请求锁的绝对时间的,其实就是 FIFO,否则视为“不公平”。

按序排队公平锁,就是判断同步队列是否还有先驱节点的存在,如果没有先驱节点才能获取锁;而非公平锁是先占先得,不管这个事的,只要能抢获到同步状态就可以。

public class ReentrantLock implements Lock, java.io.Serializable {

  abstract static class Sync extends AbstractQueuedSynchronizer {
    /**
     * Performs non-fair tryLock.  tryAcquire is implemented in
     * subclasses, but both need nonfair try for trylock method.
     */
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

    // ...

  }

  static final class FairSync extends Sync {

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

    // ...

  }
}

(1)为什么默认非公平锁?

恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从 CPU 的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用 CPU 的时间片,尽量减少 CPU 空闲状态时间。

使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当 1 个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

(2)非公平锁可能会导致什么问题?

非公平锁忽视排队规则,就有可能导致有些线程长时间在排队,也没有机会获取到锁,造成“锁饥饿”。

(3)什么时候用公平?什么时候用非公平?

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。

3.3 死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

4. 阻塞队列

4.1 概念

  • 试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素。
  • 试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增。

4.2 用处

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起。

为什么需要 BlockingQueue?

好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切 BlockingQueue 都给你一手包办了。

在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

4.3 架构&种类

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列
  • LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为 integer.MAX_VALUE)阻塞队列
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列
  • DelayQueue:使用优先级队列实现的延迟无界阻塞队列
  • SynchronousQueue:同步队列,不存储元素的阻塞队列,也即单个元素的队列
  • LinkedTransferQueue:由链表组成的无界阻塞队列
  • LinkedBlockingDeque:由链表组成的双向阻塞队列

4.4 示例代码

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

public class BlockQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        // 1. 抛出异常
        // throwExTest();
        // 2. 特殊值
        // retBoolTest();
        // 3. 阻塞
        // blockingTest();
        // 4. 超时退出
        timeoutTest();
    }

    private static void timeoutTest() throws InterruptedException {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        System.out.println(queue.offer("a")); // true
        System.out.println(queue.offer("b")); // true
        System.out.println(queue.offer("c")); // true
        System.out.println(queue.offer("d", 3, TimeUnit.SECONDS)); // false
        System.out.println(queue.poll()); // a
        System.out.println(queue.poll()); // b
        System.out.println(queue.poll()); // c
        System.out.println(queue.poll(4, TimeUnit.SECONDS)); // null
    }

    private static void blockingTest() throws InterruptedException {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        queue.put("a");
        queue.put("b");
        queue.put("c");
        // queue.put("d"); -> blocking ...
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
        // System.out.println(queue.take()); -> blocking ...
    }


    private static void retBoolTest() {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        System.out.println(queue.offer("a")); // true
        System.out.println(queue.offer("b")); // true
        System.out.println(queue.offer("c")); // true
        System.out.println(queue.peek()); // a
        System.out.println(queue.offer("d")); // false
        System.out.println(queue.poll()); // a
        System.out.println(queue.poll()); // b
        System.out.println(queue.poll()); // c
        System.out.println(queue.poll()); // null
    }

    private static void throwExTest() {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        // 1. 添加
        System.out.println(queue.add("a")); // true
        System.out.println(queue.add("b")); // true
        System.out.println(queue.add("c")); // true
        System.out.println(queue.element()); // a
        // System.out.println(queue.add("d")); -> IllegalStateException: Queue full

        // 2. 移除
        System.out.println(queue.remove());
        System.out.println(queue.remove());
        System.out.println(queue.remove());
        // System.out.println(queue.remove()); -> NoSuchElementException
    }
}

5. 线程池

5.1 引入

10 年前单核 CPU 电脑,假的多线程,CPU 需要来回切换;现在是多核电脑,多个线程各自跑在独立的 CPU 上,不用切换效率高。

线程池的优势:线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果任务数量超过了线程最大数量,超出数量的任务排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

线程池的主要特点:线程复用;控制最大并发数;管理线程

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控。

5.2 使用

5.2.1 架构说明

Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor 这几个类。

5.2.2 有关 API

  • Executors.newSingleThreadExecutor():一个任务一个任务的执行,一池一线程。
  • Executors.newFixedThreadPool(N):执行长期任务性能好,创建一个有 N 个固定的线程的线程池。
  • Executors.newCachedThreadPool():执行很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重用它们。可扩容,遇强则强。

示例代码:

public class ThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(3); // 固定个数
        ExecutorService threadPool2 = Executors.newSingleThreadExecutor(); // 单个
        ExecutorService threadPool3 = Executors.newCachedThreadPool(); // 可扩展
        for (int i = 1; i <= 500; i++) {
            threadPool3.execute(()->{
                System.out.println(Thread.currentThread().getName() + "处理任务");
            });
        }
        threadPool.shutdown();
        threadPool2.shutdown();
        threadPool3.shutdown();
    }
}

5.2.3 底层源码

ThreadPoolExecutor

public ThreadPoolExecutor(
            int corePoolSize,
            int maximumPoolSize,
            long keepAliveTime,
            TimeUnit unit,
            BlockingQueue<Runnable> workQueue,
            ThreadFactory threadFactory,
            RejectedExecutionHandler handler) {

    if (corePoolSize < 0 || maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize || keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();

    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
  1. corePoolSize:线程池中的常驻核心线程数(惰性加载)
  2. maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须 >= 1。
  3. keepAliveTime:多余的空闲线程的存活时间。当池中线程数量超过 corePoolSize 并且线程空闲时间达到 keepAliveTime 时,多余线程会被销毁直到只剩下 corePoolSize 个线程为止。
  4. unit:keepAliveTime 的单位
  5. workQueue:任务队列,被提交但尚未被执行的任务。
  6. threadFactory:表示生成线程池中工作线程的线程工厂。用于创建线程,一般默认的即可。
  7. handler:拒绝策略。表示当队列满了,并且工作线程大于等于线程池的 maximumPoolSize 时如何来拒绝请求执行的 Runnable 的策略。

在工作中单一的/固定数的/可变的 3 种创建线程池的方法哪个用的多?注意,哪个都不用!

5.3 工作原理

提交优先级与执行优先级:

  • 提交优先级:核心线程、等待队列、非核心线程;
  • 执行优先级:核心线程、非核心线程、等待队列。

1. 在创建了线程池后,线程池中的线程数为 0。

2. 当调用 execute() 方法添加一个请求任务时,线程池会做出如下判断:

  • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
  • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
  • 如果这个时候队列满了且正在运行的线程数量还小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个新来的任务
  • 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。

4. 当一个线程无事可做超过一定的时间(keepAliveTime) 时,线程会判断:如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以,当线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

5.4 拒绝策略

等待队列已经排满了,再也塞不下新任务了。同时,线程池中的 max 线程也达到了,无法继续为新任务服务。这个时候我们就需要拒绝策略机制合理的处理这个问题。

示例代码:20 个任务

public class ThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                3L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        try {
            for (int i = 0; i < 20; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

JDK内置的拒绝策略(以下均实现了 RejectedExecutionHandle<I>

  • AbortPolicy(默认):直接抛出 RejectedExecutionException 阻止系统正常运行。
    pool-1-thread-1
    pool-1-thread-4
    pool-1-thread-1
    pool-1-thread-1
    pool-1-thread-3
    pool-1-thread-2
    pool-1-thread-4
    pool-1-thread-5
    java.util.concurrent.RejectedExecutionException: Task cn.edu.nuist.threadpool
        .ThreadPoolTest$$Lambda$1/821270929@85ede7b rejected from java.util.concurrent
        .ThreadPoolExecutor@5674cd4d
        [Running, pool size = 5, active threads = 0, queued tasks = 0, completed tasks = 8]
    
  • CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
    pool-1-thread-1
    pool-1-thread-3
    pool-1-thread-2
    main <- 将某些任务回退到调用者
    pool-1-thread-3
    pool-1-thread-1
    pool-1-thread-3
    pool-1-thread-5
    main <- 将某些任务回退到调用者
    pool-1-thread-4
    pool-1-thread-2
    pool-1-thread-3
    pool-1-thread-1
    pool-1-thread-2
    pool-1-thread-4
    pool-1-thread-1
    pool-1-thread-2
    pool-1-thread-3
    pool-1-thread-1
    pool-1-thread-4
    
  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。
    pool-1-thread-1
    pool-1-thread-4
    pool-1-thread-3
    pool-1-thread-3
    pool-1-thread-2
    pool-1-thread-1
    pool-1-thread-4
    pool-1-thread-5
    
  • DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。
    pool-1-thread-1
    pool-1-thread-3
    pool-1-thread-2
    pool-1-thread-1
    pool-1-thread-4
    pool-1-thread-4
    pool-1-thread-4
    pool-1-thread-4
    pool-1-thread-5
    pool-1-thread-4
    pool-1-thread-3
    pool-1-thread-4
    pool-1-thread-5
    pool-1-thread-3
    

5.5 如何设计线程池

5.5.1 回答思路

摘自公众号:yes的练级攻略

这种设计类问题还是一样,先说下理解,表明你是知道这个东西的用处和原理的,然后开始 BB。基本上就是按照现有的设计来说,再添加一些个人见解。

线程池讲白了就是存储线程的一个容器,池内保存之前建立过的线程来重复执行任务,减少创建和销毁线程的开销,提高任务的响应速度,并便于线程的管理。

我个人觉得如果要设计一个线程池的话得考虑池内工作线程的管理、任务编排执行、线程池超负荷处理方案、监控等方面。

要将初始化线程数、核心线程数、最大线程池都暴露出来可配置,包括超过核心线程数的线程空闲消亡相关配置。

然后任务的存储结构也得可配置,可以是无界队列也可以是有界队列,也可以根据配置,分多个队列来分配不同优先级的任务,也可以采用 stealing 的机制来提高线程的利用率。

再提供配置来表明此线程池是 IO 密集型还是 CPU 密集型来改变任务的执行策略。

超负荷的方案可以有多种,包括丢弃任务、拒绝任务并抛出异常、丢弃最旧的任务或自定义等等。

至于监控的话,线程池设计要埋好点,暴露出用于监控的接口,如已处理任务数、待处理任务数、正在运行的线程数、拒绝的任务数等等信息。

我觉得基本上这样答就差不多了,等着面试官的追问就好。注意不需要跟面试官解释什么叫核心线程数之类的,都懂的没必要。

当然这种开放型问题还是仁者见仁智者见智,我这个不是标准答案,仅供参考。建议把线程池相关的关键字都要说出来,表面你对线程池的内部原理的理解是透彻的。

5.5.2 IO/CPU 密集型

https://blog.csdn.net/youanyyou/article/details/78990156

  • CPU 密集型(CPU bound)
    • CPU 密集型也叫计算密集型,指的是系统的硬盘、内存性能相对 CPU 要好很多,此时,系统运作大部分的状况是 CPU Loading 100%,CPU 要读/写 I/O(硬盘/内存),I/O 在很短的时间就可以完成,而 CPU 还有许多运算要处理,CPU Loading 很高。
    • 在多重程序系统中,大部份时间用来做计算、逻辑判断等 CPU 动作的程序称之 CPU bound。例如一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部份时间用在三角函数和开根号的计算,便是属于 CPU bound 的程序。CPU bound 的程序一般而言 CPU 占用率相当高。这可能是因为任务本身不太需要访问 I/O 设备,也可能是因为程序是多线程实现因此屏蔽掉了等待 I/O 的时间。
  • IO 密集型(I/O bound)
    • IO 密集型指的是系统的 CPU 性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是 CPU 在等 I/O (硬盘/内存) 的读/写操作,此时 CPU Loading 并不高。
    • I/O bound 的程序一般在达到性能极限时,CPU 占用率仍然较低。这可能是因为任务本身需要大量 I/O 操作,而 pipeline 做得不是很好,没有充分利用处理器能力。

CPU密集型 vs IO密集型

我们可以把任务分为计算密集型和 IO 密集型。

计算密集型任务的特点是要进行大量的计算,消耗 CPU 资源,比如计算圆周率、对视频进行高清解码等等,全靠 CPU 的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU 执行任务的效率就越低,所以,要最高效地利用 CPU,计算密集型任务同时进行的数量应当等于 CPU 的核心数。计算密集型任务由于主要消耗 CPU 资源,因此,代码运行效率至关重要。Python 这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用 C 语言编写。

涉及到网络、磁盘 IO 的任务都是 IO 密集型任务,这类任务的特点是 CPU 消耗很少,任务的大部分时间都在等待 IO 操作完成(因为 IO 的速度远远低于 CPU 和内存的速度)。对于 IO 密集型任务,任务越多,CPU 效率越高,但也有一个限度。常见的大部分任务都是 IO 密集型任务,比如 Web 应用。IO 密集型任务执行期间,99% 的时间都花在 IO 上,花在 CPU 上的时间很少,因此,用运行速度极快的 C 语言替换用 Python 这样运行速度极低的脚本语言,完全无法提升运行效率。对于 IO 密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C 语言最差。

总之,计算密集型程序适合 C 语言多线程,I/O 密集型适合脚本语言开发的多线程。

6. 分支合并框架

Fork:把一个复杂任务进行分拆,大事化小;Join:把分拆任务的结果进行合并。

  • ForkJoinPool:分支合并池
  • ForkJoinTask:类比“FutureTask”
  • RecursiveTask:递归任务,继承后可以实现递归调用的任务

public class ForkJoinDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyTask myTask = new MyTask(0, 100);
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
        System.out.println(forkJoinTask.get());
    }
}

class MyTask extends RecursiveTask<Integer> {
    public static final int CALCULATE_RANGE = 10;
    private int begin;
    private  int end;
    private int result;

    public MyTask(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        if ((end - begin) <= CALCULATE_RANGE) {
            for (int i = begin; i <= end; i++) {
                result += i;
            }
        } else {
            int mid = (begin + end) >> 1;
            MyTask task1 = new MyTask(begin, mid);
            MyTask task2 = new MyTask(mid+1, end);
            task1.fork();
            task2.fork();
            result = task1.join() + task2.join();
        }
        return result;
    }
}

7. 中断协商机制

如何停止、中断一个运行中的线程?

首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop/Thread.suspend/Thread.resume 都已经被废弃了。

其次,在 Java 中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java 提供了一种用于停止线程的机制 —— 中断。

在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务逻辑。

  • 通过一个 volatile 变量实现
  • 通过 AtomicBoolean
  • 通过 Thread 类自带的中断 API 实现

中断只是一种协作机制,Java 没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的 interrupt() 方法,该方法也仅仅是将线程对象的中断标识设成 true;接着你需要自己写代码不断地检测当前线程的标识位,如果为 true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。

每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为 true 表示中断,为 false 表示未中断;通过调用线程对象的 interrupt() 方法将该线程的标识位设为 true;可以在别的线程中调用,也可以在自己的线程中调用。

API 说明
void interrupt() 实例方法;仅仅是设置线程的中断状态为 true,不会停止线程;
static boolean interrupted() 静态方法;判断线程是否被中断,并清除当前中断状态。这个方法做了两件事:① 返回当前线程的中断状态;② 将当前线程的中断状态设为 false;
boolean isInterrupted() 实例方法;判断当前线程是否被中断(通过检查中断标志位)。

具体来说,当对一个线程,调用 interrupt() 时:

  • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true(中断只是一种协同机制,修改中断标识位仅此而已!不是立刻 stop 打断);
    被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。
  • 如果线程处于被阻塞状态(例如处于 sleep, wait, join 等状态),在别的线程中调用当前线程对象的 interrupt() 方法,那么线程将立即退出被阻塞状态,并抛出一个 InterruptedException 异常。
public static void main(String[] args) throws InterruptedException {
    Thread a = new Thread(() -> {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("|---> Thread-a exit.");
                break;
            }
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
                // Thread.currentThread().interrupt();
            }
            System.out.println("Thread-a run.");
        }
    });
    Thread b = new Thread(() -> a.interrupt());
    a.start();
    TimeUnit.SECONDS.sleep(3);
    b.start();
}

控制台打印:

查看 Thread 源码:

public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

// 该方法连续调用两次,第二次调用结果将返回 false(中断标识被清空)
// 除非当前线程在第一次和第二次调用该方法之间再次被 interrupt.
public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

public boolean isInterrupted() {
    return isInterrupted(false);
}

// 中断状态将会根据传入的 ClearInterrupted 参数值确定是否重置
private native boolean isInterrupted(boolean ClearInterrupted);

private native void interrupt0();

8. park&unpark

8.1 引入

park()/unpark() 这两个方法都是 LockSupport 类名下的方法,park() 用来暂停线程,unpark() 用来将暂停的线程恢复。

【引入】3 种让线程等待和唤醒的方法

  1. 使用 Object 中的 wait() 方法让线程等待,notify() 方法唤醒线程;
    • Object 类中的 wait、notify、notifyAll 用于线程等待和唤醒的方法,都必须在 synchronized 内部执行;
    • 先 wait 后 notify、notifyall 方法,等待中的线程才会被唤醒,否则无法唤醒。
  2. 使用 JUC 包中 Condition 的 await() 方法让线程等待,signal() 方法唤醒线程;
    • 调用 Condition 中线程等待和唤醒的方法的前提:要在 lock 和 unlock 方法中,要有锁才能调用;
    • 先 await 后 signal 才 OK,否则线程无法被唤醒。
  3. LockSupport 类中的 park()unpark() 分别用来阻塞当前线程以及唤醒指定被阻塞的线程。

park & unpark 是以线程为单位来阻塞唤醒线程,而 notify 只能随机唤醒一个等待线程 / notifyAll 是唤醒所有等待线程,就不那么精确

8.2 使用

LockSupport 是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。

LockSupport 提供 park() 和 unpark() 方法实现阻塞线程和解除线程阻塞的过程。归根结底,LockSupport 调用的是 Unsafe 类中的 native 方法。

public static void park() {
    UNSAFE.park(false, 0L);
}

public static void unpark(Thread thread) {
    if (thread != null) UNSAFE.unpark(thread);
}
  • LockSupport 使用一种名为 Permit(凭证)的概念来做到阻塞和唤醒的功能,每个线程都有一个〈凭证〉,permit 只有两个值:0 和 1。可以把〈凭证〉看成是一种 0/1 信号量,但与 Semaphore 不同的是,〈凭证〉的累加上限是 1。
  • permit 默认是 0,所以一开始调用 LockSupport.park(),当前线程就会阻塞,直到别的线程将当前线程的 permit 设置为 1,park() 方法会被唤醒,然后会将 permit 再次设置为 0 并返回。
  • 当线程调用 unpark(t) 方法后,就会将 t 线程的 permit 设置为 1(重复调用 unpark() 不会累积凭证)会自动唤醒 t 线程,即之前阻塞中的 LockSupport.park() 方法会立即返回。

(1)为什么可以先唤醒线程后阻塞线程?

因为 unpark() 获得了一个〈凭证〉,之后再调用 park() 就可以名正言顺地消费〈凭证〉,故不会阻塞。所以,若先执行 unpark(t) 方法,则后续在 t 线程中调用 park() 等于白调。

(2)为什么唤醒两次后阻塞两次,最终还是会阻塞线程?

因为〈凭证〉的数量最多只有 1,无论调用几次 unpark() 都只会增加一个〈凭证〉;而调用多次 park() 却需要消费两个〈凭证〉,〈凭证〉不够,不能放行。

8.3 补充

当线程执行到 LockSupport.park(t) 这句之后,线程会进入到什么状态呢?

可以看到,线程进入到 WAITING,这里的状态是线程在 JVM 中的线程状态,那么这个线程在操作系统中的状态又是什么呢?

根据上面的堆栈信息,可以看到操作系统的线程 ID=0xde9,先将这个十六进制的 0xde9 转成十进制 3561。通过 ps 命令查看本进程中的操作系统线程状态:

从图中看到,线程的状态是 Sleep。

说完了线程的状态,那么再来说下如何解除线程的 WAITING/Sleep,让线程继续运行呢?

LockSupport#park 让线程等待后,唤醒方式除了调用 unpark,还可以通过调用线程的 interrupt() 方法,main 给等待的线程发送中断协商信号,来唤醒线程。

public static void main(String[] args) throws InterruptedException {
  Thread t = new Thread(() -> {
    Thread t1 = Thread.currentThread();
    System.out.println(t1.getName() + " 开始执行... 此时的中断标志位:" + t1.isInterrupted());
    LockSupport.park();
    System.out.println(t1.getName() + " 被唤醒... 此时的中断标志位:" + t1.isInterrupted());
  });
  t.start();

  TimeUnit.SECONDS.sleep(3);
  t.interrupt();
  System.out.println("after MAIN invoke t.interrupt(), 此时的中断标志位:" + t.isInterrupted());

  TimeUnit.SECONDS.sleep(3);
  System.out.println("after t#run <线程处于非活跃状态>, 此时的中断标志位:" + t.isInterrupted());
}

// ======================= 控制台打印 =======================
// Thread-0 开始执行... 此时的中断标志位:false
// after MAIN invoke t.interrupt(), 此时的中断标志位:true
// Thread-0 被唤醒... 此时的中断标志位:true
// after t#run <线程处于非活跃状态>, 此时的中断标志位:false
  • 线程 park 时被 interrupt,会将线程唤醒,但是唤醒后线程无法再次 park,因为此时打断标记已经变为 true,如果想再次 park 线程,需要手动调用线程的 interrupted() 方法(该方法返回线程当前打断标记并将打断状态重置为 false),如此方能再次 park;
  • isInterrupted() 与 interrupted() 比较:首先 isInterrupted() 是实例方法,interrupted() 是静态方法,它们的用处都是查看当前打断的状态,但是 isInterrupted() 查看线程的时候,不会将打断标记清空,也就是置为 false;interrupted() 查看线程打断状态后,会将打断标志置为 false,也就是清空打断标记;
  • 简单来说,interrupt() 类似于 setter 设置中断值,isInterrupted() 类似于 getter 获取中断值,interrupted() 类似于 getter + setter 先获取中断值,然后清除标志。

9. 原子操作类

9.1 FieldUpdater

AtomicIntegerFieldUpdater

/**
 * @author 6x7
 * @Description 以一种线程安全的方式操作非线程安全对象内的某些字段
 * @createTime 2022年04月27日
 */
public class AtomicIntegerFieldUpdaterDemo {

  public static void main(String[] args) throws InterruptedException {
    BankAccount bankAccount = new BankAccount();
    CountDownLatch countDownLatch = new CountDownLatch(10);
    for (int i = 0; i < 10; i++) {
      new Thread(() -> {
        bankAccount.transfer(bankAccount);
        countDownLatch.countDown();
      }).start();
    }
    countDownLatch.await();
    System.out.println("[End] " + bankAccount.balance);
  }

  static class BankAccount {
    // other field ...

    private volatile int balance;

    AtomicIntegerFieldUpdater balanceUpdater =
            AtomicIntegerFieldUpdater.newUpdater(BankAccount.class, "balance");

    public void transfer(BankAccount bankAccount) {
      int newestBalance = balanceUpdater.incrementAndGet(bankAccount);
      System.out.println("当前余额: " + newestBalance);
    }

  }
}

AtomicReferenceFieldUpdater

/**
 * @author 6x7
 * @Description 多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,要求只能初始化一次
 * @createTime 2022年04月27日
 */
public class AtomicReferenceFieldUpdaterDemo {

  static class TargetRes {

    private volatile Boolean isInit = Boolean.FALSE;

    AtomicReferenceFieldUpdater<TargetRes, Boolean> initFieldUpdater =
            AtomicReferenceFieldUpdater.newUpdater(TargetRes.class, Boolean.class, "isInit");

    public void init(TargetRes myVar) {
      if (initFieldUpdater.compareAndSet(myVar, Boolean.FALSE, Boolean.TRUE)) {
        System.out.println(Thread.currentThread().getName() + "\t" + "--- init start!");
        try {
          TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "--- init over!");
      } else {
        System.out.println(Thread.currentThread().getName() + "\t" + "--- init failed!");
      }
    }
  }

  public static void main(String[] args) {
    TargetRes myVar = new TargetRes();

    for (int i = 1; i <= 5; i++) {
      new Thread(() -> myVar.init(myVar)).start();
    }
  }

}

9.2 LongAdder

a. 引入

《阿里 Java 开发手册》#编程规约#并发控制#17:

  • 热点商品点赞计算器,点赞数累加统计,不要求实时精确;
  • 一个很大的 List,里面都是 int 类型,如何实现累加,说说思路;

b. 原理

(1)LongAdder 是 Striped64 的子类,Striped64 有几个比较重要的成员;

/**
 * Number of CPUS, to place bound on table size
 * CPU 数量,即 cells 数组的最大长度
 */
static final int NCPU = Runtime.getRuntime().availableProcessors();

/**
 * Table of cells. When non-null, size is a power of 2.
 * cells 数组,为 2 的幂,2,4,8,16 ...,方便以后位运算
 */
transient volatile Cell[] cells;

/**
 * Base value, used mainly when there is no contention, but also as
 * a fallback during table initialization races. Updated via CAS.
 * 基础 value 值,当并发较低时,只累加该值主要用于没有竞争的情况,通过 CAS 更新。
 */
transient volatile long base;

/**
 * Spinlock (locked via CAS) used when resizing and/or creating Cells.
 * 创建或者扩容 Cells 数组时使用的自旋锁变量调整单元格大小(扩容),创建单元格时使用的锁。
 */
transient volatile int cellsBusy;

/**
 * CASes the cellsBusy field from 0 to 1 to acquire lock.
 * 通过 CAS 操作修改 cellsBusy 的值,CAS 成功代表获取锁,返回 true
 */
final boolean casCellsBusy() {
    return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
}

/**
 * Returns the probe value for the current thread.
 * Duplicated from ThreadLocalRandom because of packaging restrictions.
 * 获取当前线程的 hash 值
 */
static final int getProbe() {
    return UNSAFE.getInt(Thread.currentThread(), PROBE);
}

/**
 * Pseudo-randomly advances and records the given probe value for the given thread.
 * Duplicated from ThreadLocalRandom because of packaging restrictions.
 * 重置当前线程的 hash 值
 */
static final int advanceProbe(int probe) {
    probe ^= probe << 13;   // xorshift
    probe ^= probe >>> 17;
    probe ^= probe << 5;
    UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
    return probe;
}

(2)Cell 是 java.util.concurrent.atomic.Striped64 中的一个内部类;

/**
 * Padded variant of AtomicLong supporting only raw accesses plus CAS.
 *
 * JVM intrinsics note: It would be possible to use a release-only
 * form of CAS here, if it were provided.
 */
@sun.misc.Contended static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long valueOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> ak = Cell.class;
            valueOffset = UNSAFE.objectFieldOffset
                (ak.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

(3)LongAdder 为什么这么快?

LongAdder 的基本思路就是“分散热点”,将 value 值分散到一个 Cell[] 中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行 CAS 操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的 long 值,只要将各个槽中的变量值累加返回。

sum() 会将所有 Cell[] 中的 value 和 base 累加作为返回值,核心的思想就是将之前 AtomicLong 一个 value 的更新压力分散到多个 value 中去,从而降级更新热点。

LongAdder 在无竞争的情况,跟 AtomicLong 一样,对同一个 base 进行操作,当出现竞争关系时则是采用化整为零的做法,从空间换时间,用一个数组 cells,将一个 value 拆分进这个数组 cells。多个线程需要同时对 value 进行操作时候,可以对线程 id 进行 hash 得到一个 hash 值,再根据 hash 值映射到这个数组 cells 的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组 cells 的所有值和无竞争值 base 都加起来作为最终结果。

  • base 变量:非竞态条件下,直接累加到该变量上;
  • Cell 数组:竞态条件下,累加个各个线程自己的槽 Cell[i] 中;

c. 源码

LongAdder

/**
 * Adds the given value.
 *
 * @param x the value to add
 */
public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        if (as == null
                || (m = as.length - 1) < 0
                || (a = as[getProbe() & m]) == null
                || !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

Striped64

/**
 * Handles cases of updates involving initialization, resizing,
 * creating new Cells, and/or contention. See above for explanation.
 * This method suffers the usual non-modularity problems of optimistic
 * retry code, relying on rechecked sets of reads.
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * 上述代码首先给当前线程分配一个hash值,然后进入一个for(;;)自旋,这个自旋分为 3 个分支:
 *     CASE1:Cell[]已经初始化
 *     CASE2:Cell[]未初始化(首次新建)
 *     CASE3:Cell[]正在初始化中
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * @param x the value 需要增加的值,一般默认是 1.
 * @param fn the update function, or null for add (this convention avoids
 *          the need for an extra field or function in LongAdder). 传 null
 * @param wasUncontended false if CAS failed before call 竞争标识
 *        false 代表有竞争。只有 cells 初始化之后,并且当前线程 CAS 竞争修改失败,才会是 false
 */
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
    int h;
    if ((h = getProbe()) == 0) {
        ThreadLocalRandom.current(); // force initialization
        h = getProbe();
        wasUncontended = true;
    }
    boolean collide = false;                // True if last slot nonempty
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        if ((as = cells) != null && (n = as.length) > 0) {
            if ((a = as[(n - 1) & h]) == null) {
                if (cellsBusy == 0) {       // Try to attach new Cell
                    Cell r = new Cell(x);   // Optimistically create
                    if (cellsBusy == 0 && casCellsBusy()) {
                        boolean created = false;
                        try {               // Recheck under lock
                            Cell[] rs; int m, j;
                            if ((rs = cells) != null
                                    && (m = rs.length) > 0
                                    && rs[j = (m - 1) & h] == null) {
                                rs[j] = r;
                                created = true;
                            }
                        } finally {
                            cellsBusy = 0;
                        }
                        if (created)
                            break;
                        continue;           // Slot is now non-empty
                    }
                }
                collide = false;
            }
            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // Continue after rehash
            else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
                break;
            else if (n >= NCPU || cells != as)
                collide = false;            // At max size or stale
            else if (!collide)
                collide = true;
            else if (cellsBusy == 0 && casCellsBusy()) {
                try {
                    if (cells == as) {      // Expand table unless stale
                        Cell[] rs = new Cell[n << 1];
                        for (int i = 0; i < n; ++i)
                            rs[i] = as[i];
                        cells = rs;
                    }
                } finally {
                    cellsBusy = 0;
                }
                collide = false;
                continue;                   // Retry with expanded table
            }
            h = advanceProbe(h);
        }
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            boolean init = false;
            try {                           // Initialize table
                if (cells == as) {
                    Cell[] rs = new Cell[2];
                    rs[h & 1] = new Cell(x);
                    cells = rs;
                    init = true;
                }
            } finally {
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
            break;                          // Fall back on using base
    }
}

sum() 会将 Cell[] 中所有的 value 和 base 累加作为返回值。核心的思想就是将之前 AtomicLong 一个 value 的更新压力分散到多个 value 中去,从而降级更新热点。

/**
 * Returns the current sum. The returned value is NOT an atomic snapshot; invocation
 * in the absence of concurrent updates returns an accurate result, but concurrent
 * updates that occur while the sum is being calculated might not be incorporated.
 * @return the sum
 */
public long sum() {
    Cell[] as = cells; Cell a;
    long sum = base;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

sum() 执行时,并没有限制对 base 和 cells 的更新,所以 LongAdder 不是强一致性的,它是最终一致性的。首先,最终返回的 sum 局部变量,初始被复制为 base,而最终返回时,很可能 base 已经被更新了,而此时局部变量 sum 不会更新,造成不一致;其次,这里对 cell[] 的读取也无法保证是最后一次写入的值。所以,sum() 方法在没有并发的情况下,可以获得正确的结果。

d. 小结

posted @ 2021-05-21 12:04  tree6x7  阅读(46)  评论(0编辑  收藏  举报