java多线程-线程池以及自定义线程池

1,043 阅读3分钟

线程池以及自定义线程池

为了放置资源被滥用,合理控制连接的数量,我们采用池化的操作。

数据库连接池:不能随便new连接

也不能随便new线程,保证线程的统一处理。

线程池

java通过Excutors提供四种线程池,分别为:

  • newCachedThreadPool
  • newFixedThreadPool
  • newScheduledThreadPool
  • newSingleThreadPool

线程池的核心参数

image-20210402205116310

根据给这些参数传入不同的值来确定我们想要什么样的线程池。

int corePoolSize, //核心线程数,无论忙不忙,池子里都会有这么多线程,只要new除这个池子,就会有这么多的线程
int maximumPoolSize,//最大线程数,当任务比核心线程数多时,就会new线程,但上限是maximumPoolSize
long keepAliveTime,//线程超过核心线程时,闲下来的时候过多久会回收
TimeUnit unit,//上面时间的单位
BlockingQueue<Runnable> workQueue,//阻塞队列(卡在那里等着你取),线程池无法处理的多余的任务放在这里排队
ThreadFactory threadFactory,//线程工厂,负责创建线程
RejectedExecutionHandler handler//拒绝策略
    

阻塞队列

放10个数字,取12个数字,此时程序不会结束,会等着人往里再送两个。

注意BlockingQueue是个接口,具体的实现是ArrayBlockingQueueLinkedBlockingQueue

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author SJ
 * @date 2021/4/2
 */
