你要的JAVA多线程在这

java中为了提高并发度,可以使用多线程共同执行,但是如果有大量线程短时间之内被创建和销毁,会占用大量的系统时间,影响系统效率。

为了解决上面的问题,java中引入了线程池,可以使创建好的线程在指定的时间内由系统统一管理,而不是在执行时创建,执行后就销毁,从而避免了频繁创建、销毁线程带来的系统开销。

Java线程池的四种创建方式
Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。

newCachedThreadPool
这种线程池内部没有核心线程,线程的数量是没有限制的。
在创建任务时,若有空闲的线程时则复用空闲的线程,若没有则新建线程。
没有工作的线程(闲置状态)在超过了60S还不做事,就会销毁。

newFixedThreadPool
该线程池的最大线程数等于核心线程数,所以在默认情况下,该线程池的线程不会因为闲置状态超时而被销毁。
如果当前线程数小于核心线程数,并且也有闲置线程的时候提交了任务,这时也不会去复用之前的闲置线程,会创建新的线程去执行任务。如果当前执行任务数大于了核心线程数,大于的部分就会进入队列等待。等着有闲置的线程来执行这个任务。

newScheduledThreadPool
DEFAULT_KEEPALIVE_MILLIS就是默认10L,这里就是10秒。这个线程池有点像是把CachedThreadPool和FixedThreadPool 结合了一下。

这个线程池是上述4个中为唯一个有延迟执行和周期执行任务的线程池。

newSingleThreadExecutor
有且仅有一个工作线程执行任务
所有任务按照指定顺序执行,即遵循队列的入队出队规则

ThreadPoolTaskExecutor线程池(默认newFixedThreadPool )

1.corePoolSize(线程池的基本大小/核心线程数量)

当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
核心线程会一直存活,即使没有任务需要执行

2、queueCapacity:任务队列容量

当核心线程数达到最大时,新任务会放在队列中排队等待执行

3、、maxPoolSize:最大线程数

当线程数>corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务,直到线程数量达到maxPoolSize
当线程数已经=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常,进入配置拒绝策略

4、、keepAliveTime:线程空闲时间

当线程空闲时间达到keepAliveTime时,线程会被销毁,直到线程数量=corePoolSize
如果allowCoreThreadTimeout=true,则会直到线程数量=0(这个特性需要注意),默认的是false

5、rejectedExecutionHandler:任务拒绝处理器(用户可以自定义拒绝后的处理方式)

当线程数已经达到maxPoolSize,且任务队列已满时,会拒绝新任务,进入到配置的拒绝策略

threadPoolExecutor线程池的拒绝策略(默认的拒绝策略是:AbortPolicy):
1: AbortPolicy 丢弃任务,抛运行时异常
2:CallerRunsPolicy 执行任务(这个策略重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功);使用调用者所在线程来运行任务。
3:DiscardPolicy 对拒绝任务直接无声抛弃,没有异常信息
4:DiscardOldestPolicy 对拒绝任务不抛弃,而是抛弃队列里面等待最久的(队列头部的任务将被删除)一个线程,然后把拒绝任务加到队列尾部(Queue是先进先出的任务调度算法)
5:实现RejectedExecutionHandler接口,可自定义拒绝策略处理器

简要概括如下:
当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满,进入拒绝策略

代码实例如下

package com.joysfintech.config;

import com.joysfintech.common.constant.WarnConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.concurrent.ThreadPoolExecutor;

@Component
@Slf4j
public class ThreadPooConfig {

    ThreadPoolTaskExecutor poolTaskExecutor = new ThreadPoolTaskExecutor();

