线程、多线程和线程池

 

       1 创建多线程

1.1 继承Thread 类

1.2 实现Runnable 接口

2 Executor框架

2.1 线程池的优点

2.2 Executors 提供四种线程池

2.3 自定义线程池ThreadPoolExecutor

2.4 线程池大小


在文章 线程安全和锁的分类 中有讲过线程相关的基础概念,本文不再赘述。

                     

由线程的生命周期我们知道,启动一个线程是用start() 方法。

前言:进程和线程的关系

一个进程,代表计算机中实际跑起来的一个程序,每个进程都有自己独立的进程地址空间和上下文堆栈。但是就一个程序本身而言,是不执行进程代码的,它只是提供了一个大环境容器,在进程中实际执行体是“线程”,因为一个进程至少得有一个线程,就是“主线程”。 也就是说,一个进程至少要有一个主线程。

       默认情况下,一个线程的栈要预留 1M 的内存空间,而一个进程中可用的内存空间只有 2G,所以理论上一个进程最多可以开 2048 个进程,但是内存当然不可能完全拿来做线程的栈,所以实际数目要比这个值小很多。

        Springboot 内嵌的 tomcat 默认最大线程是 200。 同一台机器,能起多少个线程也是受到内存限制的。

 

 

1 创建多线程

创建线程有四种方法:

1、继承Thread 类,扩展线程。

2、实现Runnable接口。  

3、ExecutorService 实现线程池

首先我们需要明确的是:Thread 也是实现了Runnable 接口。

1.1 继承Thread 类

public class DemoThread  extends Thread{
    @Override
    public void run() {
       super.run();
        System.out.println(Thread.currentThread());

    }

    public static void main(String[] args) {
        DemoThread t = new DemoThread();
        t.start();
    }

}

1.2 实现Runnable 接口

public class DemoThread extends OtherClass implements Runnable{
    @Override
    public void run() {
        for (int i=0; i<100; i++) {
            System.out.println("实现Runnable接口" + i + Thread.currentThread());
        }


    }

    public static void main(String[] args) {
        Thread t = new Thread(new DemoThread());
        t.start();

    }

}

问题:用 Runnable 还是 Thread ?

1、Thread类本身也是实现了Runnable接口,因此也是实现了Runnable接口中的run方法。

2、当使用继承Thread类去实现线程时,我们需要重写run方法,因为Thread类中的run方法本身什么事情都不干。

3、当使用实现Runnable接口去实现线程时,我们需要重写run方法,然后使用new Thread(Runnable)这种方式来生成线程对象,这个时候线程对象中的run方法才会去执行我们自己实现的Runnable接口中的run方法。

4、这两者的区别,主要是Java类和接口的区别,Java不支持类的多重继承,但允许你调用多个接口。所以,如果这个线程对象还要继承其他的类,当然是调用Runnable接口比较好。

 

2 Executor框架

Executor框架在Java 5中被引入,Executor 框架是一个根据一组执行策略调用、调度、执行和控制的异步任务的框架。

来看一下Executor的基本组成:

                     

Java里面线程池的顶级接口是 Executor,不过真正的线程池接口是 EecutorService, ExecutorService 的默认实现是 ThreadPoolExecutor;普通类 Executors 里面调用的就是 ThreadPoolExecutor。

那么在现实应用中,我们可以通过下图来理解 应用和线程池的交互以及线程池内部工作过程:

                      

工作队列负责存储用户提交的各个任务。内部的线程池需要在运行过程中管理线程的创建和销毁。

阻塞性:
BlockQueue存入任务队列时是没有阻塞,使用的是offer,无阻塞添加方法。
BlockQueue取出任务队列时是有阻塞,有超时使用poll取值,无超时使用take阻塞方法取值

添加任务逻辑:
1.当任务数小于核心线程数,新建核心线程来执行任务
2.任务数大于核心线程数,队列不满,放入任务队列
3.任务数大于核心线程数,队列已满,新建线程执行
4.任务数大于核心线程数,队列已满,工作线程已达最大线程数,拒绝任务,抛出异常(而不是阻塞任务,等待进入队列)

 

2.1 线程池的优点

无限制的创建线程会引起应用程序内存溢出,所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用 Executor 框架可以非常方便的创建一个线程池。

线程池的优点:

  1. 避免线程的创建和销毁带来的性能开销。
  2. 避免大量的线程间因互相抢占系统资源导致的阻塞现象。
  3. 能够对线程进行简单的管理并提供定时执行、间隔执行等功能。

