查看原文
其他

说说Java的线程

2017-11-18 IT哈哈

线程是一个独立执行的调用序列,同一个进程的线程在同一时刻共享一些系统资源(比如文件句柄等)也能访问同一个进程所创建的对象资源(内存资源)。java.lang.Thread对象负责统计和控制这种行为。


每个程序都至少拥有一个线程-即作为Java虚拟机(JVM)启动参数运行在主类main方法的线程。在Java虚拟机初始化过程中也可能启动其他的后台线程。这种线程的数目和种类因JVM的实现而异。然而所有用户级线程都是显式被构造并在主线程或者是其他用户线程中被启动。


这里对Thread类中的主要方法和属性以及一些使用注意事项作出总结。这些内容会在这本书(《Java Concurrency Constructs》)上进行进一步的讨论阐述。Java语言规范以及已发布的API文档中都会有更详细权威的描述。


构造方法

Thread类中不同的构造方法接受如下参数的不同组合:

  • 一个Runnable对象,这种情况下,Thread.start方法将会调用对应Runnable对象的run方法。如果没有提供Runnable对象,那么就会立即得到一个Thread.run的默认实现。

  • 一个作为线程标识名的String字符串,该标识在跟踪和调试过程中会非常有用,除此别无它用。

  • 线程组(ThreadGroup),用来放置新创建的线程,如果提供的ThreadGroup不允许被访问,那么就会抛出一个SecurityException 。


Thread类本身就已经实现了Runnable接口,因此,除了提供一个用于执行的Runnable对象作为构造参数的办法之外,也可以创建一个Thread的子类,通过重写其run方法来达到同样的效果。然而,比较好的实践方法却是分开定义一个Runnable对象并用来作为构造方法的参数。将代码分散在不同的类中使得开发人员无需纠结于Runnable和Thread对象中使用的同步方法或同步块之间的内部交互。更普遍的是,这种分隔使得对操作的本身与其运行的上下文有着独立的控制。更好的是,同一个Runnable对象可以同时用来初始化其他的线程,也可以用于构造一些轻量化的执行框架(Executors)。另外需要提到的是通过继承Thread类实现线程的方式有一个缺点:使得该类无法再继承其他的类。


Thread对象拥有一个守护(daemon)标识属性,这个属性无法在构造方法中被赋值,但是可以在线程启动之前设置该属性(通过setDaemon方法)。当程序中所有的非守护线程都已经终止,调用setDaemon方法可能会导致虚拟机粗暴的终止线程并退出。isDaemon方法能够返回该属性的值。守护状态的作用非常有限,即使是后台线程在程序退出的时候也经常需要做一些清理工作。(daemon的发音为”day-mon”,这是系统编程传统的遗留,系统守护进程是一个持续运行的进程,比如打印机队列管理,它总是在系统中运行。)


启动线程

调用start方法会触发Thread实例以一个新的线程启动其run方法。新线程不会持有调用线程的任何同步锁。


当一个线程正常地运行结束或者抛出某种未检测的异常(比如,运行时异常(RuntimeException),错误(ERROR) 或者其子类)线程就会终止。当线程终止之后,是不能被重新启动的。在同一个Thread上调用多次start方法会抛出InvalidThreadStateException异常。


如果线程已经启动但是还没有终止,那么调用isAlive方法就会返回true.即使线程由于某些原因处于阻塞(Blocked)状态该方法依然返回true。如果线程已经被取消(cancelled),那么调用其isAlive在什么时候返回false就因各Java虚拟机的实现而异了。没有方法可以得知一个处于非活动状态的线程是否已经被启动过了(译者注:即线程在开始运行前和结束运行后都会返回false,你无法得知处于false的线程具体的状态)。另一点,虽然一个线程能够得知同一个线程组的其他线程的标识,但是却无法得知自己是由哪个线程调用启动的。


优先级

Java虚拟机为了实现跨平台(不同的硬件平台和各种操作系统)的特性,Java语言在线程调度与调度公平性上未作出任何的承诺,甚至都不会严格保证线程会被执行。但是Java线程却支持优先级的方法,这些方法会影响线程的调度:

每个线程都有一个优先级,分布在Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间(分别为1和10)
默认情况下,新创建的线程都拥有和创建它的线程相同的优先级。main方法所关联的初始化线程拥有一个默认的优先级,这个优先级是Thread.NORM_PRIORITY (5).

线程的当前优先级可以通过getPriority方法获得。
线程的优先级可以通过setPriority方法来动态的修改,一个线程的最高优先级由其所在的线程组限定。


当可运行的线程数超过了可用的CPU数目的时候,线程调度器更偏向于去执行那些拥有更高优先级的线程。具体的策略因平台而异。比如有些Java虚拟机实现总是选择当前优先级最高的线程执行。有些虚拟机实现将Java中的十个优先级映射到系统所支持的更小范围的优先级上,因此,拥有不同优先级的线程可能最终被同等对待。还有些虚拟机会使用老化策略(随着时间的增长,线程的优先级逐渐升高)动态调整线程优先级,另一些虚拟机实现的调度策略会确保低优先级的线程最终还是能够有机会运行。设置线程优先级可以影响在同一台机器上运行的程序之间的调度结果,但是这不是必须的。


