Java线程之线程概述

    不知从什么时候开始,似乎到处都充斥着高并发的味道,或许是源于网络购票曾经遇到过的种种困难,或许是由于各个购物网站的各种促销,于是乎在各种招聘启事中都明确要求有丰富的高并发、可扩展性等要求,那么多少数量的并发才可以称之为高并发呢?该如何评价自己是否具有高并发服务器程序的技能和经验呢?对这些问题,本人不能给出明确的答案,首先是因为没有开发过高并发的服务器程序(参与过的大型项目是基于ssh的web应用),其次在接触计算机编程的年代,到处都在鼓吹Java面向对象的各种优点,诸如不用操心内存的分配和释放,可移植性(其实很多语言都具有可移植性),不用重复造轮子(工作后才发现能够自己制造轮子是多么的幸福),Java多线程非常容易实现,Java提供了各种数据结构集合等,于是很多底层基础知识都直接跳过了,等到发现如果想要更进一步弄清楚某些问题的根源时,往往就会发现到处都是瓶颈,甚至都不知道自己需要百度什么。于是工作后开始重新阅读操作系统、算法等相关书籍,当再次学习这些基础却非常重要的知识时,总会得到不同的启示,也许是因为上学时纯粹是为了应付考试。

    出自兴趣和工作的需求,接触到大数据相关领域,也开始使用开源的大数据存储、处理和分析工具,比如Hadoop、Hive和HBase,还接触到了OpenStack云计算框架,其中工作用到的是Swift模块。这些软件也好,框架也罢,都在标榜自己具有高并发性、高可用性和扩展性,于是就在思考它们是如何实现高并发的呢?由于自己好歹有点Java编程知识,自然而然的就想到了多线程,创建了Java多线程后就一定是高并发了吗?答案显然是不一致的。有的情况使用多线程确实可以得到更高的效率,比如一个件任务可以被分为互不影响的子任务,无论是在单处理器还是多处理器上都可以得到效率的提升,相反如果一个任务是顺序执行的,后面的代码必须等到前面的代码执行完毕才能继续执行,那么多线程反而有可能降低效率。由此可以简单的得出结论多线程的使用要依赖于具体的场景,这也是很多开源软件在介绍某个参数时非常喜欢使用的一句话,但对于我来说却非常不喜欢这句话,因为这并没有给我明确唯一的答案(中国学生总喜欢明确唯一的答案)。为了寻找最爱,只好再一次拿起《操作系统概念》,并准备好了Hadoop和HBase的源代码,方便随时参考和查阅具体实现。

1.1         线程概念

    尽管多线程编程的概念已经非常普及,还是有必要重申一下线程的概念及其与进程的关系。线程是运行于CPU中的基本单元,对于即使不支持多线程的系统来说——尽管这样的系统少之又少,但确实存在过——这一说法也是成立的。或许有人要问运行于CPU中的基本单元不是进程吗,那么就有必要澄清一下进程的概念,尽管到处都在谈论进程。广义上或者通俗地讲,进程指的是运行中的程序,因此是活动的实体,更具体地来说,进程包括程序代码(文本段或代码段),程序计数器的值和寄存器集合中的内容,数据段,堆栈等。而线程是进程代码的执行流,包括线程ID、程序计数器、寄存器集合和线程栈,与属于同一进程的其它线程(如果存在的话)共享代码段、数据段和其它操作系统资源,比如打开文件和信号等。由于线程具有进程的某些属性,如程序计数器、寄存器等,因此也被称为轻量级进程,而传统意义上的进程则被称为重量级进程,可以用下面的图形象地描述单线程进程和多线程进程的差别:

Java线程之线程概述_第1张图片

    从图中可以清晰地发现,在单线程进程中线程就是进程,在多线程进程中存在多个线程,也就意味着可以同时做许多任务(同时在单处理器中并不是真正意义上的在同一时刻,而是由于处理器超快的处理速度给使用者一种近乎真实同时的错觉,但在多处理器中却可以做到真正的同一时刻)。还可以确定的是线程必属于某个进程且不能独立存在于进程之外,而进程则至少拥有一个线程。

    既然线程仅是进程中的执行序列,而多线程进程中可以同时存在多个执行序列以完成多个任务,那为什么不将多个线程拆分到多个进程中来完成多个任务,或者说在某个系统中仅存在重量级进程而不存在轻量级进程,反过来说,为什么不将所有的任务都合并到一进程中,即整个系统只存在一个进程,该进程由众多的线程组成。答案显然是非常复杂的,简单地讲,之所以不将多个线程创建为多个进程,是因为:

