juc概述

  1. wait 和 sleep的区别 ```xml sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用 sleep不会释放锁,它也不需要占用锁.wait会释放锁,但调用它的前提是当前线程占有锁 他们都可以被interrupted方法中断

原子性:即一个操作或多个操作,要么全部执行并且执行过程中不会被任何因素打断,要么就都不执行

  1. 2. 并发和并行
  2. ```xml
  3. 串行模式: 多个任务依次执行
  4. 并行模式
  5. 并发: 同一时刻多个线程在访问同一个资源,多个线程对一点
  6. 并行: 多项工作一起执行,之后再汇总
  1. 管程 ```xml Monitor: 监视器(锁) 是一种同步机制,保证同一时间,只有一个线程访问被保护的数据(或代码)

jvm同步基于进入和退出,使用管程对象实现的

  1. 4. 用户线程和守护线程
  2. ```xml
  3. 用户线程: 自定义线程
  4. 守护线程: 比如垃圾回收(运行在后台)
  5. 主线程结束了,用户线程还在运行,jvm还在存活状态
  6. 没有用户线程,都是守护线程,jvm就会停止运行
  1. public class test {
  2. public static void main(String[] args) {
  3. Thread aa = new Thread(() -> {
  4. //isDaemon():判断该线程是否是守护线程 true是
  5. System.out.println(Thread.currentThread().getName()+"::"+Thread.currentThread().isDaemon());
  6. while (true){
  7. //让该线程一级一直执行
  8. }
  9. }, "aa");
  10. aa.start();
  11. System.out.println(Thread.currentThread().getName()+"over");
  12. }
  13. }

image.png

Lock接口

  1. Synchronized关键字 ```xml 多线程编程步骤:
    1. 创建资源类,在资源类中创建属性和操作方法(高内聚)
    2. 在资源类操作方法中 a.判断 b.干活 c.通知
    3. 创建多个线程,调用资源类的操作方法
    4. 防止虚假唤醒

一个对象里面如果有多个Synchronized方法,某一时刻内,只要一个线程去调用其中的一个Synchronized方法了, 其他的线程都只能等待,换句话说,某一时刻内,只有唯一一个线程去访问这些Synchronized方法 锁的是当前对象this,呗锁定后,其他的线程都不能进入到当前对象的其他Synchronized方法

所有的非静态同步方法用的都是同一把锁-实例对象本身 Synchronized实现同步的基础:java中的每一个对象都可以作为锁 具体表现有三种形式

  1. 1. 对于普通同步方法,锁是当前实例对象
  2. 2. 对于静态同步方法,锁的是当前类的Class对象
  3. 3. 对于同步方法块,锁的是Synchronized括号里配置的对象

当一个线程试图访问同步代码块时,他首先必须得到锁,退出或异常的时候必须释放锁 所有静态同步方法用的是同一把锁—类对象本身 这两把锁(this,Class)是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞争条件的 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁之后才能获取锁 不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之就按,只要他们同一个类的实例对象

  1. 2. Look接口
  2. ```xml
  3. Lock实现提供了比使用synchronized方法和语句可获得更广泛的锁定操作
  4. 可重入锁: 可重复使用
  5. Lock和Synchronized区别
  6. 1. Lock不是java语言内置的,synchronized是java语言的关键字,因此是内置特性,Lock是一个类
  7. 通过这个类可以实现同步访问.
  8. 2. Lock和synchronized有一点非常大的不同,采用synchronized不需要用户手动去释放锁,当synchronized
  9. 方法或者synchronized代码块执行完成之后,系统会自动让线程释放对锁的占用,Lock则必须要用户手动释放锁
  10. 如果没有手动释放锁,会出现死锁现象.
  11. 3. synchronized在发生异常的时,会自动释放线程占有的锁,因此不会导致死锁现象;而Lock在发生异常时,如果没有
  12. 主动通过unLock()去释放锁,则很可能造成死锁现象,因此在使用Lock的时候需要在finally中释放锁
  13. 4. Lock可以让等待的线程响应中断,而synchronized却不行,使用synchronized时,等待线程会一直等待下去,不能够响应中断
  14. 5. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
  15. 6. Lock可以提高多个线程进行读操作的效率
  16. 在性能上来说,如果竞争资源不激烈,两者性能差距是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized

