JUC学习笔记(六)线程池

8 篇文章 0 订阅

线程池

第一节 阻塞队列

阻塞队列是线程池的核心组件,了解阻塞队列为学习线程池打好基础。

1、概念

①阻塞

images

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

②BlockingQueue

images

BlockingQueue即阻塞队列,是java.util.concurrent下的一个接口,因此不难理解,BlockingQueue是为了解决多线程中数据高效安全传输而提出的。从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种:

  • 当队列了的时候进行入队列操作
  • 当队列了的时候进行出队列操作

阻塞队列主要用在生产者/消费者的场景,下面这幅图展示了一个线程生产、一个线程消费的场景:

images

为什么需要BlockingQueue?好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

2、BlockingQueue接口

images

BlockingQueue接口主要有以下7个实现类:

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

BlockingQueue接口有以下方法:

抛出异常特定值阻塞超时
插入add(e)offer(e)put(e)offer(e,tiem,unit)
移除remove()poll()take()poll(time,unit)
检查element()peek()不可用不可用

详细说明如下:

add()

  • 正常执行返回true,
  • 当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full

element()

  • 正常情况:返回阻塞队列中的第一个元素
  • 当阻塞队列空时:再调用element()检查元素会抛出NoSuchElementException

remove()

  • 正常执行:返回阻塞队列中的第一个元素并删除这个元素
  • 当阻塞队列空时:再往队列里remove()移除元素会抛NoSuchElementException

offer(e)

  • 成功:true
  • 失败:false

poll()

  • 队列中有元素:返回删除的元素
  • 队列中无元素:返回null

peek()

  • 队列中有元素:返回队列中的第一个元素
  • 队列中无元素:返回null

put(e)

  • 队列未满:添加成功
  • 队列已满:线程阻塞等待,直到能够添加为止

take()

  • 队列非空:获取队列中的第一个元素
  • 队列为空:线程阻塞等待,直到队列非空时获取第一个元素

offer(e,tiem,unit)

如果试图执行的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。 返回true或false以告知该操作是否成功

poll(time,unit)

如果试图执行的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。

3、测试代码

// 创建一个BlockingQueue对象
BlockingQueue<String> breadShop = new ArrayBlockingQueue<>(3);

