计算机笔记--【并发编程②】

5.5.共享模型之工具

5.5.1. 线程池

1、自定义线程池(TODO手写
在这里插入图片描述

  • 步骤1:自定义拒绝策略接口

    @FunctionalInterface // 拒绝策略,任务定义为泛型,因为不一定是 Runable,也有可能是 Callable
    interface RejectPolicy<T> {
        void reject(BlockingQueue<T> queue, T task);
    }
    
  • 步骤2:自定义任务队列

    @Slf4j(topic = "c.BlockingQueue")
    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();
            }
        }
    }
    
  • 步骤3:自定义线程池

    @Slf4j(topic = "c.ThreadPool")
    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 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);
                }
            }
        }
    
        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;
        }
    
        // 线程对象封装
        class Worker extends Thread{
            private Runnable task;
    
            // 初始化时的任务对象
            public Worker(Runnable task) {
                this.task = task;
            }
    
            @Override
            public void run() {
                // 执行任务
                // 1) 当 task 不为空,执行任务
                // 2) 当 task 执行完毕,再接着从任务队列获取任务并执行
    //            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);
                }
            }
        }
    }
    
  • 步骤4:测试

    @Slf4j(topic = "c.TestPool")
    public class TestPool {
        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) 让调用者自己执行任务,主线程将run方法当做普通方法执行
                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);
                });
            }
        }
    }
    

2、ThreadPoolExecutor
在这里插入图片描述

  • 1)线程池状态

    ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量
    在这里插入图片描述
    从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING(有符号位)
    这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值,减少 CAS 操作
    在这里插入图片描述

  • 2)构造方法
    在这里插入图片描述

    • corePoolSize 核心线程数目 (最多保留的线程数)
    • maximumPoolSize 最大线程数目
    • keepAliveTime 生存时间 - 针对救急线程
    • unit 时间单位 - 针对救急线程
    • workQueue 阻塞队列
    • threadFactory 线程工厂 - 可以为线程创建时起个好名字
    • handler 拒绝策略
  • 工作方式:
    在这里插入图片描述
    在这里插入图片描述

  • 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。

  • 当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排队,直到有空闲的线程。

  • 如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。

  • 如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现:

    • AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
    • CallerRunsPolicy 让调用者运行任务
    • DiscardPolicy 放弃本次任务
    • DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
    • Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
    • Netty (网络框架)的实现,是创建一个新线程来执行任务
    • ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
    • PinPoint (链路追踪的框架)的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
  • 当高峰过去后,超过 corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTimeunit 来控制。
    在这里插入图片描述
    根据这个构造方法,JDK Executors 类(工具类)中提供了众多工厂方法来创建各种用途的线程池

3)newFixedThreadPool
在这里插入图片描述
特点:

  • 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
  • 阻塞队列是无界的,可以放任意数量的任务
  • 评价:适用于任务量已知,相对耗时的任务
@Slf4j(topic = "c.TestThreadPool")
public class TestThreadPool {
    public static void main(String[] args) throws InterruptedException {

        ExecutorService threadPool = Executors.newFixedThreadPool(2);

        for (int i = 0; i < 3; i++) {
            int j = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("{}", j);
            });
        }
    }
}

在这里插入图片描述
t.setDaemon(false); 线程池中的线程是非守护线程,所以并不会结束。

@Slf4j(topic = "c.TestThreadPool")
public class TestThreadPool {
    public static void main(String[] args) throws InterruptedException {

        // 通过工厂,自己定义线程的名字
        ExecutorService threadPool = Executors.newFixedThreadPool(2, new ThreadFactory() {
            private AtomicInteger t = new AtomicInteger(1);
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "my_pool_thread_" + t.getAndIncrement());
            }
        });

        for (int i = 0; i < 3; i++) {
            int j = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("{}", j);
            });
        }
    }
}

4)newCachedThreadPool
在这里插入图片描述
特点:

  • 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着
    • 全部都是救急线程(60s 后可以回收)
    • 救急线程可以无限创建
  • 队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货,放的线程因为没有人来取从而阻塞住,像是两个线程之间交换任务的队列)
  • 评价:整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况
@Slf4j(topic = "c.TestSynchronousQueue")
public class TestSynchronousQueue {
    public static void main(String[] args) {
        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();
    }
}

在这里插入图片描述

5)newSingleThreadExecutor
在这里插入图片描述

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