1.       属于同一进程的所有线程共享共同的数据,它们之间的通信不需要进程间通信的额外开销。

2.       由于共享相同的资源,创建线程通常比创建进程更加快速(只需要创建所使用的线程栈),且线程的上下文切换比进程的上下文切换同样更加高效。

3.       对某些服务,比如打印机或者web服务器,多线程往往比多进程能够提供更加高效的响应速度,因为同一进程中的线程间通信往往比进程间的通信更加高效。

     既然多线程存在这么多优点,为什么不将所有的进程合并为一个进程,该进程中的线程将完成之前所有进程的工作。显然这样的设想是不成立的,因为操作系统本身就不仅仅是一个单独的进程,而是由若干完成不同任务的进程组成,或者说由不同的模块组成。那退一步讲,假设操作系统仅仅是由一个进程组成,所有用户的任务由一个进程组成,这势必会招致安全问题,因为所有用户的任务共享相同的内存空间,彼此可以访问对方的数据,而如果想避免这些问题则会增加操作系统在安全方面的复杂性。另外从软件工程的角度讲,将不相干的任务合并为一个进程违反了松耦合的原则,比如将打印机和web服务器或者浏览器放在一个进程中,这种强行粘合的行为带来的危害性远远大于多线程提供的诸多优点。

1.2         上下文切换

    在上文中几次提到上下文切换这一概念,笼统地讲,上下文切换指的是将CPU的使用权由一个进程或者线程交由另一个进程或线程,而涉及的细节往往与特定操作系统或硬件有密切的关系。进程的上下文切换指的是不同进程之间的轮流获得CPU的使用权,比如当一个进程等待IO的完成,则要交出CPU的使用权,另一个等待CPU的进程获得使用权进而可以执行。线程的上下文切换指的是同一进程中的线程轮流获得CPU的使用权,不同进程的线程上下文切换即为进程的上下文切换。在单处理器的系统中,无论是进程内的线程上下文切换还是进程间的上下文切换都容易理解,因为仅有单个CPU,进程内的线程上下文切换仅需要保存之前运行的线程的状态,然后载入即将运行线程的状态,而进程间的线程上下文切换实际为进程上下文切换,不管这些进程是单线程还是多线程的。在多处理器的系统中,进程内的线程上下文切换与单处理器的情况相同,但进程间的线程上下文切换是进程上下文切换还是等同于进程内的线程上下文切换呢?如果要执行上下文切换的线程属于单线程进程,那么显然是进程上下文切换,如果是多线程进程呢?在此需要明确的一点时,进程内的线程上下文切换不需要改变进程的地址空间,而进程上下文切换则需要改变进程的地址空间,由于不同进程的线程拥有不同的地址空间,进程间的线程上下文也需要改变地址空间,因此即使是多处理器系统,进程间的线程上下文切换也是进程上下文切换。根据以上的论述可以得出,在不明确指出进程间线程上下文切换的情况下,线程上下文切换即为进程内的线程上下文切换,因此只有这一种情况线程上下文切换比进程上下文切换高效,而不论单CPU还是多CPU系统,进程间的上下文切换都等价于进程上下文切换。

1.3         线程类型

    根据线程是否由操作系统管理和调度,可以将线程分为两类:用户线程和内核线程。顾名思义,用户线程由用户或者用户创建的应用程序管理,用户应用程序负责线程的创建、调度、上下文切换、线程间信息的传递及线程的销毁,从操作系统的角度看,用户线程的多线程进程仅是单线程进程。由于用户应用程序负责用户线程的各个方面,并不需要执行系统调用,因此用户线程的调度、上下文切换等也更加快速高效,但由此导致的缺点也是非常明显的。首先,由于不是内核负责用户线程的调度,内核只能调度用户应用程序(单线程进程),因此无法将多线程调度到多个CPU中,因而无法利用多CPU的并行优势。其次,当某个用户线程执行系统调用被阻塞时,即使存在可以执行的用户线程,这样的线程也无法执行,因为内核见到的单线程进程。最后,内核分配给用户线程进程的时间片需要由应用程序分配给用户线程,即所有的用户线程共享该时间片。

     内核线程则由操作系统负责创建、调度、上下文切换和销毁,由于操作系统的参与,内核线程克服了用户线程的缺点,即内核线程可以被分配时间片运行,当同一进程中的某个内核线程阻塞时,其余内核线程依然有机会可以运行(取决于操作系统是否调度其运行),可以将多个内核线程分配到多个CPU上运行以充分利用多CPU的优势。凡事存在有利的一面就存在不利的一面,相对用户线程,内核线程的创建和管理更加复杂和低效。