2.2 Executors 提供四种线程池

  1. newCachedThreadPool 是一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute() 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。注意,可以使用 ThreadPoolExecutor 构造方法创建具有类似属性但细节不同(例如超时参数)的线程池。适用场景:快速处理大量耗时较短的任务,如Netty的NIO接受请求时,可使用CachedThreadPool。

  2. newSingleThreadExecutor 创建是一个单线程池,也就是该线程池只有一个线程在工作,所有的任务是串行执行的,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。适用场景:快速处理大量耗时较短的任务,如Netty的NIO接受请求时,可使用CachedThreadPool。

  3. newFixedThreadPool 创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小,线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

  4. newScheduledThreadPool 创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。

 

如果我想创建5个线程池,可以  ExecutorService executorService = Executors.newFixedThreadPool(5);

2.3 自定义线程池ThreadPoolExecutor

但是,不允许用Executors 去创建线程池,而是通过ThreadPoolExecutor 的方式。这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

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

(1)FixedThreadPool 和 SingleTreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM

(2)CachedThreadPool:允许的创建线程数为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM

 

 

那么我们来看一下,ThreadPoolExecutor 的构造函数:

   public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }

相关参数解释:

  1. corePoolSize:线程池的核心线程数,一般情况下不管有没有任务都会一直在线程池中一直存活,只有在 ThreadPoolExecutor 中的方法 allowCoreThreadTimeOut(boolean value) 设置为 true 时,闲置的核心线程会存在超时机制,如果在指定时间没有新任务来时,核心线程也会被终止,而这个时间间隔由第3个属性 keepAliveTime 指定。

  2. maximumPoolSize:线程池所能容纳的最大线程数,当活动的线程数达到这个值后,后续的新任务将会被阻塞。

  3. keepAliveTime:控制线程闲置时的超时时长,超过则终止该线程。一般情况下用于非核心线程,只有在 ThreadPoolExecutor 中的方法 allowCoreThreadTimeOut(boolean value) 设置为 true时,也作用于核心线程。

  4. unit:用于指定 keepAliveTime 参数的时间单位,TimeUnit 是个 enum 枚举类型,常用的有:TimeUnit.HOURS(小时)、TimeUnit.MINUTES(分钟)、TimeUnit.SECONDS(秒) 和 TimeUnit.MILLISECONDS(毫秒)等。

  5. workQueue:线程池的任务队列,通过线程池的 execute(Runnable command) 方法会将任务 Runnable 存储在队列中。

  6. threadFactory:线程工厂,它是一个接口,用来为线程池创建新线程的。

示例代码如下,浅显易懂。

public class DemoThread{

    static class Task implements Runnable {
        private String name;

        public Task(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " is running " + this.toString());
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public String getName() {
            return name;
        }

        @Override
        public String toString() {
            return "Task [name=" + name + "]";
        }
    }


    public static void main(String[] args) throws InterruptedException, IOException {

        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("demo-pool-%d").build();

        ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingDeque<Runnable>(1024), namedThreadFactory,
                new ThreadPoolExecutor.AbortPolicy());

        //创建任务
       for (int i = 1; i <= 10; i++) {
            Task task = new Task(String.valueOf(i));
            pool.execute(task);
        }


        //pool.execute(() -> System.out.println(Thread.currentThread().getName()));
        pool.shutdown(); 
    }

结果输出:

demo-pool-0 is running Task [name=1]
demo-pool-1 is running Task [name=2]
demo-pool-2 is running Task [name=3]
demo-pool-3 is running Task [name=4]
demo-pool-4 is running Task [name=5]
demo-pool-0 is running Task [name=6]
demo-pool-2 is running Task [name=7]
demo-pool-1 is running Task [name=8]
demo-pool-3 is running Task [name=9]
demo-pool-4 is running Task [name=10]

Process finished with exit code 0

2.4 线程池大小

线程受什么限制? 首先我们想到的是CPU,CPU调度的是线程,而且线程上下文切换开销大,如果是计算型的任务的话,通常建议线程数为CPU的核数N 或者N+1

如果是较多等待型任务,例如I/O操作比较多的话,可以参照Brian Goetz 推荐的计算方法:

线程数 = CPU 核数  * 目标CPU 利用率 * (1 + 平均等待时间 / 平均工作时间)

注意,这并不是精准的计算,需要在实际工作中进行验证和调整。

实际上,除了CPU,还有其他资源的限制。



以上。?

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值