    {
        log.debug("初始化线程池开始");
        int queueCount = 10000;
        poolTaskExecutor.setQueueCapacity(queueCount);
        log.info("当前线程池配置的缓存对列数量queue:{}", queueCount);
        //获取CPU核数
        int availableProcessors = Runtime.getRuntime().availableProcessors();
        log.info("当前服务器CUP核数availableProcessors:{}", availableProcessors);
        //线程池维护线程的最小数量
        int corePoolSize = availableProcessors + 1;
        poolTaskExecutor.setCorePoolSize(corePoolSize);
        log.info("当前线程池配置的核心线程数量corePoolSize:{}", corePoolSize);
        //线程池维护线程的最大数量
        int maxPoolSize = availableProcessors * 2;
        poolTaskExecutor.setMaxPoolSize(maxPoolSize);
        log.info("当前线程池配置的最大线程数量maxPoolSize:{}", maxPoolSize);
        //空闲线程的存活时间
        poolTaskExecutor.setKeepAliveSeconds(5000);
        poolTaskExecutor.initialize();
        //如果子线程缓存队列达到上限,就会使用当前线程去执行
        poolTaskExecutor.getThreadPoolExecutor().setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        log.debug("初始化线程池结束");
    }

    public void execute(Runnable task) {
        try {
            poolTaskExecutor.execute(task);
            log.debug("当前活动线程数:{}", poolTaskExecutor.getActiveCount());
            log.debug("核心线程数:{}", poolTaskExecutor.getCorePoolSize());
            log.debug("总线程数:{}", poolTaskExecutor.getPoolSize());
            log.debug("最大线程池数量:{}", poolTaskExecutor.getMaxPoolSize());
            log.debug("线程处理队列长度:{}", poolTaskExecutor.getThreadPoolExecutor().getQueue().size());
        } catch (Exception e) {
            log.error("{}线程池,执行线程出现异常task:{}", WarnConstant.CORE_CRITICAL_ERROR, task, e);
            exceptionHandler(task);
        }
    }

    private void exceptionHandler(Runnable task) {
        try {
            Class<?> clazz = task.getClass();
            for (; clazz != Object.class; clazz = clazz.getSuperclass()) {
                Field[] fields = clazz.getDeclaredFields();
                for (Field field : fields) {
                    field.setAccessible(true);
                    String fieldName = field.getName();
                    Object fieldValue = field.get(task);
                    log.error("线程池,调用线程池出现异常,调用方信息(方便排查问题),fieldName:{},fieldValue:{},task:{}", fieldName, fieldValue, task);
                }
            }
        } catch (Exception e) {
            log.error("线程池,处理异常方法报错,task:{}", task, e);
        }
    }

}

在这里插入图片描述

合理设置线程数
如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1;CPU密集型业务(搜索、排序等)CPU空闲时间较少,线程数不能设置太多。

如果是IO密集型任务,参考值可以设置为2*NCPU

创建线程的有哪些方式

1)继承Thread类创建线程类

2)通过Runnable接口创建线程类

Java线程具有五中基本状态

1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。

5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程并发编程要素
1)原子性

原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

2)可见性

可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。(volatile)

可见性
可见性与Java的内存模型有关,模型采用缓存与主存的方式对变量进行操作,也就是说,每个线程都有自己的缓存空间,对变量的操作都是在缓存中进行的,之后再将修改后的值返回到主存中,这就带来了问题,有可能一个线程在将共享变量修改后,还没有来的及将缓存中的变量返回给主存中,另外一个线程就对共享变量进行修改,那么这个线程拿到的值是主存中未被修改的值,这就是可见性的问题。

volatile很好的保证了变量的可见性,变量经过volatile修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,加了这个指令后,会引发两件事情:

将当前处理器缓存行的数据写回到系统内存
这个写回内存的操作会使得在其他处理器缓存了该内存地址无效
意思就是说当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去主存中读取新值,这就保证了可见性

原子性
问题来了,既然它可以保证修改的值立即能更新到主存,其他线程也会捕捉到被修改后的值,那么为什么不能保证原子性呢?
首先需要了解的是,Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。

所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。

举个栗子

一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。
线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。
问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。

volatile的适用场景
状态标志
也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
volatile boolean shutdownRequested;

public void shutdown() {
shutdownRequested = true;
}

public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}

线程1执行doWork()的过程中,可能有另外的线程2调用了shutdown,所以boolean变量必须是volatile。

.多线程同步常用有哪几种方法?

Synchronized关键字
分布式锁(如redis)

为什么Redis是单线程的
官方答案:
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。

不需要各种锁的性能消耗

Redis的数据结构并不全是简单的Key-Value,还有list,hash等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除

一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。

总之,在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值