Document

线程池

在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。

线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。

线程池的优势:

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等待,等其它线程执行完毕,再从队列中取出任务来执行。

主要特点:线程复用、控制最大并发数、管理线程。

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

2.提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

3.提高线程的课管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

Java中的线程池是通过Executor框架来实现的,而我们创建时,一般使用它的子类:ThreadPoolExecutor。

 

java中提供了一个静态工厂方法来创建不同的线程池:Executors

通过静态方法创建出的线程都实现了ExecutorService接口

常用的方法包括:

FixedThreadPool(int threads):定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程

SingleThreadPool():只有一条线程来执行任务,适用于有顺序的任务的应用场景。

CachedThreadPool():可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。

SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。

WorkStealingPool:java8新增,使用目前机器上可用的处理器作为它的并行级别。

常用的线程池创建方法示例如下:

/**
 * 第四种获取线程的方式-线程池
 */
public class ThreadPoolDemo {

    public static void main(String[] args) {
        ExecutorService threadPool=Executors.newFixedThreadPool(5);//一池5个线程
        //ExecutorService threadPool=Executors.newSingleThreadExecutor();//一池一个线程
        //ExecutorService threadPool=Executors.newCachedThreadPool();//一池N线程,根据情况来创建

        try{
            //模拟十个用户来办理业务
            for(int i=1;i<=10;i++){
                //submit有返回值
                Future future=threadPool.submit( () -> {
                    System.out.println(Thread.currentThread().getName()+" 办理业务");
                    return "办理业务成功";
                });
                System.out.println("获取结果"+future.get());
                //execute无返回值
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" 办理业务");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

线程池的底层就是ThreadPoolExecutor类。

1.Executors.FixedThreadPool(int),执行长期的任务,性能好很多

 主要特点如下:

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

2).newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue。

2.Executors.SingleThreadPool( ),一个任务一个任务执行的场景

 主要特点如下:

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

2).newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue。

3.Executors.CachedThreadPool( ),适用:执行很多短期异步的小程序或者负载较轻的服务器。

 主要特点如下:

1).创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

2).newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。

 七大参数

1.corePoolSize:线程池中的常驻核心线程数

在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程;

当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。

2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1

3.keepAliveTime:多余的空闲线程的存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。

默认情况下:只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize。

4.unit:keepAliveTime的单位

5.workQueue:任务队列,被提交单尚未被执行的任务。

6.threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。

7.handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略。

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

JDK内置的拒绝策略

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

2)CallerRunsPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。

3)DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。

4)DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。

 以上内置拒绝策略均实现了RejectedExecutionHandler接口

线程池的底层工作原理

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

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

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

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

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

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

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

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

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

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

 实际开发中一般手动写线程池,这样合理利用线程资源,避免资源耗尽的。

 手写线程池示例:

public class MyThreadPoolDemo {

    public static void main(String[] args) {
        //自定义线程池
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                1L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(5),//自定义阻塞队列长度为5
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()//自定义拒绝策略,默认为AbortPolicy,CallerRunsPolicy,DiscardOldestPolicy,DiscardPolicy
        );

        try {
            //模拟十个用户来办理业务,每个用户都是一个来自外部的请求线程
            for (int i = 1; i <= 10; i++) {
                //无返回值
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //关闭线程池
            threadPool.shutdown();
        }
    }
}

使用默认的拒绝策略,能够访问的上限数=当最大线程数(maximumPoolSize) + 任务队列长度,当超过就会报java.util.concurrent.RejectedExecutionException异常。

死锁编码及定位分析

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去。如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

死锁示例:

/**
 * 死锁是指两个或者两个以上的进程在执行过程中,
 * 因争夺资源而造成的一种相互等待的现象,
 * 若无外力干涉,那它们就无法推进下去,从而造成死锁现象。
 */
public class DeadLockDemo {

    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";

        new Thread(new DeadLockResource(lockA, lockB), "ThreadA").start();
        new Thread(new DeadLockResource(lockB, lockA), "ThreadB").start();
    }
}

/**
 * 线程操作资源类
 */
class DeadLockResource implements Runnable {

    private String lockA;
    private String lockB;

    public DeadLockResource(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + "\t 拥有" + lockA + ",试图获取" + lockB);
            System.out.println("----------*****************----------");
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "\t 拥有" + lockB + ",试图获取" + lockA);
            }
        }
    }
}

ThreadA持有lockA,试图获取lockB,而ThreadB持有lockB,试图获取lockA。造成两个线程相互等待,又没有外力干涉,无法推进下去,最终导致死锁现象。

产生死锁的主要原因

1.系统资源不足

2.进程运行推进的顺序不合适

3.资源分配不当

死锁解决

Linux环境下查看进程:ps -ef | grep java

Windows环境下查看java进程:jps  -l

Windows环境下查看死锁栈信息:jstack +进程号

1.jps命令定位进程号

2.jstack找到死锁查看

 

 只有停下程序,找到对应业务代码进行修改。

合理配置线程池:

首先得知道服务器cpu核心数

获取cpu核心数

Runtime.getRuntime().availableProcessors()

业务分为cpu密集型还是IO密集型,根据实际业务来配置

1.CPU密集型

CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。

CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那些。

CPU密集型任务配置尽可能少的线程数量:

一般公式:CPU核数+1个线程的线程池

2.IO密集型

1) 由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2

2) IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集型时,大部分线程都阻塞,故需要多配置线程数:

参考公式:CPU核数/1-阻塞系数(阻塞系数在0.8~0.9之间),比如8核CPU:8/1-0.9=90个线程数

 

 

posted @ 2020-09-08 21:41  马宝云  阅读(472)  评论(0编辑  收藏  举报