java中为什么要使用线程池详解编程语言

java中为什么要使用线程池详解编程语言

    Java从最开始就是基于线程的,线程在Java里被封装成一个类java.lang.Thread。在面试中很多面试官都会问一个很基础的关于线程问题:

    Java中有几种方法新建一个线程?所有人都知道,标准答案是两种:继承Thread或者实现Runnable,在JDK源代码中Thread类的注释中也是这么写的。

    然而在我看来这两种方法根本就是一种,所有想要开启线程的操作,都必须生成了一个Thread类(或其子类)的实例,执行其中的native方法start0()。


一:Java中的线程

    Java中将线程抽象为一个普通的类,这样带来了很多好处,譬如可以很简单的使用面向对象的方法实现多线程的编程,然而这种程序写多了容易会忘记,这个对象在底层是实实在在地对应了一个OS中的线程。

    操作系统中的线程和进程

    上图中的进程(Process)可以看做一个JVM,可以看出,所有的进程有自己的私有内存,这块内存会在主存中有一段映射,而所有的线程共享JVM中的内存。在现代的操作系统中,线程的调度通常都是集成在操作系统中的,操作系统能通过分析更多的信息来决定如何更高效地进行线程的调度,这也是为什么Java中会一直强调,线程的执行顺序是不会得到保证的,因为JVM自己管不了这个,所以只能认为它是完全无序的。

    另外,类java.lang.Thread中的很多属性也会直接映射为操作系统中线程的一些属性。Java的Thread中提供的一些方法如sleep和yield其实依赖于操作系统中线程的调度算法。

    关于线程的调度算法可以去读操作系统相关的书籍,这里就不做太多叙述了。


二:线程的开销

        通常来说,操作系统中线程之间的上下文切换大约要消耗1到10微秒

    从上图中可以看出线程中包含了一些上下文信息:

        CPU栈指针(Stack)、

        一组寄存器的值(Registers),

        指令计数器的值(PC)等,

    它们都保存在此线程所在的进程所映射的主存中,而对于Java来说,这个进程就是JVM所在的那个进程,JVM的运行时内存可以简单的分为如下几部分:

        若干个栈(Stack)。每个线程有自己的栈,JVM中的栈是不能存储对象的,只能存储基础变量和对象引用。

        堆(Heap)。一个JVM只有一个堆,所有的对象都在堆上分配。

        方法区(Method Area)。一个JVM只有一个方法区,包含了所有载入的类的字节码和静态变量。

    其中#1中的栈可以认为是这个线程的上下文,创建线程要申请相应的栈空间,而栈空间的大小是一定的,所以当栈空间不够用时,会导致线程申请不成功。在Thread的源代码中可以看到,启动线程的最后一步是执行一个本地方法private native void start0(),代码1是OpenJDK中start0最终调用的方法:

    //代码1

  1.     JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  2.       JVMWrapper(“JVM_StartThread”);
  3.       JavaThread *native_thread = NULL;
  4.       bool throw_illegal_thread_state = false;
  5.       // We must release the Threads_lock before we can post a jvmti event
  6.       // in Thread::start.
  7.       {
  8.         MutexLocker mu(Threads_lock);
  9.         //省略一些代码
  10.           jlong size =
  11.                  java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
  12.           size_t sz = size > 0 ? (size_t) size : 0;
  13.           native_thread = new JavaThread(&thread_entry, sz);
  14.       }
  15.       if (native_thread->osthread() == NULL) {
  16.         THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),
  17.                   “unable to create new native thread”);
  18.       }
  19.       Thread::start(native_thread);
  20.     JVM_END

    从代码1中可以看到,线程的创建首先需要栈空间,所以过多的线程创建可能会导致OOM。

    同时,线程的切换会有以下开销:

        CPU中执行上下文的切换,导致CPU中的「指令流水线(Instruction Pipeline)」的中断和CPU缓存的失效。

        如果线程太多,线程切换的时间会比线程执行的时间要长,严重浪费了CPU资源。

        对于共享资源的竞争(锁)会导致线程切换开销急剧增加。

    根据以上的描述,所以通常建议尽可能创建较少的线程,减少锁的使用(尤其是synchronized),尽量使用JDK提供的同步工具。而为了减少线程上下文切换带来的开销,通常使用线程池是一个有效的方法。


