高并发基础——JUC(concurrentMap,executorService)

一.ConcurrentMap(并发映射):

引入:

之前在Java基础中,我们学习过了map和它的子类hashmap,hashtable。所以在学习concurrentMap之前我们需要复习一下,hashmap的原理。

hashmap原理:

底层是使用 数组+链表 ,数组查询快,链表增删快。所以hashmap查询快增删也快。默认初始容量是16,默认加载因子0.75
高并发基础——JUC(concurrentMap,executorService)_第1张图片
hashmap中我们知道,它是线程不安全的,通俗的说,就是一个线程进来是没有锁来锁这个map的,这就会导致线程不安全,而今天要学习的concurrentMap,就解决了高并发情况下的线程安全问题,相比于hashTable它的效率更高。
concurrent拥有两个子类:ConcurrentHashMap ,ConcurrentNavigableMap。

ConcurrentHashMap 并发哈希映射:

在学习之前,需要明白这个东西在什么地方使用,它的用法合hashmap一样,你只需要在使用hashmap的时候思考一下,我是不是还有个叫ConcurrentHashMap的东西可以使用。
底层原理:
它的底层合hashmap一样是 数组+链表。数组的默认容量是16个桶,默认加载因子0.75,扩容是增加一倍的桶数。
高并发基础——JUC(concurrentMap,executorService)_第2张图片
高并发基础——JUC(concurrentMap,executorService)_第3张图片
通过源码可以更好的看出,初始值右移1个单位(/2)然后+初始容量+1。把计算出来的值再 放入下面那个函数,就是找2的N次方。
案例:初始化值是48,48>>1=24,24+48+1=73,63<73<128,128=2的7次方,实际容量就是128.

最大容量:
桶也是有极限的,2的30次方

变化为红黑树:
当桶中的元素个数超过8个,且桶的个数多余64,就会把元素数量多余超过8个的链表扭成红黑树。

ConcurrentHashMap是一个异步线程安全的映射:
异步——可以让多个线程在一个时间段内一起访问
采用了分段/桶锁机制,并且在分段锁基础上引入了读写锁机制。
读锁:允许多个线程同时读,不允许线程写
写锁:只允许一个线程写,不允许线程读

实现方法:
假如是hashmap,比如线程a先去访问链表做查询操作,在查询出数据的前0.00001秒线程b抢到了CPU,进去把数据修改了,这个时候线程a读取出来的数据就不对了。再假如是hashtable,线程a先进去访问,这个时候它就会把整个桶都锁住了,要访问其他链表的线程也没有办法访问,这就造成了效率低。
这个时候,主角出现了就是我们的concurrentHashMap,它是怎么做的?它不锁整个桶,它锁的是桶里的列表,比如你线程a访问第一个链表做操作,它是不会影响线程c对第三个链表做操作的。而且多个线程访问同一个链表,还做了读锁和写锁;读锁,当有多个线程去读取同一个链表,这时就不允许有线程去修改该链表。写锁,当有一个线程去对链表做写操作,就不允许其他线程去读这个列表。
高并发基础——JUC(concurrentMap,executorService)_第4张图片
使用代码:
声明

 ConcurrentHashMap<String, String> map =new ConcurrentHashMap<>(48);

至于它的方法,和hashmap基本一样,就不做解释了。

ConcurrentNavigableMap - 并发导航映射:

