12.4 Java与线程
12.4.1 线程的实现
实现线程主要有三种方式:使用内核线程实现、使用用户线程实现、使用用户线程加轻量级进程混合实现。
1、使用内核线程实现
内核线程就是直接由操作系统内核支持的线程。
程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口—轻量级进程,轻量级进程就是我们通常意义上讲的线程,由于每个轻量级进程都由一个内核线程支持,因些只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程之间的1:1的关系称为一对一的线程模型,如下图所示。
由于每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的。
2、使用用户线程实现
广义上来讲,一个线程只要不是内核线程,那就可以认为是用户线程,因此从这个定义上来讲轻量级进程也属于用户线程。
而狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因操作可以是非常快速且低消耗的,也可以支持规模更大的线程数量,部分高性能的数据库中的多线程就是由用户线程实现的。这进程与用户线程之间1:N的关系称为一对多的线程模型,如下图所示。
使用用户线程的优势在于不需要系统内核支持,劣势也在于没有系统内核的支持,所有的线程操作都需要用户程序自己处理。因而使用用户线程实现的程序一般都比较复杂。
3、混合实现
将内核线程与用户线程一起使用的实现方式。集两者的优点于一身。
4、Java线程的实现
Java线程在JDK1.2之前,是用户线程实现的,而在JDK1.2中,线程模型被替换为基于操作系统原生线程模型来实现。
12.4.2 Java线程调度
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度和抢占式线程调度。
使用协同式线程调度的多线程系统,线程执行的时间由线程本身控制,线程把自己的工作执行完成后,主动通知系统切换到另外一个线程上去。优点的实现简单,切换操作对线程自己是可知的,所以没有同步问题。缺点是如果有一个线程有问题一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。
使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,在这种实现线程调度的方式下,线程执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题,Java使用是的抢占式来实现多线程的。
Java中可以通过设置线程优先级调节线程的招行时间。不过线程优先级并不是太靠谱,原因是Java的线程是被映射到系统的原生线程上来实现的,交由操作系统来调度,虽然现在很多操作系统都提供线程优先级的概念,但是并不见得能与Java线程的优先级一一对应。
12.4.3 状态转换
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
阻塞的情况分三种:
(1)、等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒。
(2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
(3)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
注:拿到对象的锁标记,即为获得了对该对象(临界区)的使用权限。即该线程获得了运行所需的资源,进入“就绪状态”,只需获得CPU,就可以运行。因为当调用wait()后,线程会释放掉它所占有的“锁标志”,所以线程只有在此获取资源才能进入就绪状态。