@Slf4j(topic = "c.TestExecutors")
public class TestExecutors {
    public static void main(String[] args) throws InterruptedException {
        // test1();
         test2();
    }

    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");
        });
    }

    private static void test1() {
        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");
        });
    }
}

在这里插入图片描述

区别:

  • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作。
  • Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
    • FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
  • Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改
    • 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
      TODO 异步编排

6)提交任务
在这里插入图片描述

@Slf4j(topic = "c.TestSubmit")
public class TestSubmit {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(1);
        method1(pool);
    }

    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());
    }

    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();
            }
        });
    }
}

method1(pool); 执行结果:
在这里插入图片描述
method2(pool); 执行结果:
在这里插入图片描述
method3(pool); 执行结果:
在这里插入图片描述

7)关闭线程池

  • shutdown
    在这里插入图片描述
    在这里插入图片描述
  • shutdownNow
    在这里插入图片描述
    在这里插入图片描述
  • 其他方法
    在这里插入图片描述
@Slf4j(topic = "c.TestShutDown")
public class TestShutDown {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        Future<Integer> result1 = pool.submit(() -> {
            log.debug("task 1 running...");
            Thread.sleep(1000);
            log.debug("task 1 finish...");
            return 1;
        });

        Future<Integer> result2 = pool.submit(() -> {
            log.debug("task 2 running...");
            Thread.sleep(1000);
            log.debug("task 2 finish...");
            return 2;
        });

        Future<Integer> result3 = pool.submit(() -> {
            log.debug("task 3 running...");
            Thread.sleep(1000);
            log.debug("task 3 finish...");
            return 3;
        });

        log.debug("shutdown");
        pool.shutdown();
//        pool.awaitTermination(3, TimeUnit.SECONDS);
//        List<Runnable> runnables = pool.shutdownNow();
        log.debug("other.... {}" , runnables);
    }
}

pool.shutdown(); 结果演示:
在这里插入图片描述
pool.shutdownNow(); 结果演示:
在这里插入图片描述