ConcurrentNavigableMap 是一个接口,我们要学习使用的是它的实现类 ConcurrentSkipListMap - 并发跳跃表映射。
底层:
ConcurrentSkipListMap 的底层就是跳跃链表。
跳跃链表: 链表一开始是存入0-4号元素,使用跳跃链表,链表中存入1和3两个元素。比如要寻找2号元素,先去跳远链表中看,2大于1,1小于3,那就去1的右边3的左边找。
跳跃列表的注意: 针对有序列表进行操作 ,适用于查询多而增删少的场景,最上层跳跃表的元素个数不能少于2个,如果在跳跃表中新添了元素,那么新添的元素是否要提取到上层的跳跃表中遵循"抛硬币"原则
高并发基础——JUC(concurrentMap,executorService)_第5张图片
截取map:
ConcurrentNavigableMap是可以像string list那样做skip的,这就是为什么它的内容是要有序的。
使用代码:

      // 提供了用于截取子映射的方法
        ConcurrentNavigableMap<String, Integer> map =
                new ConcurrentSkipListMap<>();
        map.put("d", 5);
        map.put("e", 3);
        map.put("a", 4);
        map.put("y", 6);
        map.put("u", 5);
        map.put("o", 9);
        map.put("m", 0);
        System.out.println(map);
        // 从头开始截取到指定的位置
        System.out.println(map.headMap("e"));
        // 从指定位置开始截取到末尾
        System.out.println(map.tailMap("e"));
        // 截取指定范围内的元素
        System.out.println(map.subMap("e", "u"));

二.ExecutorService - 执行器服务

引入:

虽然名字很有迷惑性,但是它的本质就是一个线程池。要使用它我们就需要知道线程池里有什么内容,需要定义一些什么,是怎么工作的。

工作原理:
线程池刚创建的时候是空的

  • core thread(核心线程):每过来一个请求,就会在线程池中创建一个核心线程来处理这个请求。核心线程的数量在定义线程池的时候需要指定。核心线程用完之后不会被销毁而是等待下一个请求 。只要核心线程没有达到指定的数量,那么每一个请求都会触发创建一个新的核心线程处理 。

  • work queue(工作队列):如果核心线程被全部占用,那么新来的请求会放到工作队列中进行排队等待。工作队列本质上是一个阻塞式队列(blocking queue)

  • temporary thread(临时线程):如果工作队列被全部占用,那么新来的请求会交给临时线程来处理。临时线程的数量在定义线程池的时候需要指定 临时线程用完之后会存活一段时间,如果在这段时间内没有接收到新的任务那么就会被销毁。工作队列中的任务不会被临时线程执行:尽量缩短临时线程的存活时间,尽量提高核心线程的利用率 。

  • rejectedExecutionHandler(拒绝执行处理器):如果临时线程被全部占用,那么新来的请求会交给拒绝执行处理器来处理
    高并发基础——JUC(concurrentMap,executorService)_第6张图片

代码:
声明线程池:

// corePoolSize - 核心线程数量
        // maximumPoolSize - 总线程数量 = 核心线程数 + 临时线程数
        // keepAliveTime - 临时线程的存活时间
        // unit - 时间单位
        // workQueue - 工作队列
        // handler - 拒绝执行处理器,可以不指定
        ExecutorService es = new ThreadPoolExecutor(
                5, // 5个核心线程
                12, // 7个临时线程
                5, TimeUnit.SECONDS, // 临时线程用完之后能够存活5s
                new ArrayBlockingQueue<Runnable>(5),
                new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        // 如果有拒绝流程,那么需要将拒绝流程写到这个方法中
                        // 例如:日志记录(请求、时间、IP等),跳转页面(努力加载中,失败了 - 秒杀等)等
                        System.out.println(r + "被拒绝了~~~");
                    }
                }
        );

声明线程类:

class ESThread implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("start");
            Thread.sleep(3000);
            System.out.println("finish");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

调用线程池中的线程:

// new Thread(new ESThread()).start();
        // 核心 - 5 临时 - 7 队列 - 5
        // 12个start,3个拒绝 -> 3s之后再有5个start
        for (int i = 0; i < 20; i++) {
            es.execute(new ESThread());
        }
        // 如果线程池用完,可以关闭线程池
        // 实际开发中,线程池开启之后一般是不关的
        es.shutdown();

executorService的两个代理线程池——newCachedThreadPool和newFixedThreadPool:

1.newCachedThreadPool:
特点:

  1. 没有核心线程全部都是临时线程。
  2. 临时线程的数量是Integer.MAX_VALUE,即2^31-1考虑到单台服务器所能承载的线程数量远远小于21亿,所以一般认为这个线程池能够处理无限多的请求。
  3. 临时线程用完之后最多存活60s。
  4. 工作队列是一个同步队列,实际生产过程中, 一般在测试阶段就会利用空请求将这个工作队列填充,此时可以认为这个线程池没有工作队列。