public class TestBlockingQueue {
    public static void main(String[] args) throws InterruptedException {
        //新建一个容量为5的阻塞队列
        BlockingQueue<Integer> blockingQueue=new ArrayBlockingQueue<>(3);

        //新建两个任务,一个负责放,一个负责取
        //放三个数字
        Runnable t1=()->{
        for (int i = 0; i < 3; i++) {
            System.out.println("放入"+i);
            try {
                blockingQueue.put(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        };

        //取5个数字
        Runnable t2=()->{
            for (int i = 0; i < 5; i++) {
                System.out.println("取出"+i);
                try {
                    blockingQueue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        };

        //创建一个线程池,把任务丢进去
        ExecutorService pools= Executors.newFixedThreadPool(2);
        pools.submit(t2);
        pools.submit(t1);

    }
}

image-20210402211214987

执行后程序并未终止,因为需要取5个数字,但队列中只有三个数字,任务还未完成,所以阻塞在这里等人放数字进去。

拒绝策略

如果使用阻塞队列使用ArrayBlockingQueue,那么任务排的队就是有序的。LinkedBlockingQueue可以做无界队列。

image-20210402211829748

当线程也满了,队列也满了,这时就要拒绝任务。

拒绝策略:

image-20210402220412288

有四种

AbortPolicy:抛异常

CallerRunsPolicy:先尝试运行再丢

DiscardOldestPolicy:丢弃队列里等待时间最长的那个

DiscardPolicy:直接丢

newFixedThreadPool

image-20210402212140038

核心线程数:nThreads

最大线程数:nThreads

核心线程数和最大线程数相同,就没有活跃时间这一说法了。活跃时间是指超出核心线程数的部分的活跃时间

单位:毫秒

等待队列是个LinkedBlockingQueue无界队列,所有任务全部接收,没有拒绝策略

newSingleThreadPool

image-20210402212817833

核心线程数:1

最大线程数:1

无界队列

newCachedThreadPool

image-20210402212934957

核心线程数:0

最大线程数:整型范围上届,相当于不限制最大线程数

活跃时间:60

单位:秒

等待队列:同步队列

同步队列

SynchronousQueue简单使用

经典的生产者-消费者模式,操作流程是这样的:

有多个生产者,可以并发生产产品,把产品置入队列中,如果队列满了,生产者就会阻塞;

有多个消费者,并发从队列中获取产品,如果队列空了,消费者就会阻塞

SynchronousQueue 也是一个队列来的,但它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先take后put,原理是一样的)。 put线程执行queue.put(1) 后就被阻塞了,只有take线程进行了消费,put线程才可以返回。可以认为这是一种线程与线程间一对一传递消息的模型。

实现原理

不像ArrayBlockingQueueLinkedBlockingQueque之类的阻塞队列依赖AQS实现并发操作,SynchronousQueue直接使用CAS实现线程的安全访问。

队列的实现策略通常分为公平模式和非公平模式。

**公平模式:**底层实现使用的是TransferQueue这个内部队列,它有一个head和tail指针,用于指向当前正在等待匹配的线程节点。

队尾匹配队头出队,先进先出,体现公平原则

**非公平模式:**底层的实现使用的是TransferStack,一个栈,实现中用head指针指向栈顶,

后进先匹配。

newScheduledThreadPool

image-20210402214306432

image-20210402214319153

核心线程数:corePoolSize

最大线程数:没有限制

超时时间:0

等待队列:延时队列

延时队列

该队列是定制的优先级队列,只能用来存储RunnableScheduledFutures任务。底层为堆结构

我们知道线程池运行时,会不断从任务队列中获取任务,然后执行任务。如果我们想实现延时或者定时执行任务,重要一点就是任务队列会根据任务延时时间的不同进行排序延时时间越短地就排在队列的前面,先被获取执行。

自定义线程池

手动创建线程池,效果会更好。

1.线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式。

Executors返回的线程池对象的弊端如下:

  • FixedThreadPool和SingleThreadPool

    允许请求队列长度为Integer.MaxValue, 可能会堆积大量的请求,从而导致内存溢出

  • CachedThreadPool:

    允许创建线程数量为Integer.MaxValue,可能会创建大量的线程,从而导致内存溢出

实例

下面我们来自己写一个线程池:

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author SJ
 * @date 2021/4/3
 */
public class MyExecutor {
    public static void main(String[] args) {

        //使用ThreadPoolExecutor这个类来构造线程池
        ThreadPoolExecutor MyThreadPool = new ThreadPoolExecutor(
                4,//核心线程数
                8,//最大线程数
                5,//活跃时间
                TimeUnit.SECONDS,//单位
                new ArrayBlockingQueue<>(300),//阻塞队列,最多300个排队
                new MyThreadFactory(new ThreadGroup("sunjie"),"goods-thread"),//线程工场
                new ThreadPoolExecutor.AbortPolicy()//拒绝策略采用抛异常
        );

    }

    //创建自己的线程工厂,不会写就源码拿过来改一改
    static class MyThreadFactory implements ThreadFactory{
        private static final AtomicInteger poolNumber = new AtomicInteger(0);
        private ThreadGroup group;
        private String namePrefix;

        //构造函数,传入组名和前缀
        public MyThreadFactory(ThreadGroup group, String namePrefix) {
            this.group = group;
            this.namePrefix = namePrefix;
        }

        @Override
        public Thread newThread(Runnable r) {
            //新建一个线程,指明组名和线程名
            Thread t = new Thread(group, r,
                    namePrefix +"-"+ poolNumber.getAndIncrement());
            //设为非守护线程
            if (t.isDaemon())
                t.setDaemon(false);
            //设定线程优先级为常规优先级
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }
}

使用

使用自定义线程池

import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author SJ
 * @date 2021/4/3
 */
public class TestMyExecutor {
    public static void main(String[] args) {
        //新建一个线程池
        MyExecutor myExecutor = new MyExecutor();
        //调用自己创建的线程池
        ThreadPoolExecutor myThreadPool = myExecutor.MyThreadPool;

        //新建任务,不停的给线程池丢任务
        while (true){

            myThreadPool.execute(()->{
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(123);
            });
        }

    }
}

再命令行输入 jconsole 选择自己定义的类查看当前活跃的线程

image-20210403103944613

此时一共有4个活跃的线程,因为任务只是输出,执行的非常快,所以4个核心线程就够用了。

如果我们把任务改成

   //新建任务,不停的给线程池丢任务
        while (true){

            myThreadPool.execute(()->{
                try {
                    Thread.sleep(10000);//线程运行10s
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(123);
            });
        }

image-20210403104626688

image-20210403104650408

控制台抛异常,因为排队的任务最多300个,while(true)会疯狂的丢任务进去,所以执行了拒绝策略。

此时4个核心线程不够用,就会new线程,最大线程数为8,此时有8个线程。