三:Java中的线程池

    Executor框架中最常用的大概就是java.util.concurrent.ThreadPoolExecutor了,对于它的描述,简单的说就是「它维护了一个线程池,对于提交到此Executor中的任务,它不是创建新的线程而是使用池内的线程进行执行」。对于「数量巨大但执行时间很小」的任务,可以显著地减少对于任务执行的开销。java.util.concurrent.ThreadPoolExecutor中包含了很多属性,通过这些属性开发者可以定制不同的线程池行为,大致如下:

    1:线程池的大小:corePoolSize和maximumPoolSize

    ThreadPoolExecutor中线程池的大小由这两个属性决定,前者指当线程池正常运行起来后的最小(核心)线程数,当一个任务到来时,若当前池中线程数小于corePoolSize,则会生成新的线程;后者指当等待队列满了之后可生成的最大的线程数。在例1中返回的对象中这两个值相等,均等于用户传入的值。

    2:用户可以通过调用java.util.concurrent.ThreadPoolExecutor上的实例方法来启动核心线程(core pool)

    3:可定制化的线程生成方式:threadFactory

    默认线程由方法Executors.defaultThreadFactory()返回的ThreadFactory进行创建,默认创建的线程都不是daemon,开发者可以传入自定义的ThreadFactory进行对线程的定制化。

    4:非核心线程的空闲等待时间:keepAliveTime

    5:任务等待队列:workQueue

    这个队列是java.util.concurrent.BlockingQueue<E>的一个实例。当池中当前没有空闲的线程来执行任务,就会将此任务放入等待队列,根据其具体实现类的不同,又可分为3种不同的队列策略:

        1.容量为0。如:java.util.concurrent.SynchronousQueue等待队列容量为0,所有需要阻塞的任务必须等待池内的某个线程有空闲,才能继续执行,否则阻塞。调用Executors.newCachedThreadPool的两个函数生成的线程池是这个策略。

        2.不限容量。如:不指定容量的java.util.concurrent.LinkedBlockingQueue等待队列的长度无穷大,根据上文中的叙述,在这种策略下,不会有多于corePoolSize的线程被创建,所以maximumPoolSize也就没有任何意义了。调用Executors.newFixedThreadPool生成的线程池是这个策略。

        3.限制容量。如:指定容量的任何java.util.concurrent.BlockingQueue<E>在某些场景下(本文中将描述这种场景),需要指定等待队列的容量,以防止过多的资源消耗,比如如果使用不限容量的等待队列,当有大量的任务到来而池内又无空闲线程执行任务时,会有大量的任务堆积,这些任务都是某个类的对象,是要消耗内存的,就可能导致OOM。如何去平衡等待队列和线程池的大小要根据实际场景去断定,如果配置不当,可能会导致资源耗尽、线程上下文切换消耗、或者线程调度消耗。这些都会直接影响系统的吞吐。

    6:任务拒绝处理器:defaultHandler

    如果任务被拒绝执行,则会调用这个对象上的RejectedExecutionHandler.rejectedExecution()方法,JDK定义了4种处理策略,用户可以自定义自己的任务处理策略。

    7:允许核心线程过期:allowCoreThreadTimeOut

    上面说的所有情况都是基于这个变量为false(默认值)来说的,如果你的线程池已经不使用了(不被引用),但是其中还有活着的线程时,这个线程池是不会被回收的,这种情况就造成了内存泄漏——一块永远不会被访问到的内存却无法被GC回收。

    用户可以通过在抛弃线程池引用的时候显式地调用shutdown()来释放它,或者将allowCoreThreadTimeOut设置为true,则在过期时间后,核心线程会被释放,则其会被GC回收。

java中为什么要使用线程池详解编程语言

转载请注明来源网站:blog.ytso.com谢谢!

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/14613.html

(0)
上一篇 2021年7月19日
下一篇 2021年7月19日

相关推荐

发表回复

登录后才能评论