// 大池子小队列
// 适用于高并发的短任务的场景,例如即时通讯
// 不适用于长任务场景
ExecutorService es =Executors.newCachedThreadPool();

2.newFixedThreadPool:

特点:

  1. 没有临时线程全部都是核心线程
  2. 工作队列是LinkedBlockingQueue,默认是Integer.MAX_VALUE, 一般认为能够存储无限多的请求

给入参数是核心线程个数

// 小池子大队列
// 适用于并发低的长任务场景,例如网盘下载
// 不适用于高并发的短任务场景
ExecutorService es =Executors.newFixedThreadPool(5);

ScheduledExecutorService(定时调度执行器服务器):
使用这个线程池可以推迟每个线程执行的时间间隔。

线程类:

class ScheduleThread implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("hello~~~");
            Thread.sleep(8000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

实现定时执行:

 //核心线程5个
 ScheduledExecutorService ses =Executors.newScheduledThreadPool(5);
 // 从上一次启动,开始计算下一次的启动时间
        // 每隔5s执行一次
        // 间隔时间是取执行时间和指定时间的最大值
        ses.scheduleAtFixedRate(new ScheduleThread(), 0,5, TimeUnit.SECONDS);
 // 从上一次结束,开始计算下一次的启动时间
        // 每隔5s执行一次
        ses.scheduleWithFixedDelay(new ScheduleThread(), 0, 5, TimeUnit.SECONDS);

分叉合并池:

分叉——将一个大任务进行拆分,拆分成多个小任务分配给多个线程执行,这个过程称之为分叉

合并——将分叉出来的任务结果进行汇总,这个过程称之为合并

工作窃取——在分叉合并中,如果一个核将它身上所有的任务执行完成之后,并不空闲下来,而是会去随机扫描一个核,然后从这个核的任务队列尾端来偷取一个任务回来执行,这种方式称之为work-stealing(工作窃取)策略,因为工作窃取策略涉及到CPU核之间的线程调度问题,所以底层是用C语言实现的

案例:求1-100000000000L的和,使用分叉合并池,把计算分配到各个核上执行,然后再合并。

mian方法中的线程调用:

//声明分叉合并线程池
ForkJoinPool p = new ForkJoinPool();
        //启动线程
        ForkJoinTask<Long> f = p.submit(new Sum(1, 100000000000L));
        //获取值
        System.out.println(f.get());
        p.shutdown();

线程类:

class Sum extends RecursiveTask<Long> {

    private long start;
    private long end;

    public Sum(long start, long end) {
        this.start = start;
        this.end = end;
    }

    // 分叉合并的逻辑就是在这个方法中覆盖
    @Override
    protected Long compute() {
        // 分叉:将大的拆成小的
        // 拆的过程中得有限度
        if (end - start <= 10000) {
            // 如果范围内的数字个数不到10000个,此时认为数量已经比较少了
            // 那么不能继续拆了,就要将这个范围内的数字来进行求和
            long sum = 0;
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else {
            // 说明范围内的数字依然比较多,那就继续分叉
            long mid = (start + end) / 2;
            Sum left = new Sum(start, mid);
            Sum right = new Sum(mid + 1, end);
            // 需要将拆分出来这两半各自定义成两个线程来执行
            left.fork();
            right.fork();
            // 分叉完之后,最后需要将结果汇总
            return left.join() + right.join();
        }
    }

使用Callable来实现线程类的创建:
之前学过使用继承thread类和实现runnable接口两种方法来实现线程的书写,但是这两个方法都没有办法在执行run方法的时候返回数据,使用callable就可以实现数据的返回

线程类:

// 泛型表示的结果类型
class CDemo implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Success~";
    }
}

启动线程:

ExecutorService es =Executors.newCachedThreadPool();
        // 将结果封装成了Future对象
        Future<String> f = es.submit(new CDemo());
        System.out.println(f.get());
        es.shutdown();

你可能感兴趣的:(多线程,java)