Java中线程池

线程池的定义及必要性

定义:线程池就是一个可以复用线程的技术。
不使用线程池的问题:
用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程处理啊,这样会严重影响系统的性能。

线程池的工作原理

线程池的工作原理:
在这里插入图片描述
在这里插入图片描述
线程池中有一块区域放一些固定数量的线程(工作线程WorkThread),还有一块区域用来放置用户请求的队列(任务队列WorkQueue)。
另外,任务是需要实现Runnable或者Callable接口才能放到任务队列中的。

线程池的创建

创建线程池
JDK5.0起提供了代表线程池的接口:ExecutorService
如何得到线程池对象?
方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
在这里插入图片描述
方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。
在这里插入图片描述
参数补充说明:
●线程池的核心线程数量也就是它固定的这些线程的数量是可以长久复用的;
●最大线程数量一般是要超过核心线程数量的,加入最大线程数量配的是5,如果核心线程数量是3的情况下,这就意味着它到时候可以创建出2个所谓的临时线程;
●临时线程的存活时间指的是当临时线程创建出来了,如果临时线程空出来多长时间没有任务可干的情况下就把这个临时线程给干掉;
●声明的存活时间还要为其声明一个单位,如秒 分 时 天;
●任务队列就是用来缓存线程池的任务队列;
●线程工厂对象负责为线程池创建线程,包括核心线程或者临时线程;
●假如线程池中的核心线程和临时线程都在忙,同时任务队列也满了,还有新任务来应该如何处理,这就是拒绝策略。

上述的过程中也可以拿生活中的案例来举例说明:假如我现在开了一个餐厅,不可能来一个客人就招一个服务员,万一来了1000个客人就要招1000个服务员,很显然资源极大浪费。所以我选择招聘一些正式工为客人服务,太忙再招一些临时工,这其实也是线程池模型。那么核心线程指的就是正式工,比如有3个;线程池最大数量指的的最大员工数,比如有5个,那么临时工就是2个;临时线程池的存活时间就指临时工空闲多久被开除;任务队列就是客人排队的地方;线程工厂就是负责招聘员工的HR;任务拒绝策略就是忙不过来咋办。

// 1、通过ThreadPoolExecutor创建一个线程池对象。
/*
public ThreadPoolExecutor(int corePoolSize,
                      int maximumPoolSize,
                      long keepAliveTime,
                      TimeUnit unit,
                      BlockingQueue<Runnable> workQueue,
                      ThreadFactory threadFactory,
                      RejectedExecutionHandler handler) {
 */
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());

补充说明:LinkedBlockingQueue基于链表实现,不限制大小,也就是说可以无限地往这个队列里面加任务。ArrayBlockingQueue基于数组实现,可以控制大小。

使用线程池创建Runnable任务

ExecutorService的常用方法
在这里插入图片描述
新任务拒绝策略
在这里插入图片描述
代码示例
MyRunnable.java

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        // 任务是干啥的?
        System.out.println(Thread.currentThread().getName() + " ==> 输出666~~");
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ThreadPool.java

public class ThreadPoolTest1 {
    public static void main(String[] args) {
        // 1、通过ThreadPoolExecutor创建一个线程池对象。
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());

        Runnable target = new MyRunnable();
        pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
        pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
        pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        // 到了临时线程的创建时机了
        pool.execute(target);
        pool.execute(target);
        // 到了新任务的拒绝时机了!
        pool.execute(target);

        // pool.shutdown(); // 等着线程池的任务全部执行完毕后,再关闭线程池
        // pool.shutdownNow(); // 立即关闭线程池!不管任务是否执行完毕!
    }
}

补充说明:

  1. 当核心线程满载且任务队列都满了的情况下才会启用临时线程;若是临时线程也使用完了这时候启用任务拒绝策略,即创建方法的第7个参数;
  2. 一般情况下因为线程池需要一直使用,所以不关闭,线程执行完也不会自动关闭,即使关闭也是等到线程全部执行完毕再关闭,否则程序会抛异常;
  3. Thread.sleep(Integer.MAX_VALUE)线程睡眠,一直抓着当前线程不放;
  4. .第4种拒绝策略说白了是让main主线程上来执行相应的任务。

使用线程池处理Callable任务

与Runnable最大的区别就是处理Callable任务的时候可以返回结果。
在这里插入图片描述
相应示例代码如下:
MyCallable.java

/**
 * 1、让这个类实现Callable接口
 */