8)模式之 Worker Thread (异步模式之工作线程)

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

    例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那么成本就太高了(对比另一种多线程设计模式:Thread-Per-Message)

    注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率。

    例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工

  • 饥饿
    固定大小线程池会有饥饿现象(线程数量不足导致的饥饿,解决方法是线程池划分,不同的任务类型使用不同的线程池

    • 两个工人是同一个线程池中的两个线程
    • 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作
      • 1)客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待
      • 2)后厨做菜:没啥说的,做就是了
    • 比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好
    • 但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了,这时没人做饭了,饥饿。解决方法可以增加线程池的大小,不过不是根本解决方案,还是前面提到的,不同的任务类型,采用不同的线程池,例如:
    @Slf4j(topic = "c.TestDeadLock")
    public class TestStarvation {
    
        static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");
        static Random RANDOM = new Random();
        static String cooking() {
            return MENU.get(RANDOM.nextInt(MENU.size()));
        }
        public static void main(String[] args) {
        	// 分了两类线程池
            ExecutorService waiterPool = Executors.newFixedThreadPool(1);
            ExecutorService cookPool = Executors.newFixedThreadPool(1);
    
            waiterPool.execute(() -> {
                log.debug("处理点餐...");
                Future<String> f = cookPool.submit(() -> {
                    log.debug("做菜");
                    return cooking();
                });
                try {
                    // 阻塞等待
                    log.debug("上菜: {}", f.get());
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            });
            waiterPool.execute(() -> {
                log.debug("处理点餐...");
                Future<String> f = cookPool.submit(() -> {
                    log.debug("做菜");
                    return cooking();
                });
                try {
                    log.debug("上菜: {}", f.get());
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            });
    
        }
    }
    

    在这里插入图片描述
    ExecutorService pool = Executors.newFixedThreadPool(2); 饥饿现象:

    @Slf4j(topic = "c.TestDeadLock")
    public class TestStarvation {
    
        static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");
        static Random RANDOM = new Random();
        static String cooking() {
            return MENU.get(RANDOM.nextInt(MENU.size()));
        }
        public static void main(String[] args) {
            ExecutorService pool = Executors.newFixedThreadPool(2);
    
            pool.execute(() -> {
                log.debug("处理点餐...");
                Future<String> f = pool.submit(() -> {
                    log.debug("做菜");
                    return cooking();
                });
                try {
                    // 阻塞等待
                    log.debug("上菜: {}", f.get());
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            });
            pool.execute(() -> {
                log.debug("处理点餐...");
                Future<String> f = pool.submit(() -> {
                    log.debug("做菜");
                    return cooking();
                });
                try {
                    log.debug("上菜: {}", f.get());
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            });
    
        }
    }
    

    在这里插入图片描述

  • 创建多少线程合适
    过小会导致程序不能充分地利用系统资源、容易导致饥饿。过大会导致更多的线程上下文切换,占用更多内存。

    • 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
      例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式
      4 * 100% * 100% / 10% = 40

  • 自定义线程池(见上)

9)任务调度线程池
希望某个任务被反复的被执行。在『任务调度线程池』功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个
任务的延迟或异常都将会影响到之后的任务。
在这里插入图片描述
输出:
在这里插入图片描述
使用 ScheduledExecutorService 改写(延时执行任务):
在这里插入图片描述
输出:
在这里插入图片描述
scheduleAtFixedRate 例子(固定速率执行任务):
在这里插入图片描述
输出:
在这里插入图片描述
scheduleAtFixedRate 例子(任务执行时间超过了间隔时间):
在这里插入图片描述
输出分析:一开始,延时 1s,接下来,由于任务执行时间 > 间隔时间,间隔被『撑』到了 2s(任务的执行时间较长,影响到了执行的间隔)
在这里插入图片描述
scheduleWithFixedDelay 例子(单独延时的执行):
在这里插入图片描述
输出分析:一开始,延时 1s,scheduleWithFixedDelay 的间隔是 上一个任务结束 <-> 延时 <-> 下一个任务开始 所以间隔都是 3s
在这里插入图片描述

评价 整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。用来执行延迟或反复执行的任务

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

  • 方法1:主动捉异常
    在这里插入图片描述
    输出:
    在这里插入图片描述
  • 方法2:使用 Future(Submit 方法的返回值,callable 配合 Future,有结果返回结果,有异常返回异常)
    在这里插入图片描述
    输出:
    在这里插入图片描述

11)应用之定时任务

如何让每周四 18:00:00 定时执行任务?

public class TestSchedule {

    // 如何让每周四 18:00:00 定时执行任务?
    public static void main(String[] args) {
        //  获取当前时间,线程安全,Java 8 新增的日期
        LocalDateTime now = LocalDateTime.now();
        System.out.println(now);
        // 获取本周四时间
        LocalDateTime time = now.withHour(18).withMinute(0).withSecond(0).withNano(0).with(DayOfWeek.THURSDAY);
        // 如果 当前时间 > 本周周四,必须找到下周周四
        if(now.compareTo(time) > 0) {
            time = time.plusWeeks(1);
        }
        System.out.println(time);
        // initailDelay 代表当前时间和周四的时间差
        // period 一周的间隔时间
        long initailDelay = Duration.between(now, time).toMillis();
        long period = 1000 * 60 * 60 * 24 * 7;
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
        pool.scheduleAtFixedRate(() -> {
            System.out.println("running...");
        }, initailDelay, period, TimeUnit.MILLISECONDS);
    }
}

12)Tomcat 线程池
Tomcat 在哪里用到了线程池呢?
在这里插入图片描述

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

Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同

  • 如果总线程数达到 maximumPoolSize
    • 1)这时不会立刻抛 RejectedExecutionException 异常
    • 2)而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常

源码 tomcat-7.0.42
在这里插入图片描述
TaskQueue.java
在这里插入图片描述
Connector 配置
在这里插入图片描述
Executor 线程配置
在这里插入图片描述
在这里插入图片描述

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

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

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

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

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

public class TestForkJoin {

    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)));
    }
}

@Slf4j(topic = "c.AddTask")
class AddTask1 extends RecursiveTask<Integer> {

    int n;

    public AddTask1(int n) {
        this.n = n;
    }

    @Override
    public String toString() {
        return "{" + n + '}';
    }

    // invoke 调用的方法
    @Override
    protected Integer compute() {
        // 终止条件
        if (n == 1) {
            log.debug("join() {}", n);
            return n;
        }
        AddTask1 t1 = new AddTask1(n - 1);

        t1.fork();
        log.debug("fork() {} + {}", n, t1);
        int result = n + t1.join();
        log.debug("join() {} + {} = {}", n, t1, result);
        return result;
    }
}

@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;
    }
}

在这里插入图片描述
用图来表示
在这里插入图片描述
改进

public class TestForkJoin {

    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)));
    }
}


@Slf4j(topic = "c.AddTask")
class AddTask3 extends RecursiveTask<Integer> {

    int begin;
    int end;

