美文网首页
并发--共享模型之工具

并发--共享模型之工具

作者: zhemehao819 | 来源:发表于2021-12-29 21:38 被阅读0次

    线程池

    1.1 自定义线程池

    先自定义任务队列

    class BlockingQueue<T> {
        // 1. 任务队列
        private Deque<T> queue = new ArrayDeque<>();
    
        // 2. 锁
        private ReentrantLock lock = new ReentrantLock();
    
        // 3. 生产者条件变量
        private Condition fullWaitSet = lock.newCondition();
        // 4. 消费者条件变量
        private Condition emptyWaitSet = lock.newCondition();
    
        // 5. 队列容量
        private int capcity;
    
        public BlockingQueue(int capcity) {
            this.capcity = capcity;
        }
    
        // 带超时阻塞获取
        public T poll(long timeout, TimeUnit unit) {
            lock.lock();
            try {
                // 将 timeout 统一转换为 纳秒
                long nanos = unit.toNanos(timeout);
                while (queue.isEmpty()) {
                    try {
                        // 返回值是剩余时间
                        if (nanos <= 0) {
                            return null;
                        }
                        nanos = emptyWaitSet.awaitNanos(nanos);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                T t = queue.removeFirst();
                fullWaitSet.signal();
                return t;
            } finally {
                lock.unlock();
            }
        }
    
        // 阻塞获取
        public T take() {
            lock.lock();
            try {
                while (queue.isEmpty()) {
                    try {
                        emptyWaitSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                T t = queue.removeFirst();
                fullWaitSet.signal();
                return t;
            } finally {
                lock.unlock();
            }
        }
    
        // 阻塞添加
        public void put(T task) {
            lock.lock();
            try {
                while (queue.size() == capcity) {
                    try {
                        log.debug("等待加入任务队列 {} ...", task);
                        fullWaitSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("加入任务队列 {}", task);
                queue.addLast(task);
                emptyWaitSet.signal();
            } finally {
                lock.unlock();
            }
        }
    
        // 带超时时间阻塞添加
        public boolean offer(T task, long timeout, TimeUnit timeUnit) {
            lock.lock();
            try {
                long nanos = timeUnit.toNanos(timeout);
                while (queue.size() == capcity) {
                    try {
                        if (nanos <= 0) {
                            return false;
                        }
                        log.debug("等待加入任务队列 {} ...", task);
                        nanos = fullWaitSet.awaitNanos(nanos);
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("加入任务队列 {}", task);
                queue.addLast(task);
                emptyWaitSet.signal();
                return true;
            } finally {
                lock.unlock();
            }
        }
    
        public int size() {
            lock.lock();
            try {
                return queue.size();
            } finally {
                lock.unlock();
            }
        }
    
        public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
            lock.lock();
            try {
                // 判断队列是否满
                if (queue.size() == capcity) {
                    rejectPolicy.reject(this, task);
                } else {
                    // 有空闲 
                    log.debug("加入任务队列 {}", task);
                    queue.addLast(task);
                    emptyWaitSet.signal();
                }
            } finally {
                lock.unlock();
            }
        }
    }
    

    自定义线程池

    class ThreadPool {
        // 任务队列
        private BlockingQueue<Runnable> taskQueue;
    
        // 线程集合
        private HashSet<Worker> workers = new HashSet<>();
    
        // 核心线程数
        private int coreSize;
    
        // 获取任务时的超时时间
        private long timeout;
        private TimeUnit timeUnit;
    
        //拒绝策略
        private RejectPolicy<Runnable> rejectPolicy;
    
        public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity, RejectPolicy<Runnable> rejectPolicy) {
            this.coreSize = coreSize;
            this.timeout = timeout;
            this.timeUnit = timeUnit;
            this.taskQueue = new BlockingQueue<>(queueCapcity);
            this.rejectPolicy = rejectPolicy;
        }
    
        // 执行任务
        public void execute(Runnable task) {
            // 当任务数没有超过 coreSize 时,直接交给 worker 对象执行
            // 如果任务数超过 coreSize 时,加入任务队列暂存
            synchronized (workers) {
                if (workers.size() < coreSize) {
                    Worker worker = new Worker(task);
                    log.debug("新增 worker{}, {}", worker, task);
                    workers.add(worker);
                    worker.start();
                } else {
                    //taskQueue.put(task);
                    // 1) 死等
                    // 2) 带超时等待
                    // 3) 让调用者放弃任务执行
                    // 4) 让调用者抛出异常
                    // 5) 让调用者自己执行任务
                    taskQueue.tryPut(rejectPolicy, task);
                }
            }
        }
    
    
        class Worker extends Thread {
            private Runnable task;
    
            public Worker(Runnable task) {
                this.task = task;
            }
    
            @Override
            public void run() {
                // 执行任务
                // 1) 当 task 不为空,执行任务
                // 2) 当 task 执行完毕,再接着从任务队列获取任务并执行
                // 因为该线程需要不断去执行任务,使用while
                // while (task != null || (task = taskQueue.take()) != null) {
                while (task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
                    try {
                            log.debug("正在执行...{}", task);
                            task.run();
                    } catch (Exception e) {
                            e.printStackTrace();
                    } finally {
                            task = null;
                    }
                }
                // 此时说明该线程执行完所有任务,从线程集合中移除
                synchronized (workers) {
                    log.debug("worker 被移除{}", this);
                    workers.remove(this);
                }
            }
        }
    }
    

    测试:

    public class Test2 {
        public static void main(String[] args) {
            ThreadPool threadPool = new ThreadPool(1,
                    1000, TimeUnit.MILLISECONDS, 1, (queue, task) -> {
                // 1. 死等
                // queue.put(task);
    
                // 2) 带超时等待
                // queue.offer(task, 1500, TimeUnit.MILLISECONDS);
    
                // 3) 让调用者放弃任务执行
                // log.debug("放弃{}", task);
    
                // 4) 让调用者抛出异常
                // throw new RuntimeException("任务执行失败 " + task);
    
                // 5) 让调用者自己执行任务
                task.run();
            });
            for (int i = 0; i < 4; i++) {
                int j = I;
                threadPool.execute(() -> {
                    try {
                        Thread.sleep(1000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug("{}", j);
                });
            }
        }
    }
    

    定义拒绝策略接口:

    @FunctionalInterface // 拒绝策略 
    interface RejectPolicy<T> {
        void reject(BlockingQueue<T> queue, T task);
    }
    

    1.2 ThreadPoolExecutor

    1) 线程池状态

    ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量

    从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING

    这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值

    // c 为旧值, ctlOf 返回结果为新值
    ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));
    
    // rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们 
    private static int ctlOf(int rs, int wc) { return rs | wc; }
    

    2) 构造方法

    public ThreadPoolExecutor(int corePoolSize, 
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit, 
                              BlockingQueue<Runnable> workQueue, 
                              ThreadFactory threadFactory, 
                              RejectedExecutionHandler handler)
    
    • corePoolSize 核心线程数目 (最多保留的线程数)
    • maximumPoolSize 最大线程数目
    • keepAliveTime 生存时间 - 针对救急线程
    • unit 时间单位 - 针对救急线程
    • workQueue 阻塞队列
    • threadFactory 线程工厂 - 可以为线程创建时起个好名字
    • handler 拒绝策略

    工作方式:

    • 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
    • 当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排队, 直到有空闲的线程。
    • 如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。
    • 如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现
      (1)AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
      (2)CallerRunsPolicy 让调用者运行任务
      (3)DiscardPolicy 放弃本次任务
      (4)DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
      (5)Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
      (6)Netty 的实现,是创建一个新线程来执行任务
      (7)ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
      (8)PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
    • 当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTime 和 unit 来控制。

    根据这个构造方法,JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池.

    3) newFixedThreadPool

    public static ExecutorService newFixedThreadPool(int nThreads) { 
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    

    特点:

    • 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间阻塞
    • 队列是无界的,可以放任意数量的任务

    评价 适用于任务量已知,相对耗时的任务

            ExecutorService pool = Executors.newFixedThreadPool(2, new ThreadFactory() {
                private AtomicInteger t = new AtomicInteger(1);
    
                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, "mypool_t" + t.getAndIncrement());
                }
            });
    
            pool.execute(() -> {
                log.debug("1");
            });
    
            pool.execute(() -> {
                log.debug("2");
            });
    
            pool.execute(() -> {
                log.debug("3");
            });
        }
    

    4) newCachedThreadPool

    public static ExecutorService newCachedThreadPool() { 
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    

    特点:

    • 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着
      • 全部都是救急线程(60s 后可以回收)
      • 救急线程可以无限创建
    • 队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交 货)
        SynchronousQueue<Integer> integers = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                log.debug("putting {} ", 1);
                integers.put(1);
                log.debug("{} putted...", 1);
                log.debug("putting...{} ", 2);
                integers.put(2);
                log.debug("{} putted...", 2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1").start();
    
        sleep(1);
    
        new Thread(() -> { try {
                log.debug("taking {}", 1);
                integers.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t2").start();
        
        sleep(1);
        
        new Thread(() -> { try {
                log.debug("taking {}", 2);
                integers.take();
            } catch (InterruptedException e) {
                e.printStackTrace(); 
            }
        },"t3").start();
    

    输出:

    11:48:15.500 c.TestSynchronousQueue [t1] - putting 1 
    11:48:16.500 c.TestSynchronousQueue [t2] - taking 1 
    11:48:16.500 c.TestSynchronousQueue [t1] - 1 putted... 
    11:48:16.500 c.TestSynchronousQueue [t1] - putting...2 
    11:48:17.502 c.TestSynchronousQueue [t3] - taking 2 
    11:48:17.503 c.TestSynchronousQueue [t1] - 2 putted...
    

    评价 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线 程。 适合任务数比较密集,但每个任务执行时间较短的情况

    5) newSingleThreadExecutor

        public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }
    

    使用场景:
    希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。

    区别:

    • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作;
    • Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
      • FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
    • Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改
      • 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
        public static void test2() {
            ExecutorService pool = Executors.newSingleThreadExecutor();
            pool.execute(() -> {
                log.debug("1");
                int i = 1 / 0;
            });
    
            pool.execute(() -> {
                log.debug("2");
            });
    
            pool.execute(() -> {
                log.debug("3");
            });
        }
    

    6) 提交任务

    // 执行任务
    void execute(Runnable command);
    
    // 提交任务 task,用返回值 Future 获得任务执行结果 
    <T> Future<T> submit(Callable<T> task);
    
    // 提交 tasks 中所有任务
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
                              throws InterruptedException;
    
    // 提交 tasks 中所有任务,带超时时间
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                        long timeout, TimeUnit unit) 
                    throws InterruptedException;
    
    // 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消 
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
                    throws InterruptedException, ExecutionException;
    
    //提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间 
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
                throws InterruptedException, ExecutionException, TimeoutException;
    

    案例:

    @Slf4j(topic = "c.TestSubmit")
    public class TestSubmit {
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            ExecutorService pool = Executors.newFixedThreadPool(1);
    
        }
    
        private static void method3(ExecutorService pool) throws InterruptedException, ExecutionException {
            String result = pool.invokeAny(Arrays.asList(
                    () -> {
                        log.debug("begin 1");
                        Thread.sleep(1000);
                        log.debug("end 1");
                        return "1";
                    },
                    () -> {
                        log.debug("begin 2");
                        Thread.sleep(500);
                        log.debug("end 2");
                        return "2";
                    },
                    () -> {
                        log.debug("begin 3");
                        Thread.sleep(2000);
                        log.debug("end 3");
                        return "3";
                    }
            ));
            log.debug("{}", result);
        }
    
        private static void method2(ExecutorService pool) throws InterruptedException {
            List<Future<String>> futures = pool.invokeAll(Arrays.asList(
                    () -> {
                        log.debug("begin");
                        Thread.sleep(1000);
                        return "1";
                    },
                    () -> {
                        log.debug("begin");
                        Thread.sleep(500);
                        return "2";
                    },
                    () -> {
                        log.debug("begin");
                        Thread.sleep(2000);
                        return "3";
                    }
            ));
    
            futures.forEach( f ->  {
                try {
                    log.debug("{}", f.get());
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            });
        }
    
        private static void method1(ExecutorService pool) throws InterruptedException, ExecutionException {
            Future<String> future = pool.submit(() -> {
                log.debug("running");
                Thread.sleep(1000);
                return "ok";
            });
    
            log.debug("{}", future.get());
        }
    }
    

    7) 关闭线程池

    /*
    线程池状态变为 SHUTDOWN
    - 不会接收新任务
    - 但已提交任务会执行完
    - 此方法不会阻塞调用线程的执行
    */
    void shutdown();
    
    /*
    线程池状态变为 STOP
    - 不会接收新任务
    - 会将队列中的任务返回
    - 并用 interrupt 的方式中断正在执行的任务
    */
    List<Runnable> shutdownNow();
    

    异步模式之工作线程

    定义

    让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式。

    创建多少线程池合适

    • 过小会导致程序不能充分地利用系统资源、容易导致饥饿
    • 过大会导致更多的线程上下文切换,占用更多内存
    CPU 密集型运算

    通常采用 cpu 核数 + 1 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因 导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费

    I/O 密集型运算

    CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率。

    经验公式如下:
    线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间

    例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式:
    4 * 100% * 100% / 50% = 8

    8) 任务调度线程池

    ScheduledExecutorService

            ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
    
            // 添加两个任务,希望它们都在 1s 后执行
            executor.schedule(() -> {
                System.out.println("任务1,执行时间:" + new Date());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                }
            }, 1000, TimeUnit.MILLISECONDS);
            
            executor.schedule(() -> {
                System.out.println("任务2,执行时间:" + new Date());
            }, 1000, TimeUnit.MILLISECONDS);
    

    以上两任务会同时执行

    scheduleAtFixedRate 例子:

            ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
            log.debug("start...");
            pool.scheduleAtFixedRate(() -> {
                log.debug("running...");
            }, 1, 1, TimeUnit.SECONDS);
    

    输出:

    21:45:43.167 c.TestTimer [main] - start... 
    21:45:44.215 c.TestTimer [pool-1-thread-1] - running...
    21:45:45.215 c.TestTimer [pool-1-thread-1] - running...
    21:45:46.215 c.TestTimer [pool-1-thread-1] - running...
    21:45:47.215 c.TestTimer [pool-1-thread-1] - running...
    

    9) 正确处理执行任务异常

    方法1:主动捉异常

    ExecutorService pool = Executors.newFixedThreadPool(1); 
    pool.submit(() -> {
      try { 
        log.debug("task1"); 
        int i = 1 / 0;
      } catch (Exception e) { 
        log.error("error:", e);
      } 
    });
    

    方法2:使用 Future

    ExecutorService pool = Executors.newFixedThreadPool(1); 
    Future<Boolean> f = pool.submit(() -> {
      log.debug("task1"); 
      int i = 1 / 0; 
      return true;
    });
    log.debug("result:{}", f.get());
    

    10) Tomcat 线程池

    • LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore 后面再讲
    • Acceptor 只负责【接收新的 socket 连接】
    • Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】
    • 一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理
    • Executor 线程池中的工作线程最终负责【处理请求】

    Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同,如果总线程数达到 maximumPoolSize,这时不会立刻抛 RejectedExecutionException 异常,而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常。
    源码 tomcat-7.0.42

      public void execute(Runnable command, long timeout, TimeUnit unit) {
            submittedCount.incrementAndGet();
            try {
                super.execute(command);
            } catch (RejectedExecutionException rx) {
                if (super.getQueue() instanceof TaskQueue) {
                    final TaskQueue queue = (TaskQueue)super.getQueue();
                    try {
                        // 使任务从新进入阻塞队列
                        if (!queue.force(command, timeout, unit)) {
                            submittedCount.decrementAndGet();
                            throw new RejectedExecutionException("Queue capacity is full.");
                        }
                    } catch (InterruptedException x) {
                        submittedCount.decrementAndGet();
                        Thread.interrupted();
                        throw new RejectedExecutionException(x);
                    }
                } else {
                    submittedCount.decrementAndGet();
                    throw rx;
                }
            }
        }
    
        public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
            if ( parent.isShutdown() )
                throw new RejectedExecutionException(
                        "Executor not running, can't force a command into the queue"
                );
            return super.offer(o,timeout,unit); //forces the item onto the queue, to be used if the task
            is rejected
        }
    

    Connector 配置

    Executor 线程配置

    1.3 Fork/Join

    1) 概念

    Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型 运算

    所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解

    Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率.

    Fork/Join 默认会创建与 cpu 核心数大小相同的线程池

    2) 使用

    提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecursiveAction(没有返回值),例如下 面定义了一个对 1~n 之间的整数求和的任务

    // 1~n 之间整数的和
    @Slf4j(topic = "c.MyTask")
    class MyTask extends RecursiveTask<Integer> {
    
        private int n;
    
        public MyTask(int n) {
            this.n = n;
        }
    
        @Override
        public String toString() {
            return "{" + n + '}';
        }
    
        @Override
        protected Integer compute() {
            // 如果 n 已经为 1,可以求得结果了
            if (n == 1) {
                log.debug("join() {}", n);
                return n;
            }
    
            // 将任务进行拆分(fork)
            AddTask1 t1 = new AddTask1(n - 1);
            t1.fork();
            log.debug("fork() {} + {}", n, t1);
    
            // 合并(join)结果
            int result = n + t1.join();
            log.debug("join() {} + {} = {}", n, t1, result);
            return result;
        }
    }
    

    然后提交给 ForkJoinPool 来执行

        public static void main(String[] args) {
            ForkJoinPool pool = new ForkJoinPool(4);
            System.out.println(pool.invoke(new MyTask(5)));
    
            // new MyTask(5)  5+ new MyTask(4)  4 + new MyTask(3)  3 + new MyTask(2)  2 + new MyTask(1)
        }
    

    用图来表示

    改进

    @Slf4j(topic = "c.AddTask")
    class AddTask2 extends RecursiveTask<Integer> {
    
        int begin;
        int end;
    
        public AddTask2(int begin, int end) {
            this.begin = begin;
            this.end = end;
        }
    
        @Override
        public String toString() {
            return "{" + begin + "," + end + '}';
        }
    
        @Override
        protected Integer compute() {
            if (begin == end) {
                log.debug("join() {}", begin);
                return begin;
            }
            if (end - begin == 1) {
                log.debug("join() {} + {} = {}", begin, end, end + begin);
                return end + begin;
            }
            int mid = (end + begin) / 2;
    
            AddTask2 t1 = new AddTask2(begin, mid - 1);
            t1.fork();
            AddTask2 t2 = new AddTask2(mid + 1, end);
            t2.fork();
            log.debug("fork() {} + {} + {} = ?", mid, t1, t2);
    
            int result = mid + t1.join() + t2.join();
            log.debug("join() {} + {} + {} = {}", mid, t1, t2, result);
            return result;
        }
    }
    

    然后提交给 ForkJoinPool 来执行

        public static void main(String[] args) {
            ForkJoinPool pool = new ForkJoinPool(4);
    //        System.out.println(pool.invoke(new AddTask1(5)));
            System.out.println(pool.invoke(new AddTask3(1, 5)));
        }
    

    用图来表示


    8.2、JUC

    1、AQS 原理

    1. 概述

    全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架

    特点:

    • 用 state 属性来表示资源的状态(独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取 锁和释放锁
      -- getState - 获取 state 状态
      -- setState - 设置 state 状态
      -- compareAndSetState - cas 机制设置 state 状态
      -- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
    • 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
    • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

    子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)

    • tryAcquire
    • tryRelease
    • tryAcquireShared
    • tryReleaseShared
    • isHeldExclusively

    获取锁的姿势

    // 如果获取锁失败
    if (!tryAcquire(arg)) {
      // 入队, 可以选择阻塞当前线程 park unpark 
    }
    

    释放锁的姿势

    // 如果释放锁成功
    if (tryRelease(arg)) {
      // 让阻塞线程恢复运行 
    }
    

    2. 实现不可重入锁

    自定义同步器

        class MySync extends AbstractQueuedSynchronizer {
            @Override
            protected boolean tryAcquire(int arg) {
                if(compareAndSetState(0, 1)) {
                    // 加上了锁,并设置 owner 为当前线程
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
                return false;
            }
    
            @Override
            protected boolean tryRelease(int arg) {
                setExclusiveOwnerThread(null);
                setState(0); //state 是volatile修饰,可防止之前的代码指令重排
                return true;
            }
    
            @Override // 是否持有独占锁
            protected boolean isHeldExclusively() {
                return getState() == 1;
            }
    
            public Condition newCondition() {
                return new ConditionObject();
            }
        }
    

    自定义锁
    有了自定义同步器,很容易复用 AQS ,实现一个功能完备的自定义锁

    // 自定义锁(不可重入锁)
    class MyLock implements Lock {
    
        private MySync sync = new MySync();
    
        @Override // 加锁(不成功会进入等待队列)
        public void lock() {
            sync.acquire(1);
        }
    
        @Override // 加锁,可打断
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }
    
        @Override // 尝试加锁(一次)
        public boolean tryLock() {
            return sync.tryAcquire(1);
        }
    
        @Override // 尝试加锁,带超时
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(time));
        }
    
        @Override // 解锁
        public void unlock() {
            sync.release(1);
        }
    
        @Override // 创建条件变量
        public Condition newCondition() {
            return sync.newCondition();
        }
    }
    

    测试一下

    public class TestAqs {
        public static void main(String[] args) {
            MyLock lock = new MyLock();
            new Thread(() -> {
                lock.lock();
                try {
                    log.debug("locking...");
                    sleep(1);
                } finally {
                    log.debug("unlocking...");
                    lock.unlock();
                }
            },"t1").start();
    
            new Thread(() -> {
                lock.lock();
                try {
                    log.debug("locking...");
                } finally {
                    log.debug("unlocking...");
                    lock.unlock();
                }
            },"t2").start();
        }
    }
    

    输出

    22:29:28.727 c.TestAqs [t1] - locking... 
    22:29:29.732 c.TestAqs [t1] - unlocking... 
    22:29:29.732 c.TestAqs [t2] - locking... 
    22:29:29.732 c.TestAqs [t2] - unlocking...
    

    不可重入测试 如果改为下面代码,会发现自己也会被挡住(只会打印一次 locking)

    2、ReentrantLock 原理

    从这张类图可以知道,ReentrantLock实现了Lock接口,内部维护了一个同步器Sync(继承了AQS),Sync是抽象的,它有两个实现:非公平锁(NonfairSync)和公平锁(FairSync)。

    1. 非公平锁实现原理

    加锁解锁流程
    先从构造器开始看,默认为非公平锁实现

        public ReentrantLock() {
            sync = new NonfairSync();
        }
    

    NonfairSync 继承自 AQS

    没有竞争时:

    看ReentrantLock源码是如何加锁,加锁方法lock(),里面调用的是sync.lock()同步器的lock方法如下,cas修改状态成功后把当前线程设置为锁的持有者。

            final void lock() {
                if (compareAndSetState(0, 1))
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    acquire(1);
            }
    

    第一个竞争出现时:

        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
    

    Thread-1 执行了

    1. CAS 尝试将 state 由 0 改为 1,结果失败,进入acquire(1)分支;
    2. 进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败
    3. 接下来进入 addWaiter 逻辑,构造 Node 队列。(如上代码)
    • 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
    • Node 的创建是懒惰的
    • 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程
        final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (;;) {
                    final Node p = node.predecessor();
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null; // help GC
                        failed = false;
                        return interrupted;
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    

    当前线程进入 acquireQueued 逻辑

    1. acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
    2. 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
    3. 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false
    1. shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
    2. 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true
    3. 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)

    再次有多个线程经历上述过程竞争失败,变成这个样子

    Thread-0 释放锁,调用ReentrantLock的unlock方法,进入调用同步器的 tryRelease 流程,如果成功

    • 设置 exclusiveOwnerThread 为 null
    • state = 0
        public final boolean release(int arg) {
            if (tryRelease(arg)) {
                Node h = head;
                if (h != null && h.waitStatus != 0)
                    unparkSuccessor(h);
                return true;
            }
            return false;
        }
    

    当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程;

    找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1 ;

    回到 Thread-1 的 acquireQueued 流程,去尝试获得锁。

    如果加锁成功(没有竞争),会设置

    • exclusiveOwnerThread 为 Thread-1,state = 1
    • head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
    • 原本的 head 因为从链表断开,而可被垃圾回收

    如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了

    如果不巧又被 Thread-4 占了先

    • Thread-4 被设置为 exclusiveOwnerThread,state = 1
    • Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞

    2. 可重入原理

    static final class NonfairSync extends Sync {
        // ...
    
        // Sync 继承过来的方法, 方便阅读, 放在此处
        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()) {
                // state++
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    
        // Sync 继承过来的方法, 方便阅读, 放在此处
        protected final boolean tryRelease(int releases) {
            // state--
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 支持锁重入, 只有 state 减为 0, 才释放成功
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
    }
    

    3. 可打断原理

    不可打断模式
    在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了

    // Sync 继承自 AQS
    static final class NonfairSync extends Sync {
        // ...
    
        private final boolean parkAndCheckInterrupt() {
            // 如果打断标记已经是 true, 则 park 会失效
            LockSupport.park(this);
            // interrupted 会清除打断标记
            return Thread.interrupted();
        }
    
        final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (; ; ) {
                    final Node p = node.predecessor();
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null;
                        failed = false;
                        // 还是需要获得锁后, 才能返回打断状态
                        return interrupted;
                    }
                    if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
                        // 如果是因为 interrupt 被唤醒, 返回打断状态为 true
                        interrupted = true;
                    }
                }
            } finally {
                if (failed) cancelAcquire(node);
            }
        }
    
        public final void acquire(int arg) {
            if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
                selfInterrupt();
            }
        }
    
        static void selfInterrupt() {
            // 重新产生一次中断 
            Thread.currentThread().interrupt();
        }
    }
    

    可打断模式

    static final class NonfairSync extends Sync {
        public final void acquireInterruptibly(int arg) throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
                // 如果没有获得到锁, 进入 (一)
            if (!tryAcquire(arg))
                doAcquireInterruptibly(arg);
        }
    
        // (一) 可打断的获取锁流程
        private void doAcquireInterruptibly(int arg) throws InterruptedException {
            final Node node = addWaiter(Node.EXCLUSIVE);
            boolean failed = true;
            try {
                for (; ; ) {
                    final Node p = node.predecessor();
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null; // help GC failed = false;
                        return;
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                            parkAndCheckInterrupt()) {
                        // 在 park 过程中如果被 interrupt 会进入此
                        // 这时候抛出异常, 而不会再次进入 for (;;)
                        throw new InterruptedException();
                    }
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    }
    

    4. 公平锁实现原理

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
    
        final void lock() {
            acquire(1);
        }
    
        // AQS 继承过来的方法, 方便阅读, 放在此处
        public final void acquire(int arg) {
            if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
                selfInterrupt();
            }
        }
    
        // 与非公平锁主要区别在于 tryAcquire 方法的实现
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 先检查 AQS 队列中是否有前驱节点, 没有才去竞争. //如果是非公平锁不会去检查等待队列
                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;
        }
    
        // (一) AQS 继承过来的方法, 方便阅读, 放在此处
        public final boolean hasQueuedPredecessors() {
            Node t = tail;
            Node h = head;
            Node s;
            // h != t 时表示队列中有 Node
            return h != t && (
                    // (s = h.next) == null 表示队列中还有没有老二
                    (s = h.next) == null ||
                    // 或者队列中老二线程不是此线程
                    s.thread != Thread.currentThread()
            );
        }
    }
    

    5. 条件变量实现原理

    每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject

    await 流程

    开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程 创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部

    接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁

    unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功

    park 阻塞 Thread-0

    signal 流程

    假设 Thread-1 要来唤醒 Thread-0

    进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node

    执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的 waitStatus 改为 -1

    Thread-1 释放锁,进入 unlock 流程,略。以上可对照源码阅读。

    3. 读写锁

    3.1 ReentrantReadWriteLock

    当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,提高性能。 类似于数据库中的 select ... from ... lock in share mode

    提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

    class DataContainer {
        private Object data;
        private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
        private ReentrantReadWriteLock.ReadLock r = rw.readLock();
        private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
    
        public Object read() {
            log.debug("获取读锁...");
            r.lock();
            try {
                log.debug("读取");
                sleep(1);
                return data;
            } finally {
                log.debug("释放读锁...");
                r.unlock();
            }
        }
    
        public void write() {
            log.debug("获取写锁...");
            w.lock();
            try {
                log.debug("写入");
                sleep(1);
            } finally {
                log.debug("释放写锁...");
                w.unlock();
            }
        }
    }
    

    测试 读锁-读锁 可以并发:

        public static void main(String[] args) throws InterruptedException {
            DataContainer dataContainer = new DataContainer();
            new Thread(() -> {
                dataContainer.read();
            }, "t1").start();
    
            new Thread(() -> {
                dataContainer.read();
            }, "t2").start();
        }
    

    测试 读锁-写锁 相互阻塞

        public static void main(String[] args) throws InterruptedException {
            DataContainer dataContainer = new DataContainer();
            new Thread(() -> {
                dataContainer.read();
            }, "t1").start();
    
            Thread.sleep(100);
            new Thread(() -> {
                dataContainer.read();
            }, "t2").start();
        }
    

    写锁-写锁 也是相互阻塞的,这里就不测试了

    注意事项

    • 读锁不支持条件变量
    • 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
        r.lock();
        try {
            // ...
            w.lock();
            try {
                // ...
            } finally {
                w.unlock();
            }
        } finally {
            r.unlock();
        }
    
    • 重入时降级支持:即持有写锁的情况下去获取读锁
    class CachedData {
        Object data;
        // 是否有效,如果失效,需要重新计算 data
        volatile boolean cacheValid;
        final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    
        void processCachedData() {
            rwl.readLock().lock();
            if (!cacheValid) {
                // 获取写锁前必须释放读锁
                rwl.readLock().unlock();
                rwl.writeLock().lock();
                try {
                    // 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新
                    if (!cacheValid) {
                        data = ...
                        cacheValid = true;
                    }
                    // 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存
                    rwl.readLock().lock();
                } finally {
                    rwl.writeLock().unlock();
                }
            }
    
            // 自己用完数据, 释放读锁 
            try {
                use(data);
            } finally {
                rwl.readLock().unlock();
            }
        }
    }
    

    缓存

    缓存更新策略

    更新时,是先清缓存还是先更新数据库

    先清缓存

    先更新数据库

    读写锁实现一致性缓存

    使用读写锁实现一个简单的按需加载缓存

    class GenericDaoCached extends GenericDao {
        private GenericDao dao = new GenericDao();
        private Map<SqlPair, Object> map = new HashMap<>();
        private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    
        @Override
        public <T> List<T> queryList(Class<T> beanClass, String sql, Object... args) {
            return dao.queryList(beanClass, sql, args);
        }
    
        @Override
        public <T> T queryOne(Class<T> beanClass, String sql, Object... args) {
            // 先从缓存中找,找到直接返回
            SqlPair key = new SqlPair(sql, args);;
            rw.readLock().lock();
            try {
                T value = (T) map.get(key);
                if(value != null) {
                    return value;
                }
            } finally {
                rw.readLock().unlock();
            }
            rw.writeLock().lock();
            try {
                // 多个线程。二次检查,防止其他线程再次查询数据库
                T value = (T) map.get(key);
                if(value == null) {
                    // 缓存中没有,查询数据库
                    value = dao.queryOne(beanClass, sql, args);
                    map.put(key, value);
                }
                return value;
            } finally {
                rw.writeLock().unlock();
            }
        }
    
        @Override
        public int update(String sql, Object... args) {
            rw.writeLock().lock();
            try {
                // 先更新库
                int update = dao.update(sql, args);
                // 清空缓存
                map.clear();
                return update;
            } finally {
                rw.writeLock().unlock();
            }
        }
    
        class SqlPair {
            private String sql;
            private Object[] args;
    
            public SqlPair(String sql, Object[] args) {
                this.sql = sql;
                this.args = args;
            }
    
            @Override
            public boolean equals(Object o) {
                if (this == o) {
                    return true;
                }
                if (o == null || getClass() != o.getClass()) {
                    return false;
                }
                SqlPair sqlPair = (SqlPair) o;
                return Objects.equals(sql, sqlPair.sql) &&
                        Arrays.equals(args, sqlPair.args);
            }
    
            @Override
            public int hashCode() {
                int result = Objects.hash(sql);
                result = 31 * result + Arrays.hashCode(args);
                return result;
            }
        }
    }
    

    注意

    • 以上实现体现的是读写锁的应用,保证缓存和数据库的一致性,但有下面的问题没有考虑
      - 适合读多写少,如果写操作比较频繁,以上实现性能低
      - 没有考虑缓存容量
      - 没有考虑缓存过期
      - 只适合单机
      - 并发性还是低,目前只会用一把锁
      - 更新方法太过简单粗暴,清空了所有 key(考虑按类型分区或重新设计 key)
    • 乐观锁实现:用 CAS 去更新

    读写锁原理

    图解流程

    读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个.

    t1 w.lock,t2 r.lock

    对照ReentrantReadWriteLock的源码进行分析

    1. t1 成功上锁,流程与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁 使用的是 state 的高 16 位

    2)t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。如果有写锁占据,那么 tryAcquireShared 返回 -1 表示失败

    tryAcquireShared 返回值表示
    -1 表示失败
    0 表示成功,但后继节点不会继续唤醒
    正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回 1

    3)这时会进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态

    4)t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁
    5)如果没有成功,在 doAcquireShared 内 for (;;) 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (;;) 循环一 次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park

    t3 r.lock,t4 w.lock

    这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子

    t1 w.unlock

    这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功,变成下面的样子

    接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
    这回再来一次 for (;;) 执行 tryAcquireShared 成功则让读锁计数加一

    这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点

    事情还没完,在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行

    这回再来一次 for (;;) 执行 tryAcquireShared 成功则让读锁计数加一

    这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点

    下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点

    t2 r.unlock,t3 r.unlock

    t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零

    t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入 doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即

    之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;;) 这次自己是老二,并且没有其他 竞争,tryAcquire(1) 成功,修改头结点,流程结束

    3.2 StampedLock

    该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用

    加解读锁

    long stamp = lock.readLock(); 
    lock.unlockRead(stamp);
    

    加解写锁

    long stamp = lock.writeLock();
    lock.unlockWrite(stamp);
    

    乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。

    long stamp = lock.tryOptimisticRead(); 
    // 验戳
    if(!lock.validate(stamp)){
        // 锁升级 
    }
    

    提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

    class DataContainerStamped {
        private int data;
        private final StampedLock lock = new StampedLock();
    
        public DataContainerStamped(int data) {
            this.data = data;
        }
    
        public int read(int readTime) {
            long stamp = lock.tryOptimisticRead();
            log.debug("optimistic read locking...{}", stamp);
            sleep(readTime);
            if (lock.validate(stamp)) {
                log.debug("read finish...{}, data:{}", stamp, data);
                return data;
            }
            // 锁升级 - 读锁
            log.debug("updating to read lock... {}", stamp);
            try {
                stamp = lock.readLock();
                log.debug("read lock {}", stamp);
                sleep(readTime);
                log.debug("read finish...{}, data:{}", stamp, data);
                return data;
            } finally {
                log.debug("read unlock {}", stamp);
                lock.unlockRead(stamp);
            }
        }
    
        public void write(int newData) {
            long stamp = lock.writeLock();
            log.debug("write lock {}", stamp);
            try {
                sleep(2);
                this.data = newData;
            } finally {
                log.debug("write unlock {}", stamp);
                lock.unlockWrite(stamp);
            }
        }
    }
    

    测试 读-读 可以优化

        public static void main(String[] args) {
            DataContainerStamped dataContainer = new DataContainerStamped(1);
            new Thread(() -> {
                dataContainer.read(1);
            }, "t1").start();
            sleep(0.5);
            new Thread(() -> {
                dataContainer.read(0);
            }, "t2").start();
        }
    

    输出结果,可以看到实际没有加读锁

    15:58:50.217 c.DataContainerStamped [t1] - optimistic read locking...256 15:58:50.717 c.DataContainerStamped [t2] - optimistic read locking...256 15:58:50.717 c.DataContainerStamped [t2] - read finish...256, data:1 15:58:51.220 c.DataContainerStamped [t1] - read finish...256, data:1
    

    测试 读-写 时优化读补加读锁

        public static void main(String[] args) {
            DataContainerStamped dataContainer = new DataContainerStamped(1);
            new Thread(() -> {
                dataContainer.read(1);
            }, "t1").start();
            sleep(0.5);
            new Thread(() -> {
                dataContainer.write(100);
            }, "t2").start();
        }
    

    输出结果

    15:57:00.219 c.DataContainerStamped [t1] - optimistic read locking...256 15:57:00.717 c.DataContainerStamped [t2] - write lock 384
    15:57:01.225 c.DataContainerStamped [t1] - updating to read lock... 256 15:57:02.719 c.DataContainerStamped [t2] - write unlock 384 
    15:57:02.719 c.DataContainerStamped [t1] - read lock 513
    15:57:03.719 c.DataContainerStamped [t1] - read finish...513, data:1000 15:57:03.719 c.DataContainerStamped [t1] - read unlock 513
    

    注意
    StampedLock 不支持条件变量
    StampedLock 不支持可重入

    4. Semaphore

    基本使用

    信号量,用来限制能同时访问共享资源的线程上限。

        public static void main(String[] args) {
            // 1. 创建 semaphore 对象
            Semaphore semaphore = new Semaphore(3);
    
            // 2. 10个线程同时运行
            for (int i = 0; i < 10; i++) {
                new Thread(() -> {
                    // 3. 获取许可
                    try {
                        semaphore.acquire();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        log.debug("running...");
                        sleep(1);
                        log.debug("end...");
                    } finally {
                        // 4. 释放许可
                        semaphore.release();
                    }
                }).start();
            }
        }
    

    输出:

    07:35:15.485 c.TestSemaphore [Thread-2] - running... 
    07:35:15.485 c.TestSemaphore [Thread-1] - running... 
    07:35:15.485 c.TestSemaphore [Thread-0] - running... 
    07:35:16.490 c.TestSemaphore [Thread-2] - end... 
    07:35:16.490 c.TestSemaphore [Thread-0] - end... 
    07:35:16.490 c.TestSemaphore [Thread-1] - end... 
    07:35:16.490 c.TestSemaphore [Thread-3] - running... 
    07:35:16.490 c.TestSemaphore [Thread-5] - running... 
    07:35:16.490 c.TestSemaphore [Thread-4] - running... 
    07:35:17.490 c.TestSemaphore [Thread-5] - end... 
    07:35:17.490 c.TestSemaphore [Thread-4] - end... 
    07:35:17.490 c.TestSemaphore [Thread-3] - end... 
    07:35:17.490 c.TestSemaphore [Thread-6] - running... 
    07:35:17.490 c.TestSemaphore [Thread-7] - running... 
    07:35:17.490 c.TestSemaphore [Thread-9] - running... 
    07:35:18.491 c.TestSemaphore [Thread-6] - end... 
    07:35:18.491 c.TestSemaphore [Thread-7] - end... 
    07:35:18.491 c.TestSemaphore [Thread-9] - end... 
    07:35:18.491 c.TestSemaphore [Thread-8] - running... 
    07:35:19.492 c.TestSemaphore [Thread-8] - end...
    

    Semaphore 应用

    • 使用 Semaphore 限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机 线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatch 的实现)
    • 用 Semaphore 实现简单连接池,对比『享元模式』下的实现(用wait notify),性能和可读性显然更好, 注意下面的实现中线程数和数据库连接数是相等的
    class Pool {
        // 1. 连接池大小
        private final int poolSize;
    
        // 2. 连接对象数组
        private Connection[] connections;
    
        // 3. 连接状态数组 0 表示空闲, 1 表示繁忙
        private AtomicIntegerArray states;
    
        private Semaphore semaphore;
    
        // 4. 构造方法初始化
        public Pool(int poolSize) {
            this.poolSize = poolSize;
            // 让许可数与资源数一致
            this.semaphore = new Semaphore(poolSize);
            this.connections = new Connection[poolSize];
            this.states = new AtomicIntegerArray(new int[poolSize]);
            for (int i = 0; i < poolSize; i++) {
                connections[i] = new MockConnection("连接" + (i+1));
            }
        }
    
        // 5. 借连接
        public Connection borrow() {// t1, t2, t3
            // 获取许可
            try {
                semaphore.acquire(); // 没有许可的线程,在此等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < poolSize; i++) {
                // 获取空闲连接
                if(states.get(i) == 0) {
                    if (states.compareAndSet(i, 0, 1)) {
                        log.debug("borrow {}", connections[I]);
                        return connections[I];
                    }
                }
            }
            // 不会执行到这里
            return null;
        }
        // 6. 归还连接
        public void free(Connection conn) {
            for (int i = 0; i < poolSize; i++) {
                if (connections[i] == conn) {
                    states.set(i, 0);
                    log.debug("free {}", conn);
                    semaphore.release();
                    break;
                }
            }
        }
    }
    

    Semaphore 原理

    加锁解锁流程

    Semaphore 有点像一个停车场,构造方法参数permits 就好像停车位数量,当线程获得了 permits 就像是获得了停车位,然后停车场显示空余车位减一

    刚开始,permits(state)为 3,这时 5 个线程来获取资源

    假设其中 Thread-1,Thread-2,Thread-4 cas 竞争成功,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列 park 阻塞

    这时 Thread-4 释放了 permits,状态如下

    接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,断开原来的 head 节点,unpark 接 下来的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态

    源码分析

    Semaphore源码分析可参考上图解,从acquire()方法开始分析,release()释放锁。

    5. CountdownLatch

    用来进行线程同步协作,等待所有线程完成倒计时。 其中构造参数用来初始化等待计数值,await() 用来等待计数归零,每个线程使用countDown() 用来让计数减一。

        private static void test4() throws InterruptedException {
            CountDownLatch latch = new CountDownLatch(3);
    
            new Thread(() -> {
                log.debug("begin...");
                sleep(1);
                latch.countDown();
                log.debug("end...{}", latch.getCount());
            }).start();
    
            new Thread(() -> {
                log.debug("begin...");
                sleep(2);
                latch.countDown();
                log.debug("end...{}", latch.getCount());
            }).start();
    
            new Thread(() -> {
                log.debug("begin...");
                sleep(1.5);
                latch.countDown();
                log.debug("end...{}", latch.getCount());
            }).start();
    
            log.debug("waiting...");
            latch.await();
            log.debug("wait end...");
        }
    

    输出:

    18:44:00.778 c.TestCountDownLatch [main] - waiting... 
    18:44:00.778 c.TestCountDownLatch [Thread-2] - begin... 
    18:44:00.778 c.TestCountDownLatch [Thread-0] - begin... 
    18:44:00.778 c.TestCountDownLatch [Thread-1] - begin... 
    18:44:01.782 c.TestCountDownLatch [Thread-0] - end...2 
    18:44:02.283 c.TestCountDownLatch [Thread-2] - end...1 
    18:44:02.782 c.TestCountDownLatch [Thread-1] - end...0 
    18:44:02.782 c.TestCountDownLatch [main] - wait end...
    

    对于上面的案例也可以使用Thread的join来实现,但必须需要每个线程运行结束,而CountDownLatch不需要线程结束。比如线程池中的线程一般就不会结束。

    可以配合线程池使用,改进如下

        private static void test5() {
            CountDownLatch latch = new CountDownLatch(3);
            ExecutorService service = Executors.newFixedThreadPool(4);
            service.submit(() -> {
                log.debug("begin...");
                sleep(1);
                latch.countDown();
                log.debug("end...{}", latch.getCount());
            });
            service.submit(() -> {
                log.debug("begin...");
                sleep(1.5);
                latch.countDown();
                log.debug("end...{}", latch.getCount());
            });
            service.submit(() -> {
                log.debug("begin...");
                sleep(2);
                latch.countDown();
                log.debug("end...{}", latch.getCount());
            });
            service.submit(()->{
                try {
                    log.debug("waiting...");
                    latch.await();
                    log.debug("wait end...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    

    输出:

    18:52:25.831 c.TestCountDownLatch [pool-1-thread-3] - begin... 
    18:52:25.831 c.TestCountDownLatch [pool-1-thread-1] - begin... 18:52:25.831 c.TestCountDownLatch [pool-1-thread-2] - begin... 18:52:25.831 c.TestCountDownLatch [pool-1-thread-4] - waiting... 18:52:26.835 c.TestCountDownLatch [pool-1-thread-1] - end...2 18:52:27.335 c.TestCountDownLatch [pool-1-thread-2] - end...1 18:52:27.835 c.TestCountDownLatch [pool-1-thread-3] - end...0 18:52:27.835 c.TestCountDownLatch [pool-1-thread-4] - wait end...
    

    同步等待多线程准备完毕:

        private static void test2() throws InterruptedException {
            AtomicInteger num = new AtomicInteger(0);
            ExecutorService service = Executors.newFixedThreadPool(10, (r) -> {
                return new Thread(r, "t" + num.getAndIncrement());
            });
            CountDownLatch latch = new CountDownLatch(10);
            String[] all = new String[10];
            Random r = new Random();
            for (int j = 0; j < 10; j++) {
                int x = j;
                service.submit(() -> {
                    for (int i = 0; i <= 100; i++) {
                        try {
                            Thread.sleep(r.nextInt(100));
                        } catch (InterruptedException e) {
                        }
                        all[x] = Thread.currentThread().getName() + "(" + (i + "%") + ")";
                        System.out.print("\r" + Arrays.toString(all));
                    }
                    latch.countDown();
                });
            }
            latch.await();
            System.out.println("\n游戏开始...");
            service.shutdown();
        }
    

    中间输出:

    [t0(52%), t1(47%), t2(51%), t3(40%), t4(49%), t5(44%), t6(49%), t7(52%), t8(46%), t9(46%)]
    

    最后输出:

    [t0(100%), t1(100%), t2(100%), t3(100%), t4(100%), t5(100%), t6(100%), t7(100%), t8(100%), t9(100%)]
    游戏开始...
    

    同步等待多个远程调用结束:

       private static void test3() throws InterruptedException, ExecutionException {
            RestTemplate restTemplate = new RestTemplate();
            log.debug("begin");
            ExecutorService service = Executors.newCachedThreadPool();
            CountDownLatch latch = new CountDownLatch(4);
            Future<Map<String,Object>> f1 = service.submit(() -> {
                Map<String, Object> response = restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1);
                return response;
            });
            Future<Map<String, Object>> f2 = service.submit(() -> {
                Map<String, Object> response1 = restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1);
                return response1;
            });
            Future<Map<String, Object>> f3 = service.submit(() -> {
                Map<String, Object> response1 = restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2);
                return response1;
            });
            Future<Map<String, Object>> f4 = service.submit(() -> {
                Map<String, Object> response3 = restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1);
                return response3;
            });
    
            System.out.println(f1.get());
            System.out.println(f2.get());
            System.out.println(f3.get());
            System.out.println(f4.get());
            log.debug("执行完毕");
            service.shutdown();
        }
    

    如果不需要返回值,就在每个人任务里面调用latch.countDown(),main线程调用latch.await()。

    6. CyclicBarrier

    循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行。类似长途汽车“人满发车”

        public static void main(String[] args) {
            ExecutorService service = Executors.newFixedThreadPool(3);
            CyclicBarrier barrier = new CyclicBarrier(2, ()-> {
                log.debug("task1, task2 finish...");
            });
            for (int i = 0; i < 3; i++) { // task1  task2  task1
                service.submit(() -> {
                    log.debug("task1 begin...");
                    sleep(1);
                    try {
                        barrier.await(); // 2-1=1
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                });
                service.submit(() -> {
                    log.debug("task2 begin...");
                    sleep(2);
                    try {
                        barrier.await(); // 1-1=0
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                });
            }
            service.shutdown();
        }
    

    注意 CyclicBarrier 与 CountDownLatch 的主要区别在于, CyclicBarrier 是可以重用的, CyclicBarrier 可以被比 喻为『人满发车』.

    7. 线程安全集合类概述

    线程安全集合类可以分为三大类:

    • 遗留的线程安全集合如 Hashtable , Vector
    • 使用 Collections 装饰的线程安全集合,如:
      Collections.synchronizedCollection
      Collections.synchronizedList
      Collections.synchronizedMap
      Collections.synchronizedSet
      Collections.synchronizedNavigableMap
      Collections.synchronizedNavigableSet
      Collections.synchronizedSortedMap
      Collections.synchronizedSortedSet
    • java.util.concurrent.*

    重点介绍 java.util.concurrent.* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词: Blocking、CopyOnWrite、Concurrent

    • Blocking 大部分实现基于锁,并提供用来阻塞的方法
    • CopyOnWrite 之类容器修改开销相对较重
    • Concurrent 类型的容器
      • 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
      • 弱一致性
        -遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍 历,这时内容是旧的
        -求大小弱一致性,size 操作未必是 100% 准确
        -读取弱一致性

    遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出 ConcurrentModificationException,不再继续遍历

    8. ConcurrentHashMap

    单词计数

    生成测试数据

        static final String ALPHA = "abcedfghijklmnopqrstuvwxyz";
    
        public static void main(String[] args) {
            int length = ALPHA.length();
            int count = 200;
            List<String> list = new ArrayList<>(length * count);
            for (int i = 0; i < length; i++) {
                char ch = ALPHA.charAt(i);
                for (int j = 0; j < count; j++) {
                    list.add(String.valueOf(ch));
                }
            }
            Collections.shuffle(list);
            for (int i = 0; i < 26; i++) {
                try (PrintWriter out = new PrintWriter(new OutputStreamWriter(
                        new FileOutputStream("tmp/" + (i + 1) + ".txt")))) {
                    String collect = list.subList(i * count, (i + 1) * count).stream()
                            .collect(Collectors.joining("\n"));
                    out.print(collect);
                } catch (IOException e) {
                }
            }
        }
    

    模版代码,模版代码中封装了多线程读取文件的代码:

        private static <V> void demo(Supplier<Map<String, V>> supplier, BiConsumer<Map<String, V>, List<String>> consumer) {
            Map<String, V> counterMap = supplier.get();
            // key value
            // a   200
            // b   200
            List<Thread> ts = new ArrayList<>();
            for (int i = 1; i <= 26; i++) {
                int idx = I;
                Thread thread = new Thread(() -> {
                    List<String> words = readFromFile(idx);
                    consumer.accept(counterMap, words);
                });
                ts.add(thread);
            }
    
            ts.forEach(t -> t.start());
            ts.forEach(t -> {
                try {
                    t.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            System.out.println(counterMap);
        }
    
        public static List<String> readFromFile(int i) {
            ArrayList<String> words = new ArrayList<>();
            try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("tmp/" + i + ".txt")))) {
                while (true) {
                    String word = in.readLine();
                    if (word == null) {
                        break;
                    }
                    words.add(word);
                }
                return words;
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    

    你要做的是实现两个参数

    • 一是提供一个 map 集合,用来存放每个单词的计数结果,key 为单词,value 为计数
    • 二是提供一组操作,保证计数的安全性,会传递 map 集合以及 单词 List

    正确结果输出应该是每个单词出现 200 次

    {a=00, b=200, c=200, d=200, e=200, f=200, g=200, h=200, i=200, j=200, k=200, l=200, m=200, n=200, o=200, p=200, q=200, r=200, s=200, t=200, u=200, v=200, w=200, x=200, y=200, z=200}
    

    下面的实现为:

            demo(
                    // 创建 map 集合
                    // 创建 ConcurrentHashMap 对不对?
                    () -> new HashMap<String, Integer>();
    
                    (map, words) -> {
                        for (String word : words) {
                            // 检查 key 有没有
                            Integer counter = map.get(word);
                            int newValue = counter == null ? 1 : counter + 1;
                            // 没有 则 put
                            map.put(word, newValue);
                        }
                    }
            );
    

    有没有问题?请改进

            demo(
                    // 创建 map 集合
                    // 创建 ConcurrentHashMap 对不对?
                    () -> new ConcurrentHashMap<String, LongAdder>(),
    
                    (map, words) -> {
                        for (String word : words) {
                            // 如果缺少一个 key,则计算生成一个 value , 然后将  key value 放入 map
                            //             a      0
                            LongAdder value = map.computeIfAbsent(word, (key) -> new LongAdder());
                            // 执行累加
                            value.increment(); // 2
                        }
                    }
            );
    

    ConcurrentHashMap 原理

    1. JDK 7 HashMap 并发死链

    测试代码

    注意:

    • java7的Hashmap里的链表新增node是从头插入,Java8是从尾部插入;
    • 要在 JDK 7 下运行,否则扩容机制和 hash 的计算方法都变了
    • 以下测试代码是精心准备的,不要随便改动
    public class TestDeadLink {
        public static void main(String[] args) {
            // 测试 java 7 中哪些数字的 hash 结果相等
            System.out.println("长度为16时,桶下标为1的key");
            for (int i = 0; i < 64; i++) {
                if (hash(i) % 16 == 1) {
                    System.out.println(i);
                }
            }
            System.out.println("长度为32时,桶下标为1的key");
            for (int i = 0; i < 64; i++) {
                if (hash(i) % 32 == 1) {
                    System.out.println(i);
                }
            }
            // 1, 35, 16, 50 当大小为16时,它们在一个桶内
            final HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
            // 放 12 个元素
            map.put(2, null);
            map.put(3, null);
            map.put(4, null);
            map.put(5, null);
            map.put(6, null);
            map.put(7, null);
            map.put(8, null);
            map.put(9, null);
            map.put(10, null);
            map.put(16, null);
            map.put(35, null);
            map.put(1, null);
    
            System.out.println("扩容前大小[main]:"+map.size());
            new Thread() {
                @Override
                public void run() {
                    // 放第 13 个元素, 发生扩容
                    map.put(50, null);
                    System.out.println("扩容后大小[Thread-0]:"+map.size());
                }
            }.start();
            new Thread() {
                @Override
                public void run() {
                    // 放第 13 个元素, 发生扩容
                    map.put(50, null);
                    System.out.println("扩容后大小[Thread-1]:"+map.size());
                }
            }.start();
        }
    
        final static int hash(Object k) {
            int h = 0;
            if (0 != h && k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h ^= k.hashCode();
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }
    }
    

    死链复现
    调试工具使用 idea
    在 HashMap 源码 590 行加断点

    断点的条件如下,目的是让 HashMap 在扩容为 32 时,并且线程为 Thread-0 或 Thread-1 时停下来

    newTable.length==32 && (
    Thread.currentThread().getName().equals("Thread-0")||
    Thread.currentThread().getName().equals("Thread-1") )
    

    断点暂停方式选择 Thread,否则在调试 Thread-0 时,Thread-1 无法恢复运行

    这是为了观察 e 节点和 next 节点的状态,Thread-0 单步执行到 594 行,再 594 处再添加一个断点(条件 Thread.currentThread().getName().equals("Thread-0"))

    这时可以在 Variables 面板观察到 e 和 next 变量,使用 view as -> Object 查看节点状态

    e          (1)->(35)->(16)->null 
    next     (35)->(16)->null
    

    在 Threads 面板选中 Thread-1 恢复运行,可以看到控制台输出新的内容如下,Thread-1 扩容已完成

    newTable[1]     (35)->(1)->null
    

    这时 Thread-0 还停在 594 处, Variables 面板变量的状态已经变化为

     e        (1)->null 
    next     (35)->(1)->null
    

    然后Thread-0 复制index=1的node到newTable[1]变成:

    newTable[1]     (1)->(35)->(1)->null
    newTable[1]     (35)->(1)->(35)->(1)->null
    。。。
    

    小结

    • 究其原因,是因为在多线程环境下使用了非线程安全的 map 集合
    • JDK 8 虽然将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),但仍不意味着能 够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据)
    2. JDK 8 ConcurrentHashMap

    重要属性和内部类

    // 默认为 0
    // 当初始化时, 为 -1
    // 当扩容时, 为 -(1 + 扩容线程数)
    // 当初始化或扩容完成后,为 下一次的扩容的阈值大小 
    private transient volatile int sizeCtl;
    
    // 整个 ConcurrentHashMap 就是一个 Node[]
    static class Node<K,V> implements Map.Entry<K,V> {}
    
    // hash 表
    transient volatile Node<K,V>[] table;
    
    // 扩容时的 新 hash 表
    private transient volatile Node<K,V>[] nextTable;
    
    // 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点 
    static final class ForwardingNode<K,V> extends Node<K,V> {}
    
    // 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 
    Node static final class ReservationNode<K,V> extends Node<K,V> {}
    
    // 作为 treebin 的头节点, 存储 root 和 first
    static final class TreeBin<K,V> extends Node<K,V> {}
    
    // 作为 treebin 的节点, 存储 parent, left, right 
    static final class TreeNode<K,V> extends Node<K,V> {}
    

    重要方法

    // 获取 Node[] 中第 i 个 Node
    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i)
    
    // cas 修改 Node[] 中第 i 个 Node 的值, c 为旧值, v 为新值
    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)
    
    // 直接修改 Node[] 中第 i 个 Node 的值, v 为新值
    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)
    

    构造器分析
    可以看到实现了懒惰初始化,在构造方法中仅仅计算了 table 的大小,以后在第一次使用时才会真正创建

        public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
            if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
                throw new IllegalArgumentException();
            if (initialCapacity < concurrencyLevel) // Use at least as many bins
                initialCapacity = concurrencyLevel; // as estimated threads long size = (long)(1.0 + (long)initialCapacity / loadFactor);
            // tableSizeFor 仍然是保证计算的大小是 2^n, 即 16,32,64 ...
            int cap = (size >= (long) MAXIMUM_CAPACITY) ?
                    MAXIMUM_CAPACITY : tableSizeFor((int) size);
            this.sizeCtl = cap;
        }
    

    get 流程

    public V get(Object key) {
        Node<K, V>[] tab;
        Node<K, V> e, p;
        int n, eh;
        K ek;
    
        // spread 方法能确保返回结果是正数
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (e = tabAt(tab, (n - 1) & h)) != null) {
            // 如果头结点已经是要查找的 key
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            // hash 为负数表示该 bin 在扩容中或是 treebin, 这时调用 find 方法来查找
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
    
            // 正常遍历链表, 用 equals 比较
            while ((e = e.next) != null) {
                if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }
    

    **put 流程 **
    以下数组简称(table),链表简称(bin)

    public V put(K key, V value) {
        return putVal(key, value, false);
    }
    
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        // 其中 spread 方法会综合高位低位, 具有更好的 hash 性
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K, V>[] tab = table; ; ) {
            // f 是链表头节点
            // fh 是链表头结点的 hash
            // i 是链表在 table 中的下标
            Node<K, V> f;
            int n, i, fh;
            // 要创建 table
            if (tab == null || (n = tab.length) == 0)
                // 初始化 table 使用了 cas, 无需 synchronized 创建成功, 进入下一轮循环
                tab = initTable();
                // 要创建链表头节点
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // 添加链表头使用了 cas, 无需 synchronized
                if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null)))
                    break;
            }
            // 帮忙扩容
            else if ((fh = f.hash) == MOVED)
                // 帮忙之后, 进入下一轮循环
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                // 锁住链表头节点
                synchronized (f) {
                    // 再次确认链表头节点没有被移动
                    if (tabAt(tab, i) == f) {
                        // 链表
                        if (fh >= 0) {
                            binCount = 1;
                            // 遍历链表
                            for (Node<K, V> e = f; ; ++binCount) {
                                K ek;
                                // 找到相同的 key
                                if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                                (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    // 更新
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K, V> pred = e;
                                // 已经是最后的节点了, 新增 Node, 追加至链表尾
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K, V>(hash, key, value, null);
                                    break;
                                }
                            }
                        }
                        // 红黑树
                        else if (f instanceof TreeBin) {
                            Node<K, V> p;
                            binCount = 2;
                            // putTreeVal 会看 key 是否已经在树中, 是, 则返回对应的 TreeNode
                            if ((p = ((TreeBin<K, V>) f).putTreeVal(hash, key, value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                    // 释放链表头节点的锁
                }
    
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        // 如果链表长度 >= 树化阈值(8), 进行链表转为红黑树
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 增加 size 计数
        addCount(1L, binCount);
        return null;
    }
    
    private final Node<K, V>[] initTable() {
        Node<K, V>[] tab;
        int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield();
                // 尝试将 sizeCtl 设置为 -1(表示初始化 table)
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                // 获得锁, 创建 table, 这时其它线程会在 while() 循环中 yield 直至 table 创建
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
    
    // check 是之前 binCount 的个数
    private final void addCount(long x, int check) {
        CounterCell[] as;
        long b, s;
        if (
            // 已经有了 counterCells, 向cell 累加
                (as = counterCells) != null ||
                        // 还没有, 向 baseCount 累加
                        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)
        ) {
            CounterCell a;
            long v;
            int m;
            boolean uncontended = true;
            if (
                // 还没有 counterCells
                as == null || (m = as.length - 1) < 0 ||
                // 还没有 cell
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                // cell cas 增加计数失败
                !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
            ) {
                // 创建累加单元数组和cell, 累加重试
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            // 获取元素个数
            s = sumCount();
        }
        if (check >= 0) {
            Node<K, V>[] tab, nt;
            int n, sc;
            while (s >= (long) (sc = sizeCtl) && (tab = table) != null &&
                    (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0)
                        break;
                    // newtable 已经创建了,帮忙扩容
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                // 需要扩容,这时 newtable 未创建
                } else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }
    

    size 计算流程
    size 计算实际发生在 put,remove 改变集合元素的操作之中

    • 没有竞争发生,向 baseCount 累加计数
    • 有竞争发生,新建 counterCells,向其中的一个 cell 累加计数
      • counterCells 初始有两个 cell
      • 如果计数竞争比较激烈,会创建新的 cell 来累加计数
    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long) Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) n);
    }
    
    final long sumCount() {
        CounterCell[] as = counterCells;
        CounterCell a;
        // 将 baseCount 计数与所有 cell 计数累加
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }
    

    Java 8 数组(Node) +( 链表 Node | 红黑树 TreeNode ) 以下数组简称(table),链表简称(bin)

    • 初始化,使用 cas 来保证并发安全,懒惰初始化 table
    • 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程 会用 synchronized 锁住链表头
    • put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素 添加至 bin 的尾部
    • get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 它会让 get 操作在新 table 进行搜索
    • 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时妙的是其它竞争线程也不是无事可 做,它们会帮助把其它 bin 进行扩容,扩容时平均只有 1/6 的节点会把复制到新 table 中
    • size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加 即可

    9. BlockingQueue

    LinkedBlockingQueue 原理

    1. 基本的入队出队
    public class LinkedBlockingQueue<E> extends AbstractQueue<E>
            implements BlockingQueue<E>, java.io.Serializable {
        static class Node<E>  {
            E item;
    
            /**
            * 下列三种情况之一
            * - 真正的后继节点
            * - 自己, 发生在出队时
            * - null, 表示是没有后继节点, 是最后了 
            */
            Node<E> next;
            Node(E x) { item = x; }
        } 
    }
    

    初始化链表 last = head = new Node<E>(null); Dummy 节点用来占位,item 为 null

    当一个节点入队 last = last.next = node;

    再来一个节点入队 last = last.next = node;

    出队

    Node<E> h = head; 
    Node<E> first = h.next; 
    h.next = h; // help GC 
    head = first;
    E x = first.item; 
    first.item = null;
    return x;
    

    h = head

    first = h.next

    h.next = h

    head = first;

    E  x = first.item; 
    first.item = null; 
    return x;
    
    2. 加锁分析

    ==高明之处==在于用了两把锁和 dummy 节点

    • 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
    • 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
      -消费者与消费者线程仍然串行
      -生产者与生产者线程仍然串行

    线程安全分析

    • 当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是 head 节点的线程安全。两把锁保证了入队和出队没有竞争
    • 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争
    • 当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞
    // 用于 put(阻塞) offer(非阻塞)
    private final ReentrantLock putLock = new ReentrantLock();
    
    // 用户 take(阻塞) poll(非阻塞)
    private final ReentrantLock takeLock = new ReentrantLock();
    

    put 操作

    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        // count 用来维护元素计数
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            // 满了等待
            while (count.get() == capacity) {
                // 倒过来读就好: 等待 notFull
                notFull.await();
            }
            // 有空位, 入队且计数加一
            enqueue(node);
            c = count.getAndIncrement();
            // 除了自己 put 以外, 队列还有空位, 由自己叫醒其他 put 线程
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        // 如果队列中有一个元素, 叫醒 take 线程
        if (c == 0)
            // 这里调用的是 notEmpty.signal() 而不是 notEmpty.signalAll() 是为了减少竞争
            signalNotEmpty();
    }
    

    take 操作

    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        // 如果队列中只有一个空位时, 叫醒 put 线程
        // 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c < capacity
        if (c == capacity)
            // 这里调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争
            signalNotFull();
        return x;
    }
    

    由 put 唤醒 put 是为了避免信号不足

    3. 性能比较

    主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较

    • Linked 支持有界,Array 强制有界
    • Linked 实现是链表,Array 实现是数组
    • Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
    • Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
    • Linked 两把锁,Array 一把锁

    10. ConcurrentLinkedQueue

    ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是

    • 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
    • dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
    • 只是这【锁】使用了 cas 来实现

    事实上,ConcurrentLinkedQueue 应用还是非常广泛的

    例如之前讲的 Tomcat 的 Connector 结构时,Acceptor 作为生产者向 Poller 消费者传递事件信息时,正是采用了 ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用

    11. CopyOnWriteArrayList

    CopyOnWriteArraySet 是它的马甲 底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,更 改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。 以新增为例:

    public boolean add(E e) {
        synchronized (lock) {
            // 获取旧的数组
            Object[] es = getArray();
            int len = es.length;
            // 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)
            es = Arrays.copyOf(es, len + 1);
            // 添加新元素
            es[len] = e;
            // 替换旧的数组
            setArray(es);
            return true;
        }
    }
    

    这里的源码版本是 Java 11,在 Java 1.8 中使用的是可重入锁而不是 synchronized

    其它读操作并未加锁

    适合『读多写少』的应用场景

    get 弱一致

    不容易测试,但问题确实存在

    迭代器弱一致性

    CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    Iterator<Integer> iter = list.iterator(); new
    
    Thread(() -> {
        list.remove(0);
        System.out.println(list);
    }).start();
    
    sleep1s();
    while(iter.hasNext()) {
        System.out.println(iter.next());
    }
    

    不要觉得弱一致性就不好
    -数据库的 MVCC 都是弱一致性的表现
    -并发高和一致性是矛盾的,需要权衡

    相关文章

      网友评论

          本文标题:并发--共享模型之工具

          本文链接:https://www.haomeiwen.com/subject/yekmqrtx.html