java多线程

目录

一、获取多线程的4种方式(Runnable/Callable/Thread/线程池)

二、相关细节

1、自定义线程池的相关参数

 1.1 生产环境中如何配置corePoolSize和maximumPoolSize?

 1.2 四种拒绝策略

2、为何使用线程池?使用线程池的好处?为何不用默认线程池?

2.1 为何用?(线程复用 / 控制最大并发数 / 管理线程)

2.2 用的好处?(降低资源消耗/提高响应速度/提高线程的可管理性)

2.3 线程池的底层原理?

三、线程不安全

1、线程不安全的ArrayList(Vector, Collections.synchronized(), JUC方法)

1.1 线程不安全的原因

1.2 解决办法

2、线程不安全的HashSet

2.1 线程不安全的原因

2.2 解决办法

3、线程不安全的HashMap

3.1 线程不安全的原因

3.2 解决办法

四、核心线程数、最大线程数、阻塞队列长度的计算

1、核心线程数

【常规-核心线程数的计算】

【max-核心线程数的计算】

【实际-核心线程数的计算】

2、最大线程数(我自己总结的)

3、阻塞队列


本文通过学习:周阳老师-尚硅谷Java大厂面试题第二季 总结的多线程相关的笔记

一、获取多线程的4种方式(Runnable/Callable/Thread/线程池)

  • 实现Runnable接口
  • 实现Callable接口
  • 实例化Thread类
  • 使用线程池获取
Runnableclass MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable");
    }
}
Callableclass MyThread2 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("Callable");
        return 1234;
    }
}
Threadfor(int i=0; i<5; i++) {
    new Thread(()->{
        System.out.println("Thread");
    }, String.valueOf(i)).start();
}
默认线程池//step1.创建
ExecutorService threadPool = Executors.newFixedThreadPool(5);//5个
ExecutorService threadPool = Executors.newSingleThreadExecutor();//1个
ExecutorService threadPool = Executors.newCacheThreadPool();//可扩展
//step2.执行
threadPool.execute(()->{  System.out.println(Thread.currentThread().getName());
});
//step3.销毁
threadPool.shutdown();
自定义线程池ExecutorService executorService = new ThreadPoolExecutor(
    corePoolSize,//核心线程数
    maximumPoolSize,//最大线程数
    keepAliveTime,//多余的空闲线程存活时间
    TimeUnit.SECONDS,//keepAliveTime的单位
    new LinkedBlockingQueue<>(3),//任务队列,SynchronousBlockingQueue
    Executors.defaultThreadFactory(),//线程工厂,生成工作线程
    new ThreadPoolExecutor.AbortPolicy());//拒绝策略

二、相关细节

1、自定义线程池的相关参数

 1.1 生产环境中如何配置corePoolSize和maximumPoolSize?

CPU密集型

(需要大量的运算,而没有阻塞,CPU一直全速运行)

CPU核数 + 1个线程数

IO密集型

(需要大量的IO操作,即大量的阻塞)

CPU核数 / (1 - 阻塞系数) = CPU核数*5 或 CPU核数*10

阻塞系数在0.8 ~ 0.9左右

 1.2 四种拒绝策略

当队列满了+工作线程大于最大线程数,执行拒绝策略,有4种:

AbortPolicy默认,直接抛出RejectedExcutionException异常,阻止系统正常运行
DiscardPolicy直接丢弃任务,不予任何处理也不抛出异常,如果运行任务丢失,这是一种好方案
CallerRunsPolicy该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者
DiscardOldestPolicy抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务

2、为何使用线程池?使用线程池的好处?为何不用默认线程池?

2.1 为何用?(线程复用 / 控制最大并发数 / 管理线程)

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

2.2 用的好处?(降低资源消耗/提高响应速度/提高线程的可管理性)

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

2.3 线程池的底层原理?
 

  1. 在创建了线程池后,等待提交过来的任务请求

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

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

    2. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列

    3. 如果这时候队列满了,并且正在运行的线程数量还小于maximumPoolSize,那么还是创建非核心线程like运行这个任务;

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

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

  4. 当一个线程无事可做操作一定的时间(keepAliveTime)时,线程池会判断:

    1. 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉

    2. 所以线程池的所有任务完成后,它会最终收缩到corePoolSize的大小

以顾客去银行办理业务为例

  1. 最开始假设来了两个顾客,因为corePoolSize为2,因此这两个顾客直接能够去窗口办理

  2. 后面又来了三个顾客,因为corePool已经被顾客占用了,因此只有去候客区,也就是阻塞队列中等待

  3. 后面的人又陆陆续续来了,候客区可能不够用了,因此需要申请增加处理请求的窗口,这里的窗口指的是线程池中的线程数,以此来解决线程不够用的问题

  4. 假设受理窗口已经达到最大数,并且请求数还是不断递增,此时候客区和线程池都已经满了,为了防止大量请求冲垮线程池,已经需要开启拒绝策略

  5. 临时增加的线程会因为超过了最大存活时间,就会销毁,最后从最大数削减到核心数