    public AddTask3(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;

        AddTask3 t1 = new AddTask3(begin, mid);
        t1.fork();
        AddTask3 t2 = new AddTask3(mid + 1, end);
        t2.fork();
        log.debug("fork() {} + {} = ?", t1, t2);

        int result = t1.join() + t2.join();
        log.debug("join() {} + {} = {}", t1, t2, result);
        return result;
    }
}

在这里插入图片描述
用图来表示
在这里插入图片描述

5.5.2. J.U.C
5.5.2.1. AQS 原理

1)概述
全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架。先明白AQS是一个接口,规范,这个接口定义了一系列规则,而是否要设定公平锁与否由实现它的类来决定。
特点:

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

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

  • tryAcquire
  • tryRelease
  • tryAcquireShared
  • tryReleaseShared
  • isHeldExclusively

获取锁的姿势。
在这里插入图片描述
释放锁的姿势。
在这里插入图片描述

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);
            // private volatile int state;  防止指令重排序,写前读后,将 setExclusiveOwnerThread(null);
            // 放在前面,有写屏障,共享变量写入主存,确保前面的都写到主存
            setState(0);
            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 // 加锁(不成功会进入等待队列)acquire 尝试多次,数字都没有用上
    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 final void acquire(int arg) {
	// 尝试加锁不成功,将线程放入队列
   if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

测试一下:

@Slf4j(topic = "c.TestAqs")
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();
    }
}

在这里插入图片描述
不可重入测试
如果改为下面代码,会发现自己也会被挡住(只会打印一次 locking)
在这里插入图片描述
3)心得TODO

5.5.2.2. ReentrantLock 原理

在这里插入图片描述

1)非公平锁实现原理

  • 加锁解锁流程
    先从构造器开始看,默认为非公平锁实现
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    NonfairSync 继承自 AQS
    没有竞争时
    在这里插入图片描述
    第一个竞争出现时:
    在这里插入图片描述
    在这里插入图片描述
    Thread-1 执行了
    1) CAS 尝试将 state 由 0 改为 1,结果失败
    2)进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败
    3)接下来进入 addWaiter 逻辑,构造 Node 队列

    • 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
    • Node 的创建是懒惰的
    • 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程。
      在这里插入图片描述

    当前线程进入 acquireQueued 逻辑
    在这里插入图片描述
    1)acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
    2)如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
    3)进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false

    在这里插入图片描述
    在这里插入图片描述
    4)shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时state 仍为 1,失败
    5) 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true
    6)进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)
    在这里插入图片描述
    在这里插入图片描述

    再次有多个线程经历上述过程竞争失败,变成这个样子:
    在这里插入图片描述
    Thread-0 释放锁,进入 tryRelease 流程,如果成功
    1)设置 exclusiveOwnerThread 为 null
    2)state = 0
    在这里插入图片描述
    可重入的情况有可能 tryRelease 为 false
    在这里插入图片描述
    在这里插入图片描述
    当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程。
    找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1。(队列内是公平的,不公平体现在入队列的时候就有机会取获得锁
    在这里插入图片描述
    回到 Thread-1 的 acquireQueued 流程。
    在这里插入图片描述
    在这里插入图片描述
    如果加锁成功(没有竞争),会设置
    1)exclusiveOwnerThread 为 Thread-1,state = 1
    2)head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
    3)原本的 head 因为从链表断开,而可被垃圾回收

    如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了
    在这里插入图片描述
    如果不巧又被 Thread-4 占了先
    1)Thread-4 被设置为 exclusiveOwnerThread,state = 1
    2)Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞

  • 加锁源码(TOSEE 文档中的源码部分P46

  • 解锁源码(TOSEE 文档中的源码部分P49

2、可重入原理
在这里插入图片描述
在这里插入图片描述

3、可打断原理
1)不可打断模式
在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了。
在这里插入图片描述
在这里插入图片描述
2)可打断模式
在这里插入图片描述
4、公平锁实现原理
在这里插入图片描述
在这里插入图片描述
5、条件变量实现原理
每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject。

1)await 流程
开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程。
创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功。
在这里插入图片描述
park 阻塞 Thread-0。
在这里插入图片描述
2)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 流程,略。
源码(TOSEE 文档中的源码部分P57

5.5.2.3. 读写锁

1、 ReentrantReadWriteLock
当读操作远远高于写操作时,这时候使用 读写锁读-读 可以并发,提高性能。 类似于数据库中的 select ...from ... lock in share mode
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法。
演示:

@Slf4j(topic = "c.DataContainer")
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();
        }
    }
}