// 创建一个线程用于存放出炉的面包
new Thread(()->{

    while (true) {

        try {
            TimeUnit.SECONDS.sleep(3);
            String bread = UUID.randomUUID().toString().replace("-", "").substring(0, 5);
            System.out.println("面包出炉:" + bread + " 货架情况:" + breadShop);
            breadShop.put(bread);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}).start();

// 创建一个线程用于卖面包
new Thread(()->{

    while (true) {

        try {
            TimeUnit.SECONDS.sleep((int)(Math.random()*10));
            String bread = breadShop.take();
            System.err.println("面包卖出:" + bread + " 货架情况:" + breadShop);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}).start();

第二节 线程池的概念与架构

1、基本概念

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

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

2、架构说明

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

images

Executor 接口是顶层接口,只有一个 execute() 方法,过于简单。通常不使用它,而是使用 ExecutorService 接口:

images

第三节 创建线程池

1、创建线程池

在 JDK 原生 API 中可以使用 Executors 工具类创建线程池对象

images

①newCachedThreadPool()

执行很多短期异步任务,线程池根据需要创建多线程。并在先前创建的线程可用时将重用它们。可扩容,遇强则强。

②newFixedThreadPool(int nThreads)

执行长期任务性能好,创建一个线程池,一池有N个固定的线程。

③newSingleThreadExecutor()

一个任务一个任务的执行,一池一线程。

2、代码演示

// 创建线程池对象
// 总结:通过查看这三个线程池创建时的参数,我们可以看到它们都有自己的缺陷
// 实际开发时我们应该使用 ThreadPoolExecutor 类的对象自定义线程池
// ①方式一:cachedThreadPool 线程池中总共能够容纳 Integer.MAX_VALUE 个线程
// ExecutorService threadPool = Executors.newCachedThreadPool();

// ②方式二:newFixedThreadPool 由程序员指定能够容纳的最大线程数
// 使用的阻塞队列是:LinkedBlockingQueue,阻塞队列最大容量是:Integer.MAX_VALUE
// 意味着:整个线程池任务等待的数量有 Integer.MAX_VALUE 这么多
// ExecutorService threadPool = Executors.newFixedThreadPool(5);

// ③方式三:newSingleThreadExecutor 只能容纳一个线程执行
// 使用的阻塞队列是:LinkedBlockingQueue,阻塞队列最大容量是:Integer.MAX_VALUE
// 意味着:整个线程池任务等待的数量有 Integer.MAX_VALUE 这么多
ExecutorService threadPool = Executors.newSingleThreadExecutor();

System.out.println();

// 给线程池分配任务
while (true) {
    TimeUnit.SECONDS.sleep(1);

    threadPool.execute(()->{
        while (true) {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("任务进行中:" + Thread.currentThread().getName());
        }
    });
}

3、底层原理

上述案例中的三个方法的本质都是ThreadPoolExecutor的实例化对象,只是具体参数值不同。

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

4、提示

使用 Executors 工具类创建的线程池参数设置非常不合理,实际开发时通常需要自己创建 ThreadPoolExecutor 的对象,自己指定参数。

第四节 线程池的7个重要参数

1、到底是五还是七?

从 Executors 类中创建线程池的代码可以看到:

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

这里调用的 ThreadPoolExecutor 类的构造器是五个参数,对应的构造器:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

这里它调用了另外一个参数为七个的构造器:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

结论:所以就本质来说,创建线程池是需要提供『七个』参数。两个构造器的差异在于一个 RejectedExecutionHandler 类型的参数(指定拒绝策略)和一个 ThreadFactory 类型的参数(创建线程对象的工厂)。

2、七个参数各自的含义

  • corePoolSize:线程池中的常驻核心线程数
  • maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
  • keepAliveTime:多余的空闲线程的存活时间。当前池中线程数量超过 corePoolSize 时,当空闲时间达到 keepAliveTime 的线程会被销毁,直到剩余线程数量等于 corePoolSize
  • unit:keepAliveTime 的时间单位
  • workQueue:任务队列,被提交但尚未被执行的任务
  • threadFactory:表示生成线程池中工作线程的工厂, 用于创建线程,一般默认的即可
  • handler:拒绝策略处理器。当任务队列已满,工作线程也达到了 maximumPoolSize,新增的工作任务将按照某个既定的拒绝策略被拒绝执行。

images

第五节 线程池的工作机制

images

重要的事情说三遍——以下重要!以下重要!以下重要

  • 反直觉刚创建线程池时,线程池中的线程数为零

    images

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

    • 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

      images

      ……

      images

    • 反直觉】如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;

      images

      ……

      images

    • 反直觉】如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

      images

    • 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

      images

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

    images

  • 当一个线程空闲的时间超过keepAliveTime时,线程池会判断:

    • 当前运行线程数大于corePoolSize:空闲时间超时线程会被停掉

      images

    • 当前运行线程数小于等于corePoolSize:无动作(所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。)

      images

第六节 线程池的拒绝策略

一般我们创建线程池时,为防止资源被耗尽,任务队列都会选择创建有界任务队列,但这种模式下如果出现任务队列已满且线程池创建的线程数达到你设置的最大线程数时,这时就需要你指定ThreadPoolExecutor的RejectedExecutionHandler参数即合理的拒绝策略,来处理线程池“超载”的情况。

ThreadPoolExecutor自带的拒绝策略如下:

  • AbortPolicy(默认):直接抛出RejectedExecutionException异常,阻止接收新的任务。
  • CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
  • DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。

以上内置的策略均实现了RejectedExecutionHandler接口,也可以自己扩展RejectedExecutionHandler接口,定义自己的拒绝策略。

第七节 自定义线程池

在《阿里巴巴Java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显式的创建线程,这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。同时线程池不允许使用 Executors 去创建,而要通过 ThreadPoolExecutor 方式,这一方面是由于JDK 中 Executor 框架虽然提供了如 newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool() 等创建线程池的方法,但都有其局限性,不够灵活;使用 ThreadPoolExecutor 有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。

本质上,自定义线程池同样是通过指定7个必要参数,创建ThreadPoolExecutor对象:

public static void main(String[] args) {

        // 1、准备好创建线程池对象需要使用的七个参数
        // ①核心线程数
        int corePoolSize = 3;
        // ②最大线程数
        int maximumPoolSize = 5;
        // ③空闲线程的超时时间
        int keepAliveTime = 5;
        // ④空闲线程的超时时间的单位
        TimeUnit timeUnit = TimeUnit.SECONDS;
        // ⑤创建线程对象的工程
        ThreadFactory factory = Executors.defaultThreadFactory();
        // ⑥拒绝策略
        RejectedExecutionHandler handler =

                // 在超出任务处理能力后会抛出异常:java.util.concurrent.RejectedExecutionException
                // new ThreadPoolExecutor.AbortPolicy();

                // 将任务返还给调用者
                // new ThreadPoolExecutor.CallerRunsPolicy();

                // 抛弃等待时间最长的任务
                // new ThreadPoolExecutor.DiscardOldestPolicy();

                // 抛弃任务,啥事儿都不干
                new ThreadPoolExecutor.DiscardPolicy();

                /*new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        System.out.println("自定义拒绝策略");
                    }
                };*/

        // ⑦阻塞队列
        int waitingTaskCount = 5;

        BlockingQueue workQueue = new ArrayBlockingQueue(waitingTaskCount);

        // 2、创建对象
        ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                timeUnit,
                workQueue,
                factory,
                handler);

        // 3、分配任务
        try {
            for (int i = 0; i < 10; i++) {
                // 4、每隔一秒分配一个任务
                // try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {}
                 String taskName = UUID.randomUUID().toString().substring(0, 5).toUpperCase();
                 threadPoolExecutor.submit(() -> {
                    try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {}
                    System.out.println(Thread.currentThread().getName() + " " + taskName);
                 });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPoolExecutor.shutdown();
        }

    }

集合线程安全

第一节 List集合

1、使用ArrayList

// 创建被测试的List集合对象
// 具体集合类型ArrayList:抛出java.util.ConcurrentModificationException异常
List<String> list = new ArrayList<>();
// 在多个线程中对这个List集合执行读写操作
for (int i = 0; i < 50; i++) {
    new Thread(()->{
        for (int j = 0; j < 30; j++) {
            // 向集合对象写入数据
            list.add(UUID.randomUUID().toString().replace("-","").substring(0, 5));
            // 打印集合对象,等于是读取数据
            System.out.println(list);
        }
    },"thread"+i).start();
}

2、各种方案比较

// 创建被测试的List集合对象
// 具体集合类型ArrayList:抛出java.util.ConcurrentModificationException异常

// 具体集合类型Vector:不会抛异常,线程安全,但是这个类太古老

// Collections.synchronizedList(new ArrayList<>()):不会抛异常,但是锁定范围大,性能低
// public void add(int index, E element) { synchronized (mutex) {list.add(index, element);} }
// public E get(int index) { synchronized (mutex) {return list.get(index);} }

// 具体集合类型CopyOnWriteArrayList:使用了写时复制技术,兼顾了线程安全和并发性能
List<String> list = new CopyOnWriteArrayList<>();

3、写时复制技术

images

  • 使用写时复制技术要向集合对象中写入数据时:先把整个集合数组复制一份
  • 将新数据写入复制得到的新集合数组
  • 再让指向集合数组的变量指向新复制的集合数组

优缺点:

  • 优点:兼顾了性能和线程安全,允许同时进行读写操作
  • 缺点:由于需要把集合对象整体复制一份,所以对内存的消耗很大

对应类中的源代码:

  • 所在类:java.util.concurrent.CopyOnWriteArrayList
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

第二节 Set集合

采用了写时复制技术的Set集合:java.util.concurrent.CopyOnWriteArraySet

测试代码:

// 1、创建集合对象
Set<String> set = new CopyOnWriteArraySet<>();

// 2、创建多个线程,每个线程中读写 List 集合
for (int i = 0; i < 5; i++) {

    new Thread(()->{

        for (int j = 0; j < 5; j++) {

            // 写操作:随机生成字符串存入集合
            set.add(UUID.randomUUID().toString().replace("-","").substring(0, 5));

            // 读操作:打印集合整体
            System.out.println("set = " + set);
        }

    }, "thread-"+i).start();

}

对应类中的源码:

  • 所在类:java.util.concurrent.CopyOnWriteArraySet
    public boolean add(E e) {
        return al.addIfAbsent(e);
    }

  • 所在类:java.util.concurrent.CopyOnWriteArrayList
    private boolean addIfAbsent(E e, Object[] snapshot) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) {
                // Optimize for lost race to another addXXX operation
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++)
                    if (current[i] != snapshot[i] && eq(e, current[i]))
                        return false;
                if (indexOf(e, current, common, len) >= 0)
                        return false;
            }
            Object[] newElements = Arrays.copyOf(current, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

第三节 Map 集合

ConcurrentHashMap

测试代码:

// 1、创建集合对象
Map<String, String> map = new ConcurrentHashMap<>();

// 2、创建多个线程执行读写操作
for (int i = 0; i < 5; i++) {
    new Thread(()->{

        for (int j = 0; j < 5; j++) {

            String key = UUID.randomUUID().toString().replace("-","").substring(0, 5);
            String value = UUID.randomUUID().toString().replace("-","").substring(0, 5);

            map.put(key, value);

            System.out.println("map = " + map);
        }

    }, "thread" + i).start();
}

ConcurrentHashMap 底层用的是【锁分段】技术。它的典型应用就是来实现微服务的注册中心

微服务名称:Map的key

微服务对象:Map的value

使用ConcurrentHashMap 实现并发读写

发现注册:读操作

服务注册:写操作

十七、ConcurrenthashMap

JDK1.8

image.png

ConcurrenthashMap在没有Hash冲突时,以CAS的方式尝试插入到数组中

如果有Hash冲突,这个时候回将当前数组索引位置锁住,以synchronized的形式挂到链表下面

如果数组长度达到了最开始的长度的0.75时,就要将数组长度扩大二倍,从来避免链表过长造成查询效率较低

ConcurrenHashMap在并发扩容时,如何保证安全?

在计算Node中key的hash值时,会特意的将hash值正常情况的数值定义为正数

负数有特殊的含义,如果hash值为-1,代表当前节点正在扩容

ConcurrenthashMap会在扩容时,每次将老数组中的数据table.size - 1 ~ table.size - 16索引的位置移动,然后再迁移其他索引位置的数据,如果有线程在插入数据时,发现正在扩容,找还没有被迁移数据的索引位置,帮助最开始扩容的线程进行扩容,

最开始扩容A:31~16

线程B插入数据,发现正在扩容,帮你迁移数据,15~0索引位置

每一个迁移完毕的数据,都会加上标识,代表扩容完毕,放上一个ForwardingNode节点,代表扩容完毕, 而且再扩容是,不会应用ConcurrentHashMap的遍历,查询和添加(发现扩容,会帮忙~)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙龙龙呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值