线程间通信

  1. class Share{
  2. private int number = 0;
  3. public synchronized void incr() throws InterruptedException {
  4. //如果number=0 就+1
  5. if (number!=0){
  6. this.wait();
  7. }
  8. number++;
  9. System.out.println(Thread.currentThread().getName()+"::"+number);
  10. //通知其他线程起来干活
  11. this.notifyAll();
  12. }
  13. public synchronized void decr() throws InterruptedException {
  14. //如果number=1 就-1
  15. if (number!=1){
  16. this.wait();
  17. }
  18. number--;
  19. System.out.println(Thread.currentThread().getName()+"::"+number);
  20. //通知其他线程起来干活
  21. this.notifyAll();
  22. }
  23. }
  1. 虚假唤醒 ```xml wait()在哪里睡在哪里醒 AA,CC线程做+1操作 BB,DD线程做-1操作 当值为0时 AA线程抢到资源对资源进行+1 资源变为1 此时CC线程抢到资源
    1. if (number!=0){
    2. this.wait();//此处睡着
    3. }
    此时AA线程再次抢到资源 在CC线程相同的位置睡 当DD/BB线程抢到资源的时候满足
    1. if (number!=1){
    2. this.wait();
    3. }
    this.notifyAll(); 唤醒其他线程 AA,CC线程被唤醒后都进行+1操作,导致资源变成2

可以使用while(number!=0) 解决这个问题

  1. <a name="Em7FX"></a>
  2. ## 线程间的定制通信
  3. 1. 举例
  4. ```xml
  5. 启动三个线程,按照如下要求
  6. 1. AA打印5次,BB打印10次,CC打印15 次
  7. 2. AA打印5次,BB打印10次,CC打印15 次
  8. 进行10轮

集合的线程安全

1.list集合

  1. public static void main(String[] args) {
  2. List<String> list = new ArrayList<>();
  3. for (int i = 0; i <50 ; i++) {
  4. new Thread(()->{
  5. list.add(UUID.randomUUID().toString().substring(0,8));
  6. System.out.println(list);
  7. },String.valueOf(i)).start();
  8. }
  9. }
  10. Exception in thread "8" java.util.ConcurrentModificationException

使用Vector解决ArrayList线程安全问题(不推荐)

  1. // Vector中的add方法
  2. public synchronized boolean add(E e) {
  3. modCount++;
  4. ensureCapacityHelper(elementCount + 1);
  5. elementData[elementCount++] = e;
  6. return true;
  7. }

使用Collections.synchronizedList解决(不推荐)

  1. List<String> list = Collections.synchronizedList(new ArrayList<>());

使用CopyOnWriteArrayList解决

  1. List<String> list = new CopyOnWriteArrayList<>();
  2. 写实复制技术
  3. 1. 并发读取
  4. 2. 独立写:写操作时将之前的集合复制一份后写入新的内容
  5. 3. 写操作完成后,合并两个集合.新的读操作来读取新的集合
  6. CopyOnWrite容器即写时复制的容器.往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy
  7. 复制出一个新的容器Object[] newElements,然后新的容器Object[] newElements里添加元素,添加完元素之后,
  8. 再讲原容器的引用指向新的容器
  9. 这样做的好处就是可以对CopyOnWrite容器进行并发读取,而不需要加锁,因为不会向当前容器添加任何元素
  10. ,所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器