测试 读锁-读锁 可以并发(加读锁的原因是因为为了让其他线程来并发读。防止其他线程来写

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

        new Thread(() -> {
            dataContainer.read();
        }, "t2").start();
    }
}

输出结果,从这里可以看到 Thread-0 锁定期间,Thread-1 的读操作不受影响。
在这里插入图片描述
测试 读锁-写锁 相互阻塞。

@Slf4j(topic = "c.TestReadWriteLock")
public class TestReadWriteLock {
    public static void main(String[] args) throws InterruptedException {
        DataContainer dataContainer = new DataContainer();
        new Thread(() -> {
            dataContainer.read();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();


        Thread.sleep(10);
        new Thread(() -> {
            dataContainer.write();
        }, "t2").start();
    }
}

输出结果:
在这里插入图片描述
写锁-写锁 也是相互阻塞的,这里就不测试了。

注意事项

  • 读锁不支持条件变量
  • 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
    在这里插入图片描述
  • 重入时降级支持:即持有写锁的情况下去获取读锁
    在这里插入图片描述

2、应用之缓存
1)缓存更新策略

更新时,是先清缓存还是先更新数据库
先清缓存(导致数据库与缓存不一致)
在这里插入图片描述
先更新数据库(做不到强一致,但能最终一致)
在这里插入图片描述
补充一种情况,假设查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询。这种情况的出现几率非常小,见 facebook 论文
在这里插入图片描述

2)读写锁实现一致性缓存

public class TestGenericDao {
    public static void main(String[] args) {
        // 缓存的优化
        GenericDao dao = new GenericDaoCached();
        System.out.println("============> 查询");
        String sql = "select * from emp where empno = ?";
        int empno = 7369;
        Emp emp = dao.queryOne(Emp.class, sql, empno);
        System.out.println(emp);
        emp = dao.queryOne(Emp.class, sql, empno);
        System.out.println(emp);
        emp = dao.queryOne(Emp.class, sql, empno);
        System.out.println(emp);

        System.out.println("============> 更新");
        dao.update("update emp set sal = ? where empno = ?", 800, empno);
        emp = dao.queryOne(Emp.class, sql, empno);
        System.out.println(emp);
    }
}

class GenericDaoCached extends GenericDao {
    private GenericDao dao = new GenericDao();
    // 缓存的 Map (SQL语句和查询结果)
    //  HashMap 作为缓存非线程安全, 需要保护
    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 {
            // 多个线程
            // get 方法上面部分是可能多个线程依次获得锁进来的, 可能已经向缓存填充了数据
            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;
        }
    }

}

注意

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

3、 读写锁原理

1)图解流程
读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个

t1 w.lock,t2 r.lock

  • 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 所在节点(以上就是为什么读读可以并发的原因,因为只要是shared节点被唤醒,就会将一连串的读锁节点都唤醒,直到遇到一个独占节点,才停止唤醒

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) 成功,修改头结点,流程结束
在这里插入图片描述

2)源码分析(TOSEE文档P68

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

加解读锁
在这里插入图片描述

加解写锁
在这里插入图片描述

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

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

@Slf4j(topic = "c.DataContainerStamped")
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);
        }
    }
}

测试 读-读 可以优化

@Slf4j(topic = "c.TestStampedLock")
public class TestStampedLock {
    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();
    }
}

输出结果,可以看到实际没有加读锁
在这里插入图片描述
测试 读-写 时优化读补加读锁

@Slf4j(topic = "c.TestStampedLock")
public class TestStampedLock {
    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(0);
        }, "t2").start();
    }
}

输出结果
在这里插入图片描述

注意

  • StampedLock 不支持条件变量
  • StampedLock 不支持可重入
5.5.2.4.Semaphore

基本使用
[ˈsɛməˌfɔr] 信号量,用来限制能同时访问共享资源的线程上限(共享资源有多个,也允许有多个共享线程来共享访问,和读写锁的独占模式不一样)。

@Slf4j(topic = "c.TestSemaphore")
public class TestSemaphore {
    public static void main(String[] args) {
        // 1. 创建 semaphore 对象
        Semaphore semaphore = new Semaphore(3); // 3 表示许可的数量,类似于例子中的停车位

        // 2. 10个线程同时运行
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                // 先获得许可
                try {
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    log.debug("running...");
                    sleep(1);
                    log.debug("end...");
                } finally {
                    // 释放许可
                    semaphore.release();
                }
            }).start();
        }
    }
}