线程优先级对语义和正确性没有任何的影响。特别是,优先级管理不能用来代替锁机制。优先级仅仅是用来表明哪些线程是重要紧急的,当存在很多线程在激励进行CPU资源竞争的情况下,线程的优先级标识将会显得非常有用。比如,在ParticleApplet中将particle animation线程的优先级设置的比创建它们的applet线程低,在某些系统上能够提高对鼠标点击的响应,而且不会对其他功能造成影响。但是即使setPriority方法被定义为空实现,程序在设计上也应该保证能够正确执行(尽管可能会没有响应)。


下面这个表格列出不同类型任务在线程优先级设定上的通常约定。在很多并发应用中,在任一指定的时间 49 31012 49 15231 0 0 4167 0 0:00:07 0:00:03 0:00:04 4167上,只有相对较少的线程处于可执行的状态(另外的线程可能由于各种原因处于阻塞状态),在这种情况下,没有什么理由需要去管理线程的优先级。另一些情况下,在线程优先级上的调整可能会对并发系统的调优起到一些作用。

范围  用途
10      Crisis management(应急处理)
7-9    Interactive, event-driven(交互相关,事件驱动)
4-6    IO-bound(IO限制类)
2-3    Background computation(后台计算)
1        Run only if nothing else can(仅在没有任何线程运行时运行的)


控制方法

只有很少几个方法可以用于跨线程交流:

  • 每个线程都有一个相关的Boolean类型的中断标识。在线程t上调用t.interrupt会将该线程的中断标识设为true,除非线程t正处于Object.wait,Thread.sleep,或者Thread.join,这些情况下interrupt调用会导致t上的这些操作抛出InterruptedException异常,但是t的中断标识会被设为false。

  • 任何一个线程的中断状态都可以通过调用isInterrupted方法来得到。如果线程已经通过interrupt方法被中断,这个方法将会返回true。

  • 但是如果调用了Thread.interrupted方法且中断标识还没有被重置,或者是线程处于wait,sleep,join过程中,调用isInterrupted方法将会抛出InterruptedException异常。调用t.join()方法将会暂停执行调用线程,直到线程t执行完毕:当t.isAlive()方法返回false的时候调用t.join()将会直接返回(return)。另一个带参数毫秒(millisecond)的join方法在被调用时,如果线程没能够在指定的时间内完成,调用线程将重新得到控制权。因为isAlive方法的实现原理,所以在一个还没有启动的线程上调用join方法是没有任何意义的。同样的,试图在一个还没有创建的线程上调用join方法也是不明智的。


起初,Thread类还支持一些另外一些控制方法:suspend,resume,stop以及destroy。这几个方法已经被声明过期。其中destroy方法从来没有被实现,估计以后也不会。


静态方法

Thread类中的部分方法被设计为只适用于当前正在运行的线程(即调用Thread方法的线程)。为强调这点,这些方法都被声明为静态的。


  • Thread.currentThread方法会返回当前线程的引用,得到这个引用可以用来调用其他的非静态方法,比如Thread.currentThread().getPriority()会返回调用线程的优先级。

  • Thread.interrupted方法会清除当前线程的中断状态并返回前一个状态。(一个线程的中断状态是不允许被其他线程清除的)

  • Thread.sleep(long msecs)方法会使得当前线程暂停执行至少msecs毫秒。


Thread.yield方法纯粹只是建议Java虚拟机对其他已经处于就绪状态的线程(如果有的话)调度执行,而不是当前线程。最终Java虚拟机如何去实现这种行为就完全看其喜好了。


尽管缺乏保障,但在不支持分时间片/可抢占式的线程调度方式的单CPU的Java虚拟机实现上,yield方法依然能够起到切实的作用。在这种情况下,线程只在被阻塞的情况下(比如等待IO,或是调用了sleep等)才会进行重新调度。在这些系统上,那些执行非阻塞的耗时的计算任务的线程就会占用CPU很长的时间,最终导致应用的响应能力降低。如果一个非阻塞的耗时计算线程会导致时间处理线程或者其他交互线程超出可容忍的限度的话,就可以在其中插入yield操作(或者是sleep),使得具有较低线程优先级的线程也可以执行。为了避免不必要的影响,你可以只在偶然间调用yield方法,比如,可以在一个循环中插入如下代码:if (Math.random() < 0.01) Thread.yield();


在支持可抢占式调度的Java虚拟机实现上,线程调度器忽略yield操作可能是最完美的策略,特别是在多核处理器上。


线程组

每一个线程都是一个线程组中的成员。默认情况下,新建线程和创建它的线程属于同一个线程组。线程组是以树状分布的。当创建一个新的线程组,这个线程组成为当前线程组的子组。getThreadGroup方法会返回当前线程所属的线程组,对应地,ThreadGroup类也有方法可以得到哪些线程目前属于这个线程组,比如enumerate方法。


ThreadGroup类存在的一个目的是支持安全策略来动态的限制对该组的线程操作。比如对不属于同一组的线程调用interrupt是不合法的。这是为避免某些问题(比如,一个applet线程尝试杀掉主屏幕的刷新线程)所采取的措施。ThreadGroup也可以为该组所有线程设置一个最大的线程优先级。


线程组往往不会直接在程序中被使用。在大多数的应用中,如果仅仅是为在程序中跟踪线程对象的分组,那么普通的集合类(比如java.util.Vector)应是更好的选择。


在ThreadGroup类为数不多的几个方法中,uncaughtException方法却是非常有用的,当线程组中的某个线程因抛出未检测的异常(比如空指针异常NullPointerException)而中断的时候,调用这个方法可以打印出线程的调用栈信息。


来源:并发编程网

作者:Doug Lea 

译者:萧欢  

校对:方腾飞



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存