三、线程不安全

1、线程不安全的ArrayList(Vector, Collections.synchronized(), JUC方法)

1.1 线程不安全的原因

因为在进行写操作的时候,方法上为了保证并发性,是没有添加synchronized修饰,所以并发写的时候,就会出现问题。源码如下:

1.2 解决办法

(1)Vector

Java Vector 类 | 菜鸟教程

Vector v = new Vector(3, 2);//初始3个,每次加2个
v.capacity());//当前容量
v.addElement(new Integer(1));//插入元素

(2)Collections.synchronized()

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

(3)采用JUC里面的方法(COW)

CopyOnWriteArrayList:写时复制,主要是一种读写分离的思想。

写时复制,CopyOnWrite容器即写时复制的容器,往一个容器中添加元素的时候,不直接往当前容器Object[]添加,而是先将Object[]进行copy,复制出一个新的容器object[] newElements,然后新的容器Object[] newElements里添加原始,添加元素完后,在将原容器的引用指向新的容器 setArray(newElements);这样做的好处是可以对copyOnWrite容器进行并发的度,而不需要加锁,因为当前容器不需要添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。就是写的时候,把ArrayList扩容一个出来,然后把值填写上去,在通知其他的线程,ArrayList的引用指向扩容后的。

public boolean add(E e) {
    //step1.加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        //step2.在末尾扩容一个单位
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //step3.把扩容后的空间,填写上需要add的内容
        newElements[len] = e;
        //step4.把内容set到Array中
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}


2、线程不安全的HashSet

2.1 线程不安全的原因

hashset在执行add方法的时候,存储一个值进入map中,只是作为key进行存储,而value存储的是一个Object类型的常量,也就是说HashSet只关心key,而不关心value

2.2 解决办法

Set<String> mySet = new CopyOnWriteArraySet<String>();
mySet.add("hiha");


3、线程不安全的HashMap

3.1 线程不安全的原因

3.2 解决办法

(1)使用Collections.synchronizedMap(new HashMap<>());

Map<Long, String> list = Collections.synchronizedMap(new HashMap<Long, String>());

(2)使用 ConcurrentHashMap

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

四、核心线程数、最大线程数、阻塞队列长度的计算

线程执行优先级: 核心线程 > 阻塞队列 > 扩容线程 > 拒绝策略(注意 先阻塞队列,后扩容线程)
线程池的底层实现,基本都是ThreadPoolExecutor,可用jvisualvm工具来预估这两个时间

1、核心线程数

【常规-核心线程数的计算】

CPU密集型: N+1
I/O密集型: 2N+1N/(1-阻尼系数)
其中,N+1的1是为了应对线程执行过程发生缺页中断,或其它异常导致线程阻塞请求。


【max-核心线程数的计算】

视频教程 https://www.bilibili.com/video/BV1PL411w7oQ
【每天】8个外卖员,8辆车,早8晚8送外卖。但是外卖员得吃饭和休息,中午和晚上分别吃饭1h,休息1小时。如何在歇人不歇车,需要请多少外卖员?
答案:(总时间*车数量)/工作时间=(12*8)/8=12人
外卖员对应多线程,车对应cpu, 工作时间=8小时,休息时间=因此极限线程数量= (线程平均工作时间+线程平均等待时间)*cpu数/线程平均工作时间
但是不能一个接口任务把cpu全耗尽,cpu还需要处理监控和日志程序。需要乘以cpu利用率(70%~80%)。因此,
最大核心线程数 = (线程平均工作时间+线程平均等待时间)*cpu数/线程平均工作时间 *  cpu利用率
其中,工作时间 = cpu计算的耗时, 等待时间 = IO请求时间(mysql,文件,调三方接口)
(1)如果接口压力比较均衡,就用以上的计算公式
(2)如果接口压力波动较大,则在(1)基础上下调整


【实际-核心线程数的计算】

public void test() {//cpu是4核
    createActivity();//IO,30ms
    calculateLogic();//cpu,10ms
}
【1次请求时间(40ms)】: 可以开4个线程(自己执行10ms, 另外30ms的空闲时间可以再开3个线程执行),4核=4*4=16个线程,*(70%~80%)=12个
【1s时间】: 可以处理(1000ms/40ms) * 12 = 300个请求。
如果每秒200个请求,那么核心线程 = 8
1s 12个线程=300个请求
1s  ?个线程=200个请求


2、最大线程数(我自己总结的)

若比较平稳,则在核心线程数基础上,稍加一些
若波动较大,则=max核心线程数


3、阻塞队列

单位时间内,队列长度=等待时间/处理时间*cpu数
【1次请求时间(40ms)】: 30ms/20ms*4=6
【1s时间】: (1000ms/40ms) * 6  = 150