输出:
在这里插入图片描述

Semaphore 应用
限制对共享资源的使用,semaphore 实现。

  • 使用 Semaphore 限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatch 的实现)
  • 用 Semaphore 实现简单连接池,对比『享元模式』下的实现(用wait notify),性能和可读性显然更好,注意下面的实现中线程数和数据库连接数是相等的
public class TestPoolSemaphore {
    public static void main(String[] args) {
        Pool pool = new Pool(2);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                Connection conn = pool.borrow();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                pool.free(conn);
            }).start();
        }
    }
}

@Slf4j(topic = "c.Pool")
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(); // 没有许可的线程,在此等待,阻塞住,实现 wait 功能
        } 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;
            }
        }
    }
}

class MockConnection implements Connection {

    private String name;

    public MockConnection(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "MockConnection{" +
                "name='" + name + '\'' +
                '}';
    }

    // TODO ...
}

Semaphore 原理
1)加锁解锁流程
Semaphore 有点像一个停车场,permits 就好像停车位数量,当线程获得了 permits 就像是获得了停车位,然后停车场显示空余车位减一
在这里插入图片描述
在这里插入图片描述
刚开始,permits(state)为 3,这时 5 个线程来获取资源
在这里插入图片描述
假设其中 Thread-1,Thread-2,Thread-4 cas 竞争成功,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列park 阻塞
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Thread-0获取资源失败,进入 doAcquireSharedInterruptibly(arg);
在这里插入图片描述
在这里插入图片描述
这时 Thread-4 释放了 permits,状态如下
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,断开原来的 head 节点,unpark 接下来的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态
在这里插入图片描述
2)源码分析(TOSEE文档P76

5.5.2.5.CountdownLatch

倒计时锁,用来进行线程同步协作,等待所有线程完成倒计时。其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一。(Join与此功能类似。但是更加底层,在线程池中使用不了
在这里插入图片描述

@Slf4j(topic = "c.TestCountDownLatch")
public class TestCountDownLatch {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        test4();
    }
    
    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...");
    }
}

输出
在这里插入图片描述
可以配合线程池使用,改进如下

@Slf4j(topic = "c.TestCountDownLatch")
public class TestCountDownLatch {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        test5();
    }

    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...");
                // await
                latch.await();
                log.debug("wait end...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}

输出
在这里插入图片描述

应用之同步等待多线程准备完毕

@Slf4j(topic = "c.TestCountDownLatch")
public class TestCountDownLatch {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        test2();
    }

    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 + "%") + ")";
                    // "\r" 能让后面的打印回退到当前行,覆盖掉当前行的内容
                    System.out.print("\r" + Arrays.toString(all));
                }
                latch.countDown();
            });
        }
        // 等所有的 latch.countDown(); 减为0
        latch.await();
        System.out.println("\n游戏开始...");
        service.shutdown();
    }
}

中间输出
在这里插入图片描述
最后输出
在这里插入图片描述

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

@RestController
public class TestCountDownlatchController {

    @GetMapping("/order/{id}")
    public Map<String, Object> order(@PathVariable int id) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("id", id);
        map.put("total", "2300.00");
        sleep(2000);
        return map;
    }

    @GetMapping("/product/{id}")
    public Map<String, Object> product(@PathVariable int id) {
        HashMap<String, Object> map = new HashMap<>();
        if (id == 1) {
            map.put("name", "小爱音箱");
            map.put("price", 300);
        } else if (id == 2) {
            map.put("name", "小米手机");
            map.put("price", 2000);
        }
        map.put("id", id);
        sleep(1000);
        return map;
    }

    @GetMapping("/logistics/{id}")
    public Map<String, Object> logistics(@PathVariable int id) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("id", id);
        map.put("name", "中通快递");
        sleep(2500);
        return map;
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

rest 远程调用