CopyOnWrite见解

  1. /**
  2. * 写时复制技术
  3. * copyOnWrite
  4. * 写入的时候,创建一个新的长度为原数组长度+1的新数组 然后将新数据写到新数组中
  5. * 再用新数组替换就数组
  6. * 删除的时候,创建一个新的长度为原数组长度-1的新数组 然后通过数组复制的方式实现删除
  7. * 然后替换原来的数组
  8. *
  9. * 写时复制技术最终目的:为了在读多写少的场景下,通过写时复制技术,让大量读请求在无需加锁
  10. * 消耗性能的情况下,保证多线程并发读写的情况下线程安全
  11. */
  12. CopyOnWrite主要基于等效不可变思想
  13. 将数组设计成是不可变的(每次都是新的数组替换就数组,不存在对数组修改的情况)
  14. 数组中的对象是可变的
  15. 这种现象叫做等效不可变
  1. public boolean add(E e) {
  2. final ReentrantLock lock = this.lock;//获取锁
  3. lock.lock();//上锁
  4. try {
  5. Object[] elements = getArray();//获取原来的数组(Array)
  6. int len = elements.length;//计算数组的长度
  7. Object[] newElements = Arrays.copyOf(elements, len + 1);//复制一个长度+1的新数组
  8. newElements[len] = e;//将新加入的数据放在新数组的尾部
  9. setArray(newElements);//将新数组复制给Array
  10. return true;
  11. } finally {
  12. lock.unlock();//解锁
  13. }
  14. }
  1. hashSet和hashMap ```xml CopyOnWriteArraySet 解决set ConcurrentHashMap 解决Map

HashSet底层是HashMap

  1. <a name="0xKjR"></a>
  2. ## 多线程锁
  3. 1. synchronized锁
  4. ```xml
  5. 1. 对于普通方法,锁是当前实例对象
  6. 2. 对于静态同步方法,锁的当前类的class对象
  7. 3. 对于同步方法块,锁是synchronized括号里配置的对象
  1. 公平锁和非公平锁

    1. 非公平锁:线程饿死,效率高
    2. 公平锁:阳光普照,效率相对低
  2. 可重入锁

  3. 死锁

    1. 产生死锁的原因
    2. 1. 系统资源不足
    3. 2. 进程运行推进顺序不合适
    4. 3. 资源分配不当
    5. 验证是否是死锁
    6. jps 类似linux中的ps -ef
    7. jstack jvm自带的堆栈跟踪工具

    Callable

    1. callable和runnable接口的区别
    2. 1.callable接口有返回值
    3. 2.callable接口有异常
    4. 3.落地方法不一样callable接口是call方法

    线程执行计数器

  4. CountDownLatch

    1. CountDownLatch主要有两个方法,当一个或多个线程调用await()方法时,这些线程会阻塞
    2. 其他线程调用countDown方法会将计数器-1(调用countDown方法的线程不会阻塞)
    3. 当计数器的值变成0时,因await()方法阻塞的线程会被唤醒,继续执行
    1. CountDownLatch countDownLatch = new CountDownLatch(6);//设置计数器技术次数
    2. for (int i = 0; i < 6; i++) {
    3. new Thread(()->{
    4. System.out.println(Thread.currentThread().getName()+"同学走了");
    5. countDownLatch.countDown(); //计数器-1
    6. },String.valueOf(i)).start();
    7. }
    8. countDownLatch.await();
    9. System.out.println("关门");
  5. CyclicBarrier

    1. 设置一个限制数,当等待进程数量达到限制数,执行指定线程.并唤醒被阻塞的线程

    ```java CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()-> System.out.println(“召唤神龙“)); for (int i = 1; i <= 7; i++) {

    1. final int ii = i;
    2. new Thread(()->{
    3. System.out.println("找到了第"+ ii+"颗龙珠");
    4. try {
    5. cyclicBarrier.await();
    6. } catch (InterruptedException | BrokenBarrierException e) {
    7. e.printStackTrace();
    8. }
    9. System.out.println("第"+ ii+"颗龙珠飞走了");
    10. },String.valueOf(i)).start();

    }

  1. 找到了第1颗龙珠
  2. 找到了第5颗龙珠
  3. 找到了第4颗龙珠
  4. 找到了第3颗龙珠
  5. 找到了第2颗龙珠
  6. 找到了第7颗龙珠
  7. 找到了第6颗龙珠
  8. ***召唤神龙***
  9. 6颗龙珠飞走了
  10. 1颗龙珠飞走了
  11. 4颗龙珠飞走了
  12. 5颗龙珠飞走了
  13. 7颗龙珠飞走了
  14. 2颗龙珠飞走了
  15. 3颗龙珠飞走了
  1. 3. Semaphore
  2. ```xml
  3. 限制某个资源最大同时访问(信号灯)
  4. 在信号量上定义两种操作:
  5. 1. acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量-1)
  6. 要么一直等待下去,知道有线程释放信号量,或超时
  7. 2. release(释放) 实际上会将信号量的值+1,然后唤醒等待的线程
  8. 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制
  1. Semaphore semaphore = new Semaphore(4); //现在有4个停车位
  2. for (int i = 1; i <= 6; i++) {
  3. new Thread(()->{
  4. try {
  5. semaphore.acquire(); //位置-1
  6. System.out.println(Thread.currentThread().getName()+"抢占了车位");
  7. //if (semaphore.tryAcquire())
  8. TimeUnit.SECONDS.sleep(4); //线程睡4s
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }finally {
  12. System.out.println(Thread.currentThread().getName()+"开走了");
  13. semaphore.release(); //让出位置
  14. }
  15. },String.valueOf(i)).start();
  16. }
  17. 1抢占了车位
  18. 4抢占了车位
  19. 3抢占了车位
  20. 2抢占了车位
  21. 1开走了
  22. 4开走了
  23. 5抢占了车位
  24. 3开走了
  25. 2开走了
  26. 6抢占了车位
  27. 5开走了
  28. 6开走了

