聊聊Java线程和CPU调度的精彩内容 (聊聊书法)
什么是线程
现代操作系统在运转一个程序时,会为其创立一个进程,例如,咱们启动一个程序,系统就会创立一个Java进程,在一个进程里可以创立多个线程,这些线程领有自己的计数器、堆栈和部分变量等属性,引入线程的概念可以将一个进程的资源调配和口头调度分开,并且能够访问共享的内存变量,如内存地址和文件I/O等,线程是计算机中比进程更轻量级的调度口头单元,也是系统调度的最小单元,也叫轻量级进程(LightWeightProcess,LWP),CPU在这些线程上高速切换,让经常使用者觉失掉这些线程在同时口头。
一个Java程序从mn()方法开局口头,而后依照既定的代码逻辑口头,看似没有其余线程介入,但实践上Java程序天生就是多线程程序,由于口头main()方法的是一个称号为main的线程。咱们经过一段代码看下一个普通的Java程序蕴含哪些线程。
publicclassthread{publicstaticvoidmain(String[]args){//失掉Java线程治理ThreadMXBeanthreadMXBean=ManagementFactory.getThreadMXBean();//仅失掉线程和线程堆栈消息ThreadInfo[]threadInfos=threadMXBean.dumpAllThreads(false,false);//遍历线程消息,仅仅打印线程ID和线程称号消息for(ThreadInfothreadInfo:threadInfos){System.out.println("["+threadInfo.getThreadId()+"]"+threadInfo.getThreadName());}}}
图片
可以看到,一个Java程序的运转不只仅是main()方法的运转,而是main线程和多个其余线程的同时运转。
线程的成功
干流的操作系统都提供了线程成功。Java言语则提供了在不同配件和操作系统平台下对线程操作一致处置的才干。在Java中,每个曾经口头start()方法且尚未完结的java.lang.Thread类的实例代表一个线程。
检查JDK的Thread类可以看到Thread类与大部分JavaAPI有清楚的差异,它的关键方法都被申明为Native。在JavaAPI中,Native方法通常象征着该方法没有经常使用或无法经常使用平台有关的手腕来成功(说明须要操作的是很底层的物品了,曾经脱离了Java言语层面的范围)。
图片
成功线程关键有3种形式:经常使用内核线程成功(1:1成功)、经常使用用户线程成功(N:1成功)和经常使用用户线程加轻量级进程混分解功(N:M成功)。
1、内核线程成功(1:1成功)
内核线程(Kernel-LevelThread,KLT)是由操作系统内核直接允许的线程,内核经过操纵调度器(Scheduler)对线程启动调度,并担任将线程的义务映射到各个处置器上。下图中KLT线程上方都有一个LWP与之对应,每个内核线程可以视为内核的一个分身,这样操作系统就能够同时处置多个义务,从而允许多线程。
程序普通不会直接经常使用内核线程,而是经常使用内核线程的一种初级接口——轻量级进程(LightWeightProcess,LWP)。轻量级进程是咱们通常所说的线程,由于每个轻量级进程都由一个内核线程允许,因此只要先允许内核线程,才干有轻量级进程。轻量级进程与内核线程之间是一对一的相关,称为一对一的线程模型。
由于内核线程的允许,每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程继续上班。但轻量级进程也有一些局限性:由于是基于内核线程成功的,各种线程操作,如创立、析构及同步,都须要启动系统调用。而系统调用的代价相对较高,须要在用户态(UserMode)和内核态(KernelMode)之间来回切换。其次,每个轻量级进程都须要有一个内核线程的允许,因此轻量级进程要消耗必定的内核资源(例如内核线程的栈空间),因此一个系统允许轻量级进程的数量是有限的。
图片
轻量级进程与内核线程之间1:1的示用意
2、用户线程成功(N:1成功)
用户线程是指齐全建设在用户空间线程库之上的线程成功,系统内核查其无法感知。即一切的用户线程都会对应到一个内核线程中,用户线程的创立、同步、销毁和调度齐全在用户空间中成功,无需内核的协助。假设程序成功切当,这些线程无需切换到内核形式,从而成功极速且低开支的操作。它们还可以允许更多的线程数量,因此在高性能数据库等场景中经常经常使用用户线程。进程与用户线程之间的相关驳回一对多的线程模型。
经常使用用户线程的长处在于不须要系统内核的允许。但是劣势在于它们也缺乏系统内核的允许,一切线程操作都须要用户程序自己处置。须要思考线程创立、切换和调度等疑问。在某一线程被阻塞时,会造成整个所属进程阻塞。Java曾经经常使用过用户线程,但最终丢弃了经常使用它们。但是比如Golang、Erlang等一些新的、以高并发为卖点的变成言语广泛允许了用户线程。
进程与用户线程之间N:1的相关示用意
3、用户线程加轻量级进程混分解功(N:M成功)
内核线程和用户线程联合的成功形式。在这种混分解功中,用户线程和轻量级进程同时存在。用户线程依然齐全建设在用户空间中,因此创立、切换和销毁用户线程的操作依然是便宜的,并且可以同时允许少量的用户线程。操作系统提供对轻量级进程的允许,它们充任用户线程和内核线程之间的桥梁。这样可以应用内核提供的线程调度和处置器映射性能。用户线程的系统调用经过轻量级进程来处置,大大降落了整个进程被齐全阻塞的危险。在这种混合模型中,用户线程和轻量级进程的比例可以变动,构成一个N:M的相关。
许多UNIX系列的操作系统都提供了N:M的线程模型成功。这些操作系统上的运行也相对更容易运行N:M的线程模型。
用户线程与轻量级进程之间N:M的相关示用意
Java线程的成功
操作系统允许怎样样的线程模型,很大水平上会影响上方的Java虚构机的线程是怎样映射的,JVM规范外面没有规则,必定经常使用哪一种模型。线程模型关键影响线程的并发规模和操作老本,关于Java程序的编码和运转环节来说,这些差异都是透明的,Java作为下层运行,其实是感知不到上方三种模型之间的区别的,即开发者无需关注详细的线程模型细节。
在JDK1.2之前,Java线程经常使用的是称为绿色线程(GreenThreads)的用户级线程成功。但是在JDK1.3起,线程模型被交流为基于操作系统原生线程模型的成功形式,即驳回1:1的线程模型。
JavaSE最罕用的JVM是Oracle/Sun研发的HotSpotVM。在这个JVM的较新版本所允许的一切平台上,它都是经常使用1:1线程模型的——除了Solaris之外,它是个特例。也就是说一个Java线程是直接经过一个操作系统原生线程来成功的,两边并没有额外的直接结构。而且HotSpotVM自己也不干预线程的调度,全权交给底下的OS去处置。
Java线程调度
线程调度是指系统为线程调配处置器经常使用权的环节,关键调度形式有两种,区分是协同式线程调度(CooperativeThreads-Scheduling)和抢占式线程调度(PreemptiveThreads-Scheduling)。
假设在多线程系统中经常使用协同式调度,每个线程的口头期间由线程自身控制。在成功上班后,线程须要被动通知系统切换到另一个线程。协同式多线程的关键长处在于繁难性,由于线程切换由线程自身通晓,因此不存在线程同步疑问。协同式调度也存在清楚的缺陷。线程的口头期间无法控制,假设一个线程出现疑问并且没有通知系统切换线程,整个进程或许会有限期地被阻塞。
假设一个多线程系统驳回抢占式调度,系统会为每个线程调配口头期间,线程切换不禁线程自身选择(在Java中,Thread.yield()可以让进口头期间,但线程自身无法控制失掉口头期间)。在这种线程调度成功中,线程的口头期间由系统控制,不会出现一个线程阻塞整个进程的状况。Java经常使用抢占式调度作为其线程调度机制。假设一个进程遇到疑问,咱们可以经常使用义务治理器终止该进程,而不会造成系统解体。
说到计算调度这里还要说一下CPU期间片
在单个处置器的期间,操作系统就能处置多线程并发义务。处置器给每个线程调配CPU期间片(TimeSlice),线程在调配取得的期间片外口头义务。CPU期间片是CPU调配给每个线程口头的期间段,普通为几十毫秒。在这么短的期间外线程相互切换,咱们基本觉得不到,所以看上去就如同是同时启动的一样。
期间片选择了一个线程可以延续占用途理器运转的时长。当一个线程的期间片用完了,或许因自身要素自愿暂停运转了,这个时刻,另外一个线程(可以是同一个线程或许其它进程的线程)就会被操作系统选中,来占用途理器。这种一个线程被暂停剥夺经常使用权,另外一个线程被选中开局或许继续运转的环节就叫做高低文切换。
高低文切换
当一个线程让出CPU期间片时,它须要记载下整个口头高低文,以便在复原口头时从上次退出的中央继续。这包括变量、计算结果、程序计数器等等。就像是对线程的运转环境启动快照,这样当它从新取得CPU期间时,可以经过检索保留的数据极速复原先前的口头高低文。这个环节被称为高低文切换。
在一个领有多个CPU的系统中,操作系统以循环形式将CPU调配给不同的线程。这造成高低文切换愈加频繁,特意是在跨不同CPU启动高低文切换时,比单个CPU内的高低文切换愈加低廉。
在操作系统中,高低文切换可以出当初进程之间或线程之间。在多线程编程的背景下,咱们关键关注线程之间高低文切换的性能影响。如今,让咱们讨论一下多线程中高低文切换的要素。但在此之前,让咱们先了解一下系统线程的生命周期形态。
图片
系统线程关键有新建(NEW)、就绪(RUNNABLE)、运转(RUNNING)、阻塞(BLOCKED)、死亡(DEAD)五种形态。到了Java层面它们都被映射为了NEW、RUNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINADTED等6种形态。
在这个运转环节中,线程由RUNNABLE转为非RUNNABLE的环节就是线程高低文切换。一个线程的形态由RUNNING转为BLOCKED,再由BLOCKED转为RUNNABLE,而后再被调度器选中口头,这就是一个高低文切换的环节。多线程的高低文切换实践上就是由多线程两个运转形态的相互切换造成的。
那么在线程运转时,线程形态由RUNNING转为BLOCKED或许由BLOCKED转为RUNNABLE,是怎样诱发的呢?
系统线程切换可以由多种状况下诱发,包括但不限于以下几种状况:
咱们可以分两种状况来剖析,一种是程序自身触发的切换,这种咱们称为自发性高低文切换,另一种是由系统或许虚构机诱发的非自发性高低文切换。
接上去咱们看一段代码,来对比串联口头和并发口头的速度
packagecom.yuyy.test;publicclassDemolication{publicstaticvoidmain(String[]args){//运转多线程MultiThreadTestertest1=newMultiThreadTester();test1.Start();//运转单线程SerialTestertest2=newSerialTester();test2.Start();}staticclassMultiThreadTesterextendsThreadContextSwitchTester{@OverridepublicvoidStart(){longstart=System.currentTimeMillis();MyRunnablemyRunnable1=newMyRunnable();Thread[]threads=newThread[4];//创立多个线程for(inti=0;i<4;i++){threads[i]=newThread(myRunnable1);threads[i].start();}for(inti=0;i<4;i++){try{//期待一同运转完threads[i].join();}catch(InterruptedExceptione){e.printStackTrace();}}longend=System.currentTimeMillis();System.out.println("multithreadexectime:"+(end-start)+"s");System.out.println("counter:"+counter);}//创立一个成功Runnable的类classMyRunnableimplementsRunnable{publicvoidrun(){while(counter<100000000){synchronized(this){if(counter<100000000){increaseCounter();}}}}}}//创立一个单线程staticclassSerialTesterextendsThreadContextSwitchTester{@OverridepublicvoidStart(){longstart=System.currentTimeMillis();for(longi=0;i<count;i++){increaseCounter();}longend=System.currentTimeMillis();System.out.println("serialexectime:"+(end-start)+"s");System.out.println("counter:"+counter);}}staticabstractclassThreadContextSwitchTester{publicstaticfinalintcount=100000000;publicvolatileintcounter=0;publicvoidincreaseCounter(){this.counter+=1;}publicabstractvoidStart();}}
口头之后,看一下两者的期间测试结果:串联的口头速度比并发的口头速度要快。这就是由于线程的高低文切换造成了额外的开支。
线程的优先级
虽然Java线程调度由系统智能处置,但咱们依然可以倡导系统为某些线程调配更多的口头期间,而为其余线程调配较少的口头期间。这可以经过设置线程优先级来成功。Java言语提供了10个级别的线程优先级。当两个线程同时处于Ready形态时,优先级较高的线程更有或许被系统选用口头,其实就是让高优先级的线程取得更多的CPU期间片。
设置优先级有助于线程布局期确定在下一次性选用哪一个线程来优先口头,设置线程优先级经常使用setPriority()方法
但是,线程优先级并不总是牢靠的,由于Java线程最终是经过映射究竟层操作系统的原生线程来成功的。因此,线程调度依然取决于操作系统。虽然许多操作系统提供了线程优先级的概念,但它们不必定直接对应于Java线程优先级。例如,Solaris领有2,147,483,648(2^32)个优先级级别,而只要7个。假设操作系统的优先级级别多于Java,将它们映射是相对繁难的,可以在它们之间留下一些空位。但是,假设操作系统的优先级级别少于Java,或许会出现多个优先级映射到同一级别的状况。
下图显示了Java线程优先级与Windows线程优先级之间的对应相关,不包括THREAD_PRIORITY_IDLE,由于它在Windows平台的JDK中未经常使用。因此假设在Java程序中对两个线程设置的优先级区分是3和4那么关于Windows来说他们的优先级还是分歧的。还有例如Windows系统中存在一个叫做优先级推动器的性能,大抵作用是当系统发现一个线程被口头的特意频繁的时刻,或许会越过线程优先级去为它调配口头期间,从而缩小线程频繁切换而带来的性能损耗。因此咱们在程序中并不能判别雷同为就绪形态且优先级分歧的多个线程系统会先口头哪一个。
总结
关于任何允许多线程的计算机言语来说,深化了解线程及写好多线程程序,都是一个渺小的应战。本关键简述Java线程与操作系统线程之间的相关。java中的线程和操作系统中的线程区分存在于虚构机和操作系统中,一个Java线程是直接经过一个操作系统线程来成功的。其中还有很多值得深挖的点。大家有兴味的话,可以细心钻研一下。
参考文档
深化了解Java虚构机(第3版)
java多线程理解
线程是系统调度中的最小单位,因为其拥有比进程更小的资源消耗,因此,在进行同类事情,需要进行互相的通讯等等事情的时候,都采用线程来进行处理。 对于只做固定的一件事情(比如:计算1+2+3+...+)来说,其性能上不会比采用单线程的整体效率高,原因是,同时都是要做这么多运算,采用多线程的话,系统在进行线程调度的过程中喙浪费一些资源和时间,从而性能上下降。 那么,多线程是否就没有存在的意义了呢?答案当然不是的。 多线程还是有存在的价值的,我们在写输入流输出流,写网络程序等等的时候,都会出现阻塞的情况,如果说,我们不使用多线程的话,从A中读数据出来的时候,A因为没有准备好,而整个程序阻塞了,其他的任何事情都没法进行。 如果采用多线程的话,你就不用担心这个问题了。 还举个例子:游戏中,如果A角色和B角色采用同一个线程来处理的话,那么,很有可能就会出现只会响应A角色的操作,而B角色就始终被占用了的情况,这样,玩起来肯定就没劲了。 因此,线程是有用的,但也不是随便乱用,乱用的话,可能造成性能的低下,它是有一点的适用范围的,一般我认为:需要响应多个人的事情,从设计上需要考虑同时做一些事情(这些事情很多情况下可能一点关系都没有,也有可能有一些关系的)。 使用多线程的时候,如果某些线程之间涉及到资源共享、互相通讯等等问题的时候,一定得注意线程安全的问题,根据情况看是不是需要使用synchronized关键字。
Java多线程程序设计详细解析
一、理解多线程多线程是这样一种机制,它允许在程序中并发执行多个指令流,每个指令流都称为一个线程,彼此间互相独立。 线程又称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统负责调度,区别在于线程没有独立的存储空间,而是和所属进程中的其它线程共享一个存储空间,这使得线程间的通信远较进程简单。 多个线程的执行是并发的,也就是在逻辑上“同时”,而不管是否是物理上的“同时”。 如果系统只有一个CPU,那么真正的“同时”是不可能的,但是由于CPU的速度非常快,用户感觉不到其中的区别,因此我们也不用关心它,只需要设想各个线程是同时执行即可。 多线程和传统的单线程在程序设计上最大的区别在于,由于各个线程的控制流彼此独立,使得各个线程之间的代码是乱序执行的,由此带来的线程调度,同步等问题,将在以后探讨。 二、在Java中实现多线程我们不妨设想,为了创建一个新的线程,我们需要做些什么?很显然,我们必须指明这个线程所要执行的代码,而这就是在Java中实现多线程我们所需要做的一切!真是神奇!Java是如何做到这一点的?通过类!作为一个完全面向对象的语言,Java提供了类来方便多线程编程,这个类提供了大量的方法来方便我们控制自己的各个线程,我们以后的讨论都将围绕这个类进行。 那么如何提供给 Java 我们要线程执行的代码呢?让我们来看一看 Thread 类。 Thread 类最重要的方法是run(),它为Thread类的方法start()所调用,提供我们的线程所要执行的代码。 为了指定我们自己的代码,只需要覆盖它!方法一:继承 Thread 类,覆盖方法 run(),我们在创建的 Thread 类的子类中重写 run() ,加入线程所要执行的代码即可。 下面是一个例子:public class MyThread extends Thread{int count= 1, number;public MyThread(int num){number = num;(创建线程 + number);}public void run() {while(true) {(线程 + number + :计数 + count);if(++count== 6) return;}}public static void main(String args[]){for(int i = 0;i 〈 5; i++) new MyThread(i+1)();}}这种方法简单明了,符合大家的习惯,但是,它也有一个很大的缺点,那就是如果我们的类已经从一个类继承(如小程序必须继承自 Applet 类),则无法再继承 Thread 类,这时如果我们又不想建立一个新的类,应该怎么办呢?我们不妨来探索一种新的方法:我们不创建Thread类的子类,而是直接使用它,那么我们只能将我们的方法作为参数传递给 Thread 类的实例,有点类似回调函数。 但是 Java 没有指针,我们只能传递一个包含这个方法的类的实例。 那么如何限制这个类必须包含这一方法呢?当然是使用接口!(虽然抽象类也可满足,但是需要继承,而我们之所以要采用这种新方法,不就是为了避免继承带来的限制吗?)Java 提供了接口 来支持这种方法。 方法二:实现 Runnable 接口Runnable接口只有一个方法run(),我们声明自己的类实现Runnable接口并提供这一方法,将我们的线程代码写入其中,就完成了这一部分的任务。 但是Runnable接口并没有任何对线程的支持,我们还必须创建Thread类的实例,这一点通过Thread类的构造函数public Thread(Runnable target);来实现。 下面是一个例子:public class MyThread implements Runnable{int count= 1, number;public MyThread(int num){number = num;(创建线程 + number);}public void run(){while(true){(线程 + number + :计数 + count);if(++count== 6) return;}}public static void main(String args[]){for(int i = 0; i 〈 5;i++) new Thread(new MyThread(i+1))();}}严格地说,创建Thread子类的实例也是可行的,但是必须注意的是,该子类必须没有覆盖 Thread 类的 run 方法,否则该线程执行的将是子类的 run 方法,而不是我们用以实现Runnable 接口的类的 run 方法,对此大家不妨试验一下。 使用 Runnable 接口来实现多线程使得我们能够在一个类中包容所有的代码,有利于封装,它的缺点在于,我们只能使用一套代码,若想创建多个线程并使各个线程执行不同的代码,则仍必须额外创建类,如果这样的话,在大多数情况下也许还不如直接用多个类分别继承 Thread 来得紧凑。 综上所述,两种方法各有千秋,大家可以灵活运用。 下面让我们一起来研究一下多线程使用中的一些问题。 三、线程的四种状态1. 新状态:线程已被创建但尚未执行(start() 尚未被调用)。 2. 可执行状态:线程可以执行,虽然不一定正在执行。 CPU 时间随时可能被分配给该线程,从而使得它执行。 3. 死亡状态:正常情况下 run() 返回使得线程死亡。 调用 stop()或 destroy() 亦有同样效果,但是不被推荐,前者会产生异常,后者是强制终止,不会释放锁。 4. 阻塞状态:线程不会被分配 CPU 时间,无法执行。 四、线程的优先级线程的优先级代表该线程的重要程度,当有多个线程同时处于可执行状态并等待获得 CPU 时间时,线程调度系统根据各个线程的优先级来决定给谁分配 CPU 时间,优先级高的线程有更大的机会获得 CPU 时间,优先级低的线程也不是没有机会,只是机会要小一些罢了。 你可以调用 Thread 类的方法 getPriority() 和 setPriority()来存取线程的优先级,线程的优先级界于1(MIN_PRIORITY)和10(MAX_PRIORITY)之间,缺省是5(NORM_PRIORITY)。 五、线程的同步由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。 Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。 由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块。 1. synchronized 方法:通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。 如:public synchronized void accessVal(int newVal);synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。 这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。 在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问。 synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。 当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。 2. synchronized 块:通过 synchronized关键字来声明synchronized 块。 语法如下:synchronized(syncObject){//允许访问控制的代码}#p#副标题#e#synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。 由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。 六、线程的阻塞为了解决对共享存储区的访问冲突,Java 引入了同步机制,现在让我们来考察多个线程对共享资源的访问,显然同步机制已经不够了,因为在任意时刻所要求的资源不一定已经准备好了被访问,反过来,同一时刻准备好了的资源也可能不止一个。 为了解决这种情况下的访问控制问题,Java 引入了对阻塞机制的支持。 阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操作系统的同学对它一定已经很熟悉了。 Java 提供了大量方法来支持阻塞,下面让我们逐一分析。 1. sleep() 方法:sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。 典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止。 2. suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。 典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。 3. yield() 方法:yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。 调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。 4. wait() 和 notify() 方法:两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。 初看起来它们与 suspend() 和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。 区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方法则相反。 上述的核心区别导致了一系列的细节上的区别。 首先,前面叙述的所有方法都隶属于 Thread 类,但是这一对却直接隶属于 Object 类,也就是说,所有对象都拥有这一对方法。 初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。 而调用 任意对象的notify()方法则导致因调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。 其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。 同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。 因此,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。 若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。 wait() 和 notify() 方法的上述特性决定了它们经常和synchronized 方法或块一起使用,将它们和操作系统的进程间通信机制作一个比较就会发现它们的相似性:synchronized方法或块提供了类似于操作系统原语的功能,它们的执行不会受到多线程机制的干扰,而这一对方法则相当于 block 和wakeup 原语(这一对方法均声明为 synchronized)。 它们的结合使得我们可以实现操作系统上一系列精妙的进程间通信的算法(如信号量算法),并用于解决各种复杂的线程间通信问题。 关于 wait() 和 notify() 方法最后再说明两点:第一:调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。 第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。 当然,只有获得锁的那一个线程才能进入可执行状态。 谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定超时期限的 wait() 方法的调用都可能产生死锁。 遗憾的是,Java 并不在语言级别上支持死锁的避免,我们在编程中必须小心地避免死锁。 以上我们对 Java 中实现线程阻塞的各种方法作了一番分析,我们重点分析了 wait() 和 notify()方法,因为它们的功能最强大,使用也最灵活,但是这也导致了它们的效率较低,较容易出错。 实际使用中我们应该灵活使用各种方法,以便更好地达到我们的目的。 七、守护线程守护线程是一类特殊的线程,它和普通线程的区别在于它并不是应用程序的核心部分,当一个应用程序的所有非守护线程终止运行时,即使仍然有守护线程在运行,应用程序也将终止,反之,只要有一个非守护线程在运行,应用程序就不会终止。 守护线程一般被用于在后台为其它线程提供服务。 可以通过调用方法 isDaemon() 来判断一个线程是否是守护线程,也可以调用方法 setDaemon() 来将一个线程设为守护线程。 八、线程组线程组是一个 Java 特有的概念,在 Java 中,线程组是类ThreadGroup 的对象,每个线程都隶属于唯一一个线程组,这个线程组在线程创建时指定并在线程的整个生命期内都不能更改。 你可以通过调用包含 ThreadGroup 类型参数的 Thread 类构造函数来指定线程属的线程组,若没有指定,则线程缺省地隶属于名为 system 的系统线程组。 在 Java 中,除了预建的系统线程组外,所有线程组都必须显式创建。 在 Java 中,除系统线程组外的每个线程组又隶属于另一个线程组,你可以在创建线程组时指定其所隶属的线程组,若没有指定,则缺省地隶属于系统线程组。 这样,所有线程组组成了一棵以系统线程组为根的树。 Java 允许我们对一个线程组中的所有线程同时进行操作,比如我们可以通过调用线程组的相应方法来设置其中所有线程的优先级,也可以启动或阻塞其中的所有线程。 Java 的线程组机制的另一个重要作用是线程安全。 线程组机制允许我们通过分组来区分有不同安全特性的线程,对不同组的线程进行不同的处理,还可以通过线程组的分层结构来支持不对等安全措施的采用。 Java 的 ThreadGroup 类提供了大量的方法来方便我们对线程组树中的每一个线程组以及线程组中的每一个线程进行操作。 九、总结在本文中,我们讲述了 Java 多线程编程的方方面面,包括创建线程,以及对多个线程进行调度、管理。 我们深刻认识到了多线程编程的复杂性,以及线程切换开销带来的多线程程序的低效性,这也促使我们认真地思考一个问题:我们是否需要多线程?何时需要多线程?多线程的核心在于多个代码块并发执行,本质特点在于各代码块之间的代码是乱序执行的。 我们的程序是否需要多线程,就是要看这是否也是它的内在特点。 假如我们的程序根本不要求多个代码块并发执行,那自然不需要使用多线程;假如我们的程序虽然要求多个代码块并发执行,但是却不要求乱序,则我们完全可以用一个循环来简单高效地实现,也不需要使用多线程;只有当它完全符合多线程的特点时,多线程机制对线程间通信和线程管理的强大支持才能有用武之地,这时使用多线程才是值得的。 #p#副标题#e#
免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。