java线程和线程池

Posted 盖丽男

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java线程和线程池相关的知识,希望对你有一定的参考价值。

线程

首先明确一下,我们最经常听到的一句话,一个进程可以对应多个线程,一个线程只能属于一个进程。在JAVA里,JVM 中的线程与操作系统的线程是一对一的关系,所以在 JVM 中每创建一个线程就需要调用操作系统提供的 API 创建线程,赋予资源,并且销毁线程同样也需要系统调用。

为什么我们需要多线程呢?

就现在的操作系统来说,天然的就是多进程/线程处理任务,最常见的,我们的电脑,可以处理word的时候听音乐,看电视等等,并不是看起来同一时刻只能做一件事。
至于多线程/进程的原因

  • 随着技术发展,cpu的运行速度越来越高,而内存,硬盘的运行速度跟不上cpu的发展,所以为了提高cpu的使用效率,简单来说就是为了让cpu不闲着,所以将任务包装成进程/线程排队等待cpu处理,这个时候,需要多线程/进程。
  • 不仅是cpu运行速度,还有cpu的数量,双核甚至多核的cpu,可以并行处理任务,这种时候,也需要多进程/线程的存在。

总的来说,就是为了提高效率。

线程的生命周期/状态

这个地方可以直接用一张图来说明

要注意的是,java的线程状态,和通用的线程状态,并不是严格一一对应的。

开启一个新线程

java中,常用的开启新线程的方式有三种。

  • 继承Thread类
public class NewTread extends Thread{
    @Override
    public void run() {
        System.out.println("====== A new thread ======");
    }
}
  • 实现Runnable接口
public class NewTread implements Runnable{

    @Override
    public void run() {
        System.out.println("====== A new thread ======");
    }
}
  • 直接在函数体使用
	//一种
    public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("====== A new thread ======");
            }

        });
        thread.start();
        
        //另一种
        Thread thread1=new Thread(()->{
            System.out.println("====== A new thread ======");
        });
        
        thread1.start();;
    }

守护线程

守护线程用到的情况应该很少,

守护线程(Daemon)。这种线程的优先级很低,通常来说,当同一个应用程序里没有其他的线程运行的时候,守护线程才运行。当程序中唯一运行的的线程是守护线程时,并且守护线程执行结束后,JVM也就结束了这个程序。

因为这种特性,守护线程通常被用来作为同一程序中普通线程(用户线程)的服务提供者。它们通常是无线循环的,以等待服务请求或者执行线程的任务。它们不能做重要工作,因为我们不可能知道守护线程什么时候获取CPU时钟,并且,在没有其他线程运行时,守护线程随时可以结束。典型应用就是JAVA
GC。

看一下守护线程的创建:

    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
            System.out.println("====== A new Daemon thread ======");
        });
        thread1.setDaemon(true);
        thread1.start();;
    }

取得线程的返回值

最常用的方法,就是主线程等待或者join

    public static void main(String[] args) throws InterruptedException {
        Dog dog=new Dog();
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("====== A new thread ======");
                dog.setName("汪汪");
            }

        });
        //第一种情况,等待
        //thread.start();
        //while (dog.getName() == null){
        //     Thread.sleep(1000);
        //}
        //第二种情况:join
        thread.join();
        thread.start();

        System.out.println(dog.toString());
  }

FutureTask

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> callable1 = new Callable<String>() {
            @Override
            public String call() throws Exception {
                String result="假设这里是耗时操作";
                return result;
            }
        };
        FutureTask<String> futureTask = new FutureTask<>(callable1);
        new Thread(futureTask).start();
        String info = futureTask.get();
        
    }

缺点

多线程这样好,难道就没有缺点吗?那肯定是有的

  • 因为线程之间,是共享进程的内存空间的,所以会有临界值的存在,当多个进程操作同一块内存空间,处理不当就会出问题,所以写出正确的多线程代码是一个问题
  • 线程/进程的频繁切换,也是要耗费资源和时间的,所以如果线程设置不当,可能不仅不能提升效率,还会造成反效果。

线程池

当我们拥有了多线程,为了提高“管理水平”,就有了线程池来管理线程,线程池可以维护一组线程,避免频繁的创建/销毁线程,也算是提高效率。

为什么我们需要线程池呢?

线程池主要解决以下几个问题:

  • 减小创建/销毁线程的开销
  • 对线程进行统一管理,比如线程数量

线程池的生命周期/状态

线程的生命周期控制的是线程的执行或者不执行,线程池的生命周期控制的是线程池接收不接收新任务。
这里抄一张图过来:

RUNNING:能接受新任务,并处理阻塞队列中的任务
SHUTDOWN:不接受新任务,但是可以处理阻塞队列中的任务
STOP:不接受新任务,并且不处理阻塞队列中的任务,并且还打断正在运行任务的线程,就是直接撂担子不干了!
TIDYING:所有任务都终止,并且工作线程也为0,处于关闭之前的状态
TERMINATED:已关闭。

创建一个线程池

我们都知道,java默认内置了4种默认的线程池

        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

自定义

因为后面4种其实最后都用的是这个,所以先说自定义的情况。我们都知道,自定义线程池的时候,会有7个参数可以传,是这样的:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

他们分别是:

  • corePoolSize:核心线程数,线程池中始终存活的线程数。
  • maximumPoolSize: 最大线程数,线程池中允许的最大线程数。
  • keepAliveTime: 存活时间,线程没有任务执行时最多保持多久时间会终止。
  • unit: 单位,参数keepAliveTime的时间单位,7种可选。
  • workQueue: 一个阻塞队列,用来存储等待执行的任务,均为线程安全,7种可选。
  • threadFactory: 线程工厂,主要用来创建线程,默及正常优先级、非守护线程。
  • handler:拒绝策略,拒绝处理任务时的策略,4种可选,默认为AbortPolicy。

简单来说,如果想自己创建一个线程池,只要按照需求传入这些参数就可以,当然有一些参数不是必填的,有的会有默认值,java默认写了几个重载的方法,自己用的时候挑选一下就可以了。

newCachedThreadPool

先来看一下newCachedThreadPool的创建都有哪些参数:

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

可以看到,newCachedThreadPool是一个核心线程数为0,最大线程数无上限(其实是2^31-1),核心线程数之外的空闲线程回收时间是60s,然后用的是SynchronousQueue 队列,这种队列,一个offer,必须对应一个poll。
也就是说,对于newCachedThreadPool,只要有任务进来,就一定会创建或者复用一个线程执行它。所以假设任务多,而且处理任务时间比较长,newCachedThreadPool是非常不适合的,会一直创建新线程。

newCachedThreadPool 适用场景:处理任务速度 > 提交任务速度,耗时少的任务(避免无限新增线程)。

newSingleThreadPool

来看下代码:

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
   

可以看到,newSingleThreadPool的核心线程数和最大线程数都是1,使用LinkedBlockingQueue队列,可以认为这是个无界的队列,可以一直往里面提交任务。
也就是说,对于newSingleThreadPool来说,只会有一个线程执行任务,执行完一个执行下一个排队最久的。
newCachedThreadPool 适用场景:任务需要排队执行的情况。

newFixedThreadPool

代码:

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

可以看到,newFixedThreadPool的核心线程数需要自己设置,
最大线程数=核心线程数,同样使用了LinkedBlockingQueue。
也就是说,newFixedThreadPool可以维护固定数目的线程,多余的任务就去排队了。
也就是说,
newFixedThreadPool 适用场景:任务数一定,执行时间比较长的情况。

newScheduledThreadPool

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }

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

可以看到,newScheduledThreadPool比较特殊,它返回了一个ScheduledExecutorService,它的核心线程数需要用户设置,最大线程数无上限,同时超出核心线程数的空闲线程会在10s之后被销毁。newS,cheduledThreadPool比较特殊的一点在于,他可以延时执行,通过提交任务时的参数可以实现,比如:

    public static void main(String[] args) {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

        ExecutorService workStealingPool = Executors.newWorkStealingPool();
        for (int i = 0; i < 20; i++) {
            scheduledThreadPool.schedule(new Runnable() {

                @Override
                public void run() {
                    System.out.println("delay 3 seconds");
                }
            }, 10, TimeUnit.SECONDS);
        }
    }

在例子中,所有被提交的任务都会在提交之后的10s后执行。
具体的newScheduledThreadPool可以看下这个文章,写的挺详细的:ScheduledThreadPoolExecutor

引用

java创建一个守护线程_JAVA 并发编程之守护线程的创建与运行

如果你是 JDK 设计者,如何设计线程池?我跟面试官大战了三十个回合

ava中线程池的使用

以上是关于java线程和线程池的主要内容,如果未能解决你的问题,请参考以下文章

Java线程池详解

Java 线程池详解

Java线程池详解

newCacheThreadPool()newFixedThreadPool()newScheduledThreadPool()newSingleThreadExecutor()自定义线程池(代码片段

IDEA对新建java线程池的建议

java中的进程,线程,线程池总结