读写锁

  1. ReaderWriteLock

    1. 多个线程同时读一个资源类没有任何问题,所以为了满足并发量.读取共享资源应该同时进行
    2. 但是如果有一个线程想去写共享资源,就不应该再有其他的线程可以对资源进行读或写
    3. 读-读能共存
    4. 读-写不能共存
    5. 写-写不能共存

    ```java private volatile Map map = new HashMap<>(); private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); public void put(String key,Object value){

    1. readWriteLock.writeLock().lock();
    2. try {
    3. System.out.println(Thread.currentThread().getName()+"开始写入数据");
    4. map.put(key, value);
    5. System.out.println(Thread.currentThread().getName()+"写入完成");
    6. } finally {
    7. readWriteLock.writeLock().unlock();
    8. }

    } public void get(String key){

    1. readWriteLock.readLock().lock();
    2. try {
    3. System.out.println(Thread.currentThread().getName()+"开始读取数据");
    4. map.get(key);
    5. System.out.println(Thread.currentThread().getName()+"读取完成");
    6. } finally {
    7. readWriteLock.readLock().unlock();
    8. }

    }

  1. 1开始写入数据
  2. 1写入完成
  3. 2开始写入数据
  4. 2写入完成
  5. 3开始写入数据
  6. 3写入完成
  7. 5开始写入数据
  8. 5写入完成
  9. 4开始写入数据
  10. 4写入完成
  11. 4开始读取数据
  12. 1开始读取数据
  13. 1读取完成
  14. 5开始读取数据
  15. 2开始读取数据
  16. 2读取完成
  17. 4读取完成
  18. 5读取完成
  19. 3开始读取数据
  20. 3读取完成
  1. <a name="hc22G"></a>
  2. ## 阻塞队列
  3. 1. BlockingQueue
  4. ```xml
  5. 当队列是空的,从队列中获取元素的操作将会被阻塞
  6. 当队列是满的,向队列中添加元素的操作将会被阻塞
  7. 试图从空的队列中获取元素的线程将会被阻塞,知道其他线程往空的队列插入新的元素
  8. 试图向已满的队列中添加新的元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空
  9. 使队列变得空闲起来并后续新增
  10. 用处:
  11. 在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会被自动唤醒
  12. 为什么需要BlockingQueue
  13. 好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切都被BlockingQueue一手包办了
  14. 在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,
  15. 尤其还要兼顾效率和线程安全,而这给我们的程序带来了不小的复杂度
  1. BlockingQueue核心方法

image.png

  1. 抛出异常:
  2. 当阻塞队列满时,再往队列里add插入元素会抛出IllegalStateException: Queue full
  3. 当阻塞队列空时,再从队列里remove移除元素会抛出NoSuchElementException
  4. 特殊值 :
  5. 插入方法,成功true失败false
  6. 移除方法,成功返回出队列的元素,队列里没有就返回空
  7. 一直阻塞:
  8. 当阻塞队列满时,生产者线程继续王队列里put元素,队列会一直阻塞直到put数据or响应中断退出
  9. 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用
  10. 超时退出:
  11. 当阻塞队列满时,队列会阻塞生产者线程一定 时间,超过限时后生产者线程会退出

线程池

为什么要用线程池

  1. 线程池的优势

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

  3. 降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗

  4. 提高响应速度.当任务到达时,任务可以不需要等待线程创建就能立即执行
  5. 提高线程的可管理性.线程是稀缺资源,如果无线制的创建,不进会消耗系统资源,还会降低系统的稳定性 使用线程池可以进行统一的分配,调优和监控 ```

    线程池的使用

  6. 线程池的分类 ```xml

  7. Executors.newFixedThreadPool(int);指定线程数量的线程池 执行长期任务性能好,创建一个线程池,一池有N个固定的线程(有固定线程数的线程)
  8. Executors.newSingleThreadExecutor();单个线程的线程池
  9. Executors.newCachedThreadPool();可扩容

    1. ```java
    2. ExecutorService service = Executors.newFixedThreadPool(5);
    3. try {
    4. for (int i = 1; i <= 10; i++) {
    5. service.execute(()->{
    6. System.out.println(Thread.currentThread().getName()+"办理业务");
    7. try {
    8. TimeUnit.SECONDS.sleep(2);
    9. } catch (InterruptedException e) {
    10. e.printStackTrace();
    11. }
    12. });
    13. }
    14. } finally {
    15. service.shutdown();
    16. }
    17. pool-1-thread-1办理业务
    18. pool-1-thread-5办理业务
    19. pool-1-thread-4办理业务
    20. pool-1-thread-3办理业务
    21. pool-1-thread-2办理业务
    22. pool-1-thread-4办理业务
    23. pool-1-thread-3办理业务
    24. pool-1-thread-1办理业务
    25. pool-1-thread-5办理业务
    26. pool-1-thread-2办理业务

    线程池的重要参数

  10. 参数介绍(源代码中的注释)

    1. 源代码中注释:(谷歌翻译)
    2. 使用给定的初始参数创建一个新的ThreadPoolExecutor 。
    3. 参数:
    4. corePoolSize – 要保留在池中的线程数,即使它们处于空闲状态,除非设置了allowCoreThreadTimeOut
    5. maximumPoolSize – 池中允许的最大线程数
    6. keepAliveTime – 当线程数大于核心数时,这是多余空闲线程在终止前等待新任务的最长时间。
    7. unit – keepAliveTime参数的时间单位
    8. workQueue – 用于在执行任务之前保存任务的队列。 这个队列将只保存execute方法提交的Runnable任务。
    9. threadFactory – 执行程序创建新线程时使用的工厂
    10. handler – 执行被阻塞时使用的处理程序,因为达到了线程边界和队列容量
    11. 抛出:
    12. IllegalArgumentException – 如果以下情况之一成立: corePoolSize < 0 keepAliveTime < 0 maximumPoolSize <= 0 maximumPoolSize < corePoolSize
    13. NullPointerException – 如果workQueue或threadFactory或handler为 null
  11. 参数解析 ```xml

  12. corePoolSize:常驻线程数 线程池中常驻核心线程数
  13. maximumPoolSize:最大线程数 线程池中能容纳同时执行的最大线程数,此值必须大于等于1
  14. keepAliveTime:多余线程等待时间 多余的空闲线程的存活时间,当前线程池中线程数量超过corePoolSize时,当空闲时达到keepAliveTime时, 多余线程会被销毁,直到线程数量等于corePoolSize
  15. unit:时间单位 keepAliveTime的单位
  16. workQueue:阻塞队列 任务队列,被提交但是尚未执行的任务
  17. threadFactory:工厂 表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认即可
  18. handler:拒绝策略 表示当线程队列满时,并且最大线程数大于等于maximumPoolSize时如何来拒绝请求执行的runnale的策略

    1. <a name="BfljT"></a>
    2. ## 线程池的底层工作原理
    3. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/21477905/1629474689538-e2b25e3d-63e8-4492-b8fb-9707187e6507.png#align=left&display=inline&height=352&margin=%5Bobject%20Object%5D&name=image.png&originHeight=352&originWidth=509&size=115354&status=done&style=none&width=509)
    4. ```xml
    5. 流程描述:
    6. 1. 在创建线程池后,开始等待请求
    7. 2. 当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
    8. 2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建(分配)线程运行这个任务
    9. 2.2 如果正在运行的线程数大于或等于corePoolSize,那么将这个线程放入队列
    10. 2.3 如果此时队列满了且运行的线程数小于maxinummPoolSize,那么就创建非核心(临时)线程运行队列中的任务
    11. 2.4 如果队列满了且正在运行的线程数量大于等于maxinumPoolSize,那么线程池会启动饱和拒绝策略
    12. 3. 当一个线程完成任务时,他会从队列中取下一个任务来执行
    13. 4. 当一个线程无事可做超过一定时间(keepAliveTime)时,线程会判断:
    14. 4.1 如果当前运行的线程数大于corePoolSize,那么这个线程会被停掉
    15. 4.2 在线程池的所有任务完成后,它最终会缩小到corePoolSize的大小
  19. 线程池的选择,生产中设置合理参数

    1. 都不用!!!
    2. 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,
    3. 这样的处理方式让写的同学更加明确线程池的运行规则,避免资源耗尽的风险

    image.png

    自定义线程池

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

  21. AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行

  22. CallerRunsPolicy:”调用者运行”一种调节机制,该策略即不会抛弃任务,也不会抛出异常, 而是将某些任务回退到调用者,从而降低新任务的流量
  23. DiscardOldestPolicy:抛弃队列中等待最久的任务,然而把当前任务加入队列中尝试再次提交当前任务
  24. DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的策略 ```

  25. 创建线程池

    1. ThreadPoolExecutor pool = new ThreadPoolExecutor(
    2. 2, //核心线程数
    3. 5,//最大线程数
    4. 2L,//超时等待时间
    5. TimeUnit.SECONDS,//等待时间单位
    6. new LinkedBlockingDeque<>(3),//阻塞队列
    7. Executors.defaultThreadFactory(),//线程创建工厂
    8. new ThreadPoolExecutor.AbortPolicy());//线程拒绝策略
    1. cpu密集型
    2. 最大线程数建议设置为cpu线程数+1
    3. Runtime.getRuntime().availableProcessors() 获取cpu的线程数