并发不一定要依赖多线程,也可以是多进程。但是在java中,并发一般是线程相关。本文主要是从java虚拟机的角度来探讨线程的实现。
线程的实现
我们都知道在操作系统层面来讲,线程是比进程更轻量级的调度执行单位,在计算机中,线程是CPU执行调度的基本单位。进程可以产生线程,所以线程的引入,把进程的资源分配和执行调度实现分离,各个线程既可以共享线程的资源,比如内存地址,文件IO等,又可以独立调度执行任务作业(如前文所说,线程是CPU最小调度单元)。
现在主流的操作系统都实现了线程。不同的平台不同的操作系统,对线程的实现原理可能都会有差别。java线程基于不同的平台和操作系统,做了统一的封装处理,从语言层面来看,各个平台和操作系统并无差异。
目前实现线程主要有3种方式:
- 使用内核线程实现
- 使用用户线程实现
- 使用用户线程加轻量级进程实现
使用内核线程实现
内核线程就是操作系统内核直接支持的线程,由操作系统内核完成线程切换,内核通过操作调度器实现对线程的执行调度,并将线程任务映射到不同的处理器上。看起来内核就像一个操作系统的分身,这样操作系统就可以实现同时处理多个任务的可能。
在实现上,现在程序一般不会值直接调用内核线程,而是调用轻量级进程,轻量级进程和内核线程一对一映射。先有支持内核线程,才能有轻量级进程。这种轻量级进程和内核线程之间1:1的关系称为一对一的线程模型
。由于是操作系统支持多线程,所以在某个任务使用一个线程执行的时候遇到了阻塞,一般也不会影响其他线程的处理。其原理如下图:
由于每个轻量级进程与内核线程一一对应,所以每个轻量级进程都是一个独立的调度单元。
其优点是每个轻量级进程相互不影响,一个阻塞不会影响其他轻量级进程。
其缺点也是非常明显,由于是内核线程,那么线程所有的操作如创建、析构、同步都需要在内核态完成,需要频繁的在用户态和内核态切换,同时需要轻量级进程和内核线程一一对应,也会消耗内核线程的栈空间。
使用用户线程实现
从广义上来讲,只要不是内核线程,就可以认为是用户线程。如果从这个角度来看,轻量级进程也算是用户态线程。而从狭义上来讲,完全建立在用户空间的线程库上,系统内核完全感知不到用户线程库的存在。像线程的创建、同步、销毁和调度等等工作全部在用户态完成。
其实现原理如下图:
如上图,可知一个进程对应多个用户线程,这种进程与用户线程1:N的关系称为
一对多的线程模型
。由于所有的线程操作与内核无关,都是在用户空间完成,所以其优缺点也很明显。
其优点是:线程所有操作都在用户态完成,不需要切换到内核态,如果实现得当,那么速度非常快,资源消耗也很低,可以支持更大规模的线程数量。
其缺点是由于线程的操作没有操作系统内核的支援,所有线程操作都需要用户程序自己实现,创建、线程间的切换、调度都需要用户程序处理,并且由于操作系统只把处理器资源分配到了进程,除了线程切换获取处理器外,线程阻塞如何处理,或者把进程内的线程映射到其他处理器,都是非常困难的。
现在使用用户线程的程序越来越少了,像Java、Ruby等语言都曾经使用过用户线程,不过现在都放弃了。
使用用户线程加轻量级进程混合实现
线程除了依赖内核实现和完全使用用户程序实现,还可以将内核线程与用户线程线程一起使用来实现。既存在用户线程,也存在轻量级进程,用户线程负责完全建立在用户控件,线程的创建、切换、析构、销毁等操作依然速度快,消耗资源少,而线程的调度,多线程和多处理器之间的映射则由轻量级进程对应的内核线程来处理,降低了线程调度和阻塞的风险。但是此时,用户线程和轻量级进程之间的数量是不确定的,即N:M的关系,这种模型称为多对多的线程模型
。
其原理如下:
Java线程的实现
Java线程在JDK1.2之前,是基于成为“绿色线程”的用户线程实现的,从JDK1.2开始,线程模型替换为使用操作系统原生的线程模型来实现。所以操作系统支持什么样的线程模型,java虚拟机的线程模型就是什么样的。在Linux和Windows系统中,由于系统本身的使用的一对一的线程模型,所以java虚拟机的线程也是一对一的,一个java线程映射到一个轻量级进程。而Solaris系统既支持一对一的线程模型(BoundThread),也支持多对多的线程模型(LWP/Thread Based Synchronization),所以Solaris版的JDK也提供了虚拟机参数-XX:+UseLWPSynchronization(默认值)和-XX:+UseBoundThreads来指定虚拟机使用哪种线程模型。
Java线程调度
线程调度是指操作系统为线程分配处理器使用权的过程。目前主要的调度方式有两种:协同式线程调度(Cooperation Threads-Scheduling)和抢占式线程调度(Preemptive Threads-Scheduling)。
- 协同式线程调度
协同式线程调度,线程的执行时间由线程自己决定,线程执行完成后就通知操作系统,我执行完成,可以切换到其他线程来执行。
优点是实现简单,并且线程调度切换对线程自身是可知的,只有执行完成才会切换,线程间频繁切换消耗的资源在这里就没有了。
缺点是线程执行时间不可控,容易造成其他线程一直阻塞或得不到执行。从而产生雪崩效应,整个系统崩溃。 - 抢占式线程调度
抢占式线程调度,线程的执行是由操作系统来分配决定的,线程间的切换不是由线程自身决定的。
优点是线程的执行时间可控,由操作系统决定,并且每个线程都有执行的机会,一旦某个线程阻塞不会影响其他线程。
缺点是线程间频繁的切换,需要对当前正在执行的线程相关上下文进行保存,恢复等,消耗资源。
在抢占式线程调度方式中,虽然操作系统决定哪个线程执行。但是我们可以告诉操作系统某些线程希望获取执行的时间多一些,机会大一些。这就是我们常说的线程优先级。在java中,一共设置了10个线程优先级Thread.MIN_PRIORITY到Thread.MAX_PRIORITY,当不同线程同时处于就绪状态后,优先级大的获得执行的概率就大。但是这并不是绝对的,因为java使用的是操作系统的原生线程模型,而不同的操作系统对线程的优先级定义跟java不同,如Solaris定义了2^32次方个优先级,Windows定义了7个线程优先级。如果操作系统线程优先级比java的多,那还好,中间空余好多优先级,java的线程优先级会一一对应的不同的系统定义的线程优先级上,但是如果比java的线程优先级少,那是比会有不同的java线程优先级映射到相同的系统定义的线程优先级上。同时,操作系统也会有“优先级推进器”,即发现某个线程执行的非常勤奋努力,那么就会越过县城优先级的设置,尽可能多的去给该线程分配执行时间。
Java线程状态转换
操作系统的进程(线程),从最经典的就绪、执行、阻塞三种状态,到后来为了管理等增加了新建、挂起、结束等状态。而Java语言定义的线程状态有5中分别是:
- 新建(New):创建后尚未启动的线程处于这种状态
- 运行(Running):运行态就是启动后的线程,即调用了start()方法的线程,该状态对应到操作系统就是就绪和运行两种状态,即要么是正在运行的线程,要么是除了没有分配cpu,其他资源都有经获取完成的线程。
- 等待(Waiting):等待状态的线程分为无限期等待(Waiting)和有限期等待(Timed Waiting)。无限期等待不会被分配cpu执行时间,需要等待其他线程显示的唤醒。如调用了:没有设置Timeout参数的Object.wait()方法,没有设置Timeout的Thread.join()方法,LockSupport.park()方法。有限期等待也不会被分配cpu执行时间,不过不是被其他线程显示唤醒,而是等待一定的时间后系统自动唤醒。如调用了:Thread.sleep()方法,设置了Timeout参数的Object.wait()方法,设置了Timeout参数的Thread.join方法,LockSupport的pardNanos()和parkUnit()方法。
- 阻塞(Blocked):阻塞状态是线程在等待获取一个排它锁,这个事件只有其他线程释放这个排它锁的时候发生,线程在等待进入同步区域的时候就是出于阻塞状态
-
结束(Terminated):执行结束的线程状态。
这五种线程状态在遇到某些特定事件后会相互转换,如下图: