【java】线程和线程池

简介: 【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中线程池的使用

相关文章
|
1月前
|
设计模式 缓存 安全
【JUC】(6)带你了解共享模型之 享元和不可变 模型并初步带你了解并发工具 线程池Pool,文章内还有饥饿问题、设计模式之工作线程的解决于实现
JUC专栏第六篇,本文带你了解两个共享模型:享元和不可变 模型,并初步带你了解并发工具 线程池Pool,文章中还有解决饥饿问题、设计模式之工作线程的实现
142 2
|
1月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
135 1
|
1月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
156 1
|
2月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
Java 数据库 Spring
134 0
|
2月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
217 16
|
3月前
|
缓存 并行计算 安全
关于Java多线程详解
本文深入讲解Java多线程编程,涵盖基础概念、线程创建与管理、同步机制、并发工具类、线程池、线程安全集合、实战案例及常见问题解决方案,助你掌握高性能并发编程技巧,应对多线程开发中的挑战。
|
3月前
|
数据采集 存储 前端开发
Java爬虫性能优化:多线程抓取JSP动态数据实践
Java爬虫性能优化:多线程抓取JSP动态数据实践
|
4月前
|
Java API 调度
从阻塞到畅通:Java虚拟线程开启并发新纪元
从阻塞到畅通:Java虚拟线程开启并发新纪元
350 83
|
4月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
198 0

热门文章

最新文章