public class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }

    // 2、重写call方法
    @Override
    public String call() throws Exception {
        // 描述线程的任务,返回线程执行返回后的结果。
        // 需求:求1-n的和返回。
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName() + "求出了1-" + n + "的和是:" + sum;
    }
}

ThreadPoolTest2.java

public class ThreadPoolTest2 {
    public static void main(String[] args) throws Exception {
        // 1、通过ThreadPoolExecutor创建一个线程池对象。
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());

        // 2、使用线程处理Callable任务。多态写法,返回Future对象是FutureTask父类
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));
        Future<String> f4 = pool.submit(new MyCallable(400));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
    }
}

运行结果如下所示:

pool-1-thread-1求出了1-100的和是:5050
pool-1-thread-2求出了1-200的和是:20100
pool-1-thread-3求出了1-300的和是:45150
pool-1-thread-3求出了1-400的和是:80200

最后一个任务的执行分配的线程是随机的,前面3个谁执行快先结束就接管第4个。

使用Executors得到线程池
Executors是一个线程池的工具类,提供了很多的静态方法用于返回不同特点的线程池对象。
在这里插入图片描述
注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。
在这里插入图片描述
在这里插入图片描述
补充:
第2个newSingleThreadExecutor类与自己创建一个线程和只有一个线程的线程池是完全不一样的。自己的创建的一个线程,如果出现了异常可能就挂掉了。但这里的方法,如果线程出现异常会补充一个新线程,这个线程相当于不死战神永远会立起来。
第4个newScheduledThreadPool是创建一个固定核心线程数量的线程池。这个有一些特殊的用途,可以用来做一些周期调度或者定时调度等。

示例代码如下:

public class ThreadPoolTest3 {
    public static void main(String[] args) throws Exception {
        // 1、通过ThreadPoolExecutor创建一个线程池对象。
//        ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
//                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
//                new ThreadPoolExecutor.CallerRunsPolicy());

        // 1-2 通过Executors创建一个线程池对象。
        ExecutorService pool = Executors.newFixedThreadPool(17);
        // 老师:核心线程数量到底配置多少呢???
        // 计算密集型的任务:核心线程数量 = CPU的核数 + 1
        // IO密集型的任务:核心线程数量 = CPU核数 * 2

        // 2、使用线程处理Callable任务。
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));
        Future<String> f4 = pool.submit(new MyCallable(400));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
    }
}

线程池的配置问题

问题:核心线程到底该如何配置呢?
参考答案:根据行业经验之谈,如果当前的线程任务是一个计算密集型的任务,核心线程数量应该等于CPU的核数+1;如果当前的线程任务是IO密集型的任务,核心线程数量应该等于CPU的核数*2。所谓的计算密集型任务就是指CPU基本上在做一些计算,如上述案例中的求和,所谓的IO密集型任务就是任务里要读取一些文件数据或者要进行一些通信,读文件数据数据相对比较慢。CPU的核数查看可通过Ctrl+Alt+Esc,在性能的菜单栏中查看。

注意:大型并发环境中使用Executors如果不注意可能出现系统风险。
对于一些大型项目,同时可能会有成百上千万的人一起来访问系统,比如京东淘宝这样的系统,这就是大型并发系统。如果在大型的并发系统中使用Executors的这些静态方法来得到线程池对象处理问题的话可能会有严重风险。看阿里巴巴开发参考手册:
在这里插入图片描述
OOM内存溢出异常,也就是用户可能会提交无穷无尽的任务,堆积过多的任务而导致任务把内存占爆了,导致内存溢出,系统瘫痪。Integer.MAX_VALUE表示系统最大值,也就是如果有一亿个用户过来,系统会马上创建以一个线程,每个线程需要占内存,可能会导致内存溢出。所以建议通过ThreadPoolExecutor自己控制线程数量。

进程 线程 并发 并行

并发和并行
进程:指的是正在运行的程序,可以理解为正在运行的软件
线程:本质是属于进程的,一个进程中可以同时运行很多个线程
并发:进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,我们的感觉这些线程在同时执行,这就是并发。(举例来说,电脑里有好几个程序运行了好多线程,CPU会把这个线程跑一跑再把下一个线程跑一跑,因为执行的速度足够快,感觉像是一起跑的一样;再比如给班级里所有人发书,每个人需要发五本书,先给每个人发一本再来下一轮,发得比较快感觉就像同时发一样;再像武侠片里一个人瞬间把所有人放倒一样,其他是放倒得比较快而已)
并行:在同一个时刻上,同时有多个线程在被CPU调度执行。

问:多线程到底是怎么在执行的?
答:并发和并行同时执行的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值