@Slf4j(topic = "c.TestCountDownLatch")
public class TestCountDownLatch {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        test3();
    }

    private static void test3() throws InterruptedException, ExecutionException {
        RestTemplate restTemplate = new RestTemplate();
        log.debug("begin");
        ExecutorService service = Executors.newCachedThreadPool();
        CountDownLatch latch = new CountDownLatch(4);
        // 将 RPC 做成任务,交给线程池运行
        Future<Map<String, Object>> f1 = service.submit(() -> {
            Map<String, Object> response = restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1);
            log.debug("end order:{}", response);
            latch.countDown();
            return response;
        });
        Future<Map<String, Object>> f2 = service.submit(() -> {
            Map<String, Object> response1 = restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1);
            log.debug("end product:{}", response1);
            latch.countDown();
            return response1;
        });
        Future<Map<String, Object>> f3 = service.submit(() -> {
            Map<String, Object> response1 = restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2);
            log.debug("end product:{}", response1);
            latch.countDown();
            return response1;
        });
        Future<Map<String, Object>> f4 = service.submit(() -> {
            Map<String, Object> response3 = restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1);
            log.debug("end product:{}", response3);
            latch.countDown();
            return response3;
        });
		// 利用future做线程执行完毕的返回结果
        /*System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());*/
        latch.await();
        log.debug("执行完毕");
        service.shutdown();
    }
}

执行结果(没有返回结果,用CountDownLatch做线程同步方便,有返回值结果用Future方便
在这里插入图片描述

5.5.2.6.CyclicBarrier
@Slf4j(topic = "c.TestCyclicBarrier")
public class TestCyclicBarrier {

    public static void main(String[] args) {
        test1();
    }

    private static void test1() {
        ExecutorService service = Executors.newFixedThreadPool(5);
        // 反复运行 3 遍
        for (int i = 0; i < 3; i++) {
            CountDownLatch latch = new CountDownLatch(2);
            service.submit(() -> {
                log.debug("task1 start...");
                sleep(1);
                latch.countDown();
            });
            service.submit(() -> {
                log.debug("task2 start...");
                sleep(2);
                latch.countDown();
            });
            try {
                // 主线程恢复运行
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("task1 task2 finish...");
        }
        service.shutdown();
    }
}

在这里插入图片描述
[ˈsaɪklɪk ˈbæriɚ] 循环栅栏,用来进行线程协作(可以重用),等待线程满足某个计数。构造时设置『计数个数』,每个线程执行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行:

@Slf4j(topic = "c.TestCyclicBarrier")
public class TestCyclicBarrier {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(2);// 线程数与任务数保持一致
        // 参数2是其他两个任务执行完了,最后再执行的
        CyclicBarrier barrier = new CyclicBarrier(2, () -> {
            log.debug("task1, task2 finish...");
        });
        // 计数重复被使用
        for (int i = 0; i < 3; i++) { 
        	// 线程池中若有三个线程,导致两个task1先执行完,达不到效果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 可以被比喻为『人满发车』

5.5.2.7.线程安全集合类概述

在这里插入图片描述

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

  • 遗留的线程安全集合如 HashtableVector
  • 使用 Collections 装饰的线程安全集合(装饰器模式,在所有的装饰方法上上加入了 synchronized 性能并没有太大的提升),如:(TODO查看源码
    • 1)Collections.synchronizedCollection
    • 2)Collections.synchronizedList
    • 3)Collections.synchronizedMap
    • 4)Collections.synchronizedSet
    • 5)Collections.synchronizedNavigableMap
    • 6)Collections.synchronizedNavigableSet
    • 7)Collections.synchronizedSortedMap
    • 8)Collections.synchronizedSortedSet
  • java.util.concurrent.*

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

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

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

5.5.2.8.ConcurrentHashMap

ConcurrentHashMap 单个方法是线程安全的,但是方法组合在一起,保证不了线程安全性,指令还是会交错执行。

练习:单词计数TOSEE文档P177

ConcurrentHashMap 原理摆烂了一波

1)JDK 7 HashMap 并发死链

测试代码
注意

  • 要在 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);
        // 扩容之后,35 1 还是在一个桶内
        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);
    }
}

死链复现TOSEE看视屏P279
调试工具使用 idea
在 HashMap 源码 590 行加断点(问题在于一个线程1已经完成了rehash扩容【1–>35–>16–>null】变为【35–>1–>null】,另外一个线程2还准备去rehash,此时2指向的引用已经发生变化【35–>1–>null】和【1–>null】而并非【1–>35–>16–>null】
在这里插入图片描述
断点的条件如下,目的是让 HashMap 在扩容为 32 时,并且线程为 Thread-0 或 Thread-1 时停下来
在这里插入图片描述
断点暂停方式选择 Thread,否则在调试 Thread-0 时,Thread-1 无法恢复运行
运行代码,程序在预料的断点位置停了下来,输出

小结

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

2)JDK 8 ConcurrentHashMap