用户线程是运行在用户空间的线程,而内核线程则是运行在内核空间的线程,这就导致了用户线程和内核线程之间存在某种联系,这种联系建立了下文将要描述的线程模型。

1.4         线程模型

   线程模型定义了如何将用户线程映射到内核线程,主要有多对一模型、一对一模型和多对多模型。

1.4.1      多对一模型

    多对一模型是将多个用户线程映射到一个内核线程。线程的管理是在用户空间进行的,由于管理不涉及系统调用,因此比较高效。但是由于多个用户线程仅能映射或关联到一个内核线程,当关联内核线程的用户线程执行了阻塞系统调用时,整个进程都会被阻塞,且由于仅有一个线程可以被操作系统调度,多个用户线程无法运行在多个CPU上。早期版本的Java,Solaris Green Thread采用多对一模型实现的。

1.4.2      一对一模型

    顾名思义,在一对一模型中,每个用户线程都有一个内核线程与之对应,这样当某个用户线程执行阻塞系统调用,如等待文件读写的完成,等待网络数据的到达等,其它用户线程不会因为阻塞线程而失去执行的机会,并且可以真正并行运行在多处理器上。由于任何事物都有两面性,一对一模型的缺点是每创建一个用户线程就要创建一个内核线程,而这是比创建用户线程低效的,且由于不同系统具有不同的配置(内存、硬盘、CPU数量及速度),创建过多的用户线程会对系统造成不同的影响,进而影响应用程序的性能。众多Linux系统和Windows系统都实现了一对一模型。

1.4.3      多对多模型

    在多对一模型中,尽管可以创建任意多的用户线程(只要有足够的内存),但每次只能够调度有一个用户线程到内核线程,因此并没有增加应用程序的并发性。而一对一模型存在的缺点是允许的内核线程数量存在上限,并且任意增加用户线程数量(也会创建相应的内核线程)会给系统或者应用程序带来性能的影响,因此在一定程度上需要限制可以同时存在的用户线程数量。为了既可以创建任意数量的用户线程,又能够以内核线程的方式并发运行程序,结合多对一和一对一模型,发展了多对多模型。多对多模型允许用户线程多路复用到同等数量或者更少数量的内核线程上,因此既允许创建任意多的用户线程,又可以使相应内核线程并行运行在多处理器上,当一个线程阻塞时并不会阻塞其余的线程。尽管多对多模型具有如此的优势,但构建和同步用户线程的调度器到内核的调度器特别困难和容易出错。在Linux内核上进行了多对多和一对一模型的比较,结果显示一对一模型在性能和用户使用方面通常更具优势,然而在特定领域多对多模型可能是正确的选择。

    在Solaris 9之前的版本,Solaris支持多对多模型,但从此之后的版本开始使用一对一模型。

1.5         Java线程

    看过上面的介绍,对于使用Java的用户可能要问的问题是,Java线程是用户线程还是内核线程,Java线程使用的哪种线程模型。有过Java线程编程经验的人都知道,Java线程是通过执行start方法(而不是new一个Thread对象)创建的,即由应用程序在用户空间创建的,因此Java线程显然是用户线程。由于java程序运行在JVM中,JVM根据运行的操作系统又有不同的实现,因此不同平台上的JVM实现了不同的多线程模型。比如在Windows和Linux中使用的是一对一模型,而在早期的Solaris中实现的是多对一模型,后来改为多对多模型,Solaris 9后又采用了一对一模型。另外,Java线程与宿主操作系统的线程库之间存在一定的联系,比如在Windows中使用Win32 API线程库创建线程,而在Linux中则使用Pthread创建线程。

你可能感兴趣的:(编程语言)