重要属性和内部类
在这里插入图片描述
重要方法
在这里插入图片描述
构造器分析
可以看到实现了懒惰初始化,在构造方法中仅仅计算了 table 的大小,以后在第一次使用时才会真正创建。
在这里插入图片描述
get 流程
在这里插入图片描述
put 流程
以下数组简称(table),链表简称(bin)

public V put(K key, V value) {
    return putVal(key, value, false);
}
// onlyIfAbsent 表示覆盖操作
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;
        }
        // 检测到 fnode 节点,帮忙扩容,锁住当前列表
        else if ((fh = f.hash) == MOVED)
            // 帮忙之后, 进入下一轮循环
            tab = helpTransfer(tab, f);
        // 能进入则说明你的哈希表既不是初始创建,也不是扩容过程中,普通的放入键值对,并产生桶下标冲突
        else {
            V oldVal = null;
            // 锁住链表头节点
            synchronized (f) {
                // 再次确认链表头节点没有被移动
                if (tabAt(tab, i) == f) {
                    // 链表,普通节点的哈希码都是大于0的,只有红黑树或者fnode才是负数(扩容)
                    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;
                            }
                        }
                    }
                    // 是否是红黑树,TreeBin 是红黑树的头结点
                    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 计数,类似于 LongAdder
    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) {
    // LongAdder 的直接实现 CounterCell[],累加单元数组
    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 累加计数
    • 1)counterCells 初始有两个 cell
    • 2)如果计数竞争比较激烈,会创建新的 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[] 当中。最后统计数量时累加即可

3)JDK 7 ConcurrentHashMap
它维护了一个 segment 数组,每个 segment 对应一把锁

  • 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的
  • 缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化

构造器分析

public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // ssize 必须是 2^n, 即 2, 4, 8, 16 ... 表示了 segments 数组的大小
    int sshift = 0;
    int ssize = 1;
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    // segmentShift 默认是 32 - 4 = 28
    // 移位属性
    this.segmentShift = 32 - sshift;
    // segmentMask 默认是 15 即 0000 0000 0000 1111
    // 掩码属性
    this.segmentMask = ssize - 1;
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;
    // 创建 segments and segments[0]
    Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                    (HashEntry<K,V>[])new HashEntry[cap]);
    // 直接创建,并不是懒惰初始化
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

构造完成,如下图所示
在这里插入图片描述
可以看到 ConcurrentHashMap 没有实现懒惰初始化,空间占用不友好
其中 this.segmentShift 和 this.segmentMask 的作用是决定将 key 的 hash 结果匹配到哪个 segment

例如,根据某一 hash 值求 segment 位置,先将高位向低位移动 this.segmentShift 位
在这里插入图片描述
结果再与 this.segmentMask 做位于运算,最终得到 1010 即下标为 10 的 segment
在这里插入图片描述
put 流程

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    // 计算出 segment 下标
    int j = (hash >>> segmentShift) & segmentMask;

    // 获得 segment 对象, 判断是否为 null, 是则创建该 segment
    if ((s = (Segment<K,V>)UNSAFE.getObject
            (segments, (j << SSHIFT) + SBASE)) == null) {
        // 这时不能确定是否真的为 null, 因为其它线程也发现该 segment 为 null,
        // 因此在 ensureSegment 里用 cas 方式保证该 segment 安全性
        s = ensureSegment(j);
    }
    // 进入 segment 的put 流程
    return s.put(key, hash, value, false);
}

segment 继承了可重入锁(ReentrantLock),它的 put 方法为

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // 尝试加锁
    HashEntry<K,V> node = tryLock() ? null :
            // 如果不成功, 进入 scanAndLockForPut 流程
            // 如果是多核 cpu 最多 tryLock 64 次, 进入 lock 流程
            // 在尝试期间, 还可以顺便看该节点在链表中有没有, 如果没有顺便创建出来
            scanAndLockForPut(key, hash, value);

    // 执行到这里 segment 已经被成功加锁, 可以安全执行
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                // 更新
                K k;
                if ((k = e.key) == key ||
                        (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                // 新增
                // 1) 之前等待锁时, node 已经被创建, next 指向链表头
                if (node != null)
                    node.setNext(first);
                else
                    // 2) 创建新 node
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                // 3) 扩容
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    // 将 node 作为链表头
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}

rehash 流程TOSEE文档P95


5.5.2.9.BlockingQueue

TOSEE文档P179

5.5.2.10. ConcurrentLinkedQueue

TOSEE文档P179

5.5.2.10. CopyOnWriteArrayList

TOSEE文档P180

总结

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值