Doug Lea-Java并发结构

前言

java并发结构讲述了Java的并发的基本概念和组件,是java并发的基本对象。原文请参考原文

线程

线程是独立于其他线程执行的调用序列,同时可能共享底层系统资源(如文件),以及访问在同一程序中构造的其他对象(参见§1.2.2)。java.lang.Thread对象维护此活动的簿记和控制。

每个程序至少由一个线程组成——这个线程运行类的主方法,作为Java虚拟机(“JVM”)的启动参数。其他内部后台线程也可以在JVM初始化期间启动。这些线程的数量和性质在不同的JVM实现中有所不同。但是,所有用户级线程都是显式构造的,并从主线程开始,或者从它们依次创建的任何其他线程开始。

下面是对Thread类的主要方法和属性的总结,以及一些用法说明。在本书中,他们被进一步讨论和说明。应该参考JavaTM语言规范(“JLS”)和已发布的API文档,以获得更详细和权威的描述。

构造

不同的线程构造函数接受参数的组合提供:

  • 一个可运行的对象,在这种情况下是一个后续的Thread.start调用提供的Runnable对象的run。如果没有提供Runnable,则立即返回Thread.run的默认实现。
  • 用作线程标识符的字符串。这对于跟踪和调试非常有用,但是没有其他作用。
  • 新线程应该放在其中的ThreadGroup。如果不允许访问ThreadGroup,则抛出SecurityException。

类线程本身实现了Runnable。因此,不需要在Runnable中提供要运行的代码并将其用作线程构造函数的参数,而是可以创建一个线程子类,它覆盖run方法来执行所需的操作。然而,最佳的默认策略是将Runnable定义为一个单独的类,并在线程构造函数中提供它。将代码隔离在一个不同的类中,可以使您不必担心在Runnable中使用的同步方法或块与类Thread的方法可能使用的任何块之间的任何潜在交互。更一般地说,这种分离允许对操作的性质和运行它的上下文进行独立控制:可以将相同的Runnable提供给以不同方式初始化的线程,也可以提供给其他轻量级执行器(见4.1.4)。还要注意,子类化Thread会阻止一个类子类化任何其他类。

线程对象还拥有一个守护进程状态属性,该属性不能通过任何线程构造函数设置,但可以仅在线程启动之前设置。setDaemon方法断言,只要程序中所有其他非守护进程线程已经终止,JVM就可能退出,并突然终止线程。isDaemon方法返回状态。守护进程状态的效用非常有限。甚至后台线程也常常需要在程序退出时进行一些清理。(daemon的拼写,通常读作“day-mon”,是系统编程传统的遗物。系统守护进程是连续的进程,例如打印队列管理器,它“始终”存在于系统中。)

启动线程

调用其start方法会导致类Thread的实例将其run方法初始化为独立的活动。调用方线程持有的同步锁没有一个被新线程持有(参见§2.2.1)。

当一个线程的run方法完成时,它要么正常返回,要么抛出一个未检查的异常(即,RuntimeException、Error或它们的一个子类)。线程是不可重新启动的,即使在它们终止之后也是如此。多次调用start会导致InvalidThreadStateException。

如果线程已经启动,但尚未终止,则isAlive方法返回true。如果线程只是以某种方式被阻塞,那么它将返回true。已知的JVM实现在isAlive为已取消的线程返回false的确切时间点上是不同的(参见§3.1.2)。没有方法告诉您是否已经启动了非活动线程。另外,一个线程不能很容易地确定是哪个其他线程启动了它,尽管它可以确定ThreadGroup中其他线程的身份(参见§1.1.2.6)。

优先级

为了能够跨不同的硬件平台和操作系统实现Java虚拟机,Java编程语言没有承诺调度或公平性,甚至没有严格保证线程向前进展(参见§3.4.1.5)。但线程支持优先级方法,启发式地影响调度程序:

  • 每个线程都有一个优先级,范围在线程之间。MIN_PRIORITY和线程。MAX_PRIORITY(分别定义为1和10)。
  • 默认情况下,每个新线程都具有与创建它的线程相同的优先级。默认情况下,与主线程关联的初始线程具有优先级线程。NORM_PRIORITY (5)。
  • 任何线程的当前优先级都可以通过方法getPriority来访问。
  • 任何线程的优先级都可以通过setPriority方法动态更改。线程允许的最大优先级由它的ThreadGroup限制。

当可运行的线程(参见1.3.2)比可用的cpu更多时,调度器通常倾向于运行优先级更高的线程。确切的策略可能也确实会因平台而异。例如,一些JVM实现总是选择当前优先级最高的线程(任意打破连结)。一些JVM实现将这10个线程优先级映射到更少的系统支持的类别中,因此具有不同优先级的线程可能被平等对待。还有一些将声明的优先级与旧的方案或其他调度策略混合在一起,以确保即使是低优先级的线程最终也有机会运行。此外,设置优先级可能会(但不必)影响在同一计算机系统上运行的其他程序的调度。

优先级与语义或正确性没有其他关系(见1.3)。特别是,不能使用优先级操作来代替锁定。优先级只能用来表示不同线程的相对重要性或迫切性,当线程之间有大量争用试图获得执行机会时,这些优先级指示将非常有用。例如,将ParticleApplet中粒子动画线程的优先级设置为低于构建它们的applet线程的优先级,可能会在某些系统上提高对鼠标点击的响应性,至少不会损害其他系统的响应性。但是,即使setPriority被定义为无操作,程序也应该设计为正确运行(尽管可能没有响应)。(类似的备注适用于收益率;参见1.1.2.5)。

下表给出了一组将任务类别链接到优先级设置的通用约定。在许多并发应用程序中,在任何给定时间都可以运行的线程相对较少(其他线程都以某种方式被阻塞),在这种情况下,几乎没有理由操纵优先级。在其他情况下,优先级设置中的细微调整可能在并发系统的最终调优中起很小的作用。

范围用途
10危机管理
7-9互动的、事件驱动的
4-6受输入输出限制的
2-3后台计算
1只有在没有其他可以运行时才运行

控制方法

只有少数几个方法可以跨线程通信:

  • 每个线程都有一个相关的布尔中断状态(参见§3.1.2)。为某个线程t调用t.interrupt会将t的中断状态设置为true,除非线程t正在处理Object.wait,Thread.sleep,或Thread.join;在本例中,中断导致这些操作(在t中)抛出InterruptedException,但是t的中断状态设置为false。
  • 任何线程的中断状态都可以用isInterrupted方法来检测。如果线程通过interrupt方法被中断,但是状态没有通过调用线程的Thread.interrupted(参见§1.1.2.5),或在等待、睡眠或连接的过程中抛出InterruptedException重置,则此方法返回true。
  • 为线程t调用t.join()将挂起调用者,直到目标线程t完成:当t.isalive()为false时返回对t.join()的调用(参见§4.3.2)。具有(毫秒)时间参数的版本即使线程没有在指定的时间限制内完成,也会返回控制权。由于isAlive是如何定义的,所以在尚未启动的线程上调用join毫无意义。出于类似的原因,尝试连接一个您没有创建的线程是不明智的。

最初,类线程支持附加的控制方法suspend、resume、stop和destroy。方法挂起、恢复和停止已被弃用;方法销毁从未在任何版本中实现过,可能也永远不会实现。使用§3.2中讨论的等待和通知技术,可以更安全可靠地获得方法挂起和恢复的效果。有关stop的问题在§3.1.2.3中进行了讨论。

静态方法

一些线程类方法被设计为只应用于当前正在运行的线程(即,调用Thread方法的线程)。为了加强这一点,这些方法被声明为静态的。

  • Thread.currentThread返回对当前线程的引用。然后可以使用这个引用来调用其他(非静态)方法。例如,Thread.currentThread().getPriority()返回执行调用的线程的优先级。
  • Thread.interrupted清除当前线程的中断状态,并返回以前的状态。(因此,一个线程的中断状态不能从其他线程中清除。)
  • Thread.sleep(long msecs)导致当前线程至少挂起msecs毫秒(参见§3.2.2)。

Thread.yield是一个纯粹的启发式提示,它告诉JVM,如果有任何其他可运行但不运行的线程,调度器应该运行一个或多个线程,而不是当前线程。JVM可以以它喜欢的任何方式解释这个提示。

尽管缺乏保证,但是yield在一些不使用分时抢占式调度的单cpu JVM实现上是实用有效的(见1.2.2)。在这种情况下,仅当一个线程阻塞时(例如在IO上或通过sleep)才重新调度线程。在这些系统上,执行耗时的非阻塞计算的线程可能会占用CPU很长时间,从而降低应用程序的响应能力。作为一种保护措施,执行非阻塞计算的方法可能会超过事件处理程序或其他反应线程可接受的响应时间,这些方法可以插入yield(甚至可能是sleep),如果需要,还可以以较低的优先级设置运行。为了最小化不必要的影响,您可以安排仅在偶然情况下调用yield;例如,一个循环可能包含:

if (Math.random() < 0.01) Thread.yield();

在采用抢占式调度策略的JVM实现上,尤其是在多处理器上,调度程序可能甚至希望直接忽略yield提供的这个提示。

ThreadGroups

每个线程都被构造为ThreadGroup的成员,默认情况下,与为其发出构造函数的线程所在的组相同。ThreadGroups以类似树的方式嵌套。当一个对象构造一个新的ThreadGroup时,它被嵌套在当前的组下。方法getThreadGroup返回任何线程的组。ThreadGroup类反过来又支持枚举等方法,这些方法指示哪些线程当前在该组中。

类ThreadGroup的一个目的是支持动态限制对线程操作访问的安全策略;例如,要使中断不在组中的线程成为非法。这是一组保护措施的一部分,以防止可能发生的问题,例如,如果applet试图杀死主屏幕显示更新线程。threadgroup还可以对任何成员线程可以拥有的最大优先级设置上限。

ThreadGroups一般不直接用于基于线程的程序。在大多数应用程序中,常规的集合类(例如java.util.Vector)是跟踪线程对象组的更好选择,以满足应用程序相关的目的。

在并发程序中经常使用的几个ThreadGroup方法中,有一个是uncaughtException方法,当组中的一个线程由于未捕获的未检查异常(例如NullPointerException)而终止时,就会调用这个方法。此方法通常会打印堆栈跟踪。

同步

对象和锁

类对象及其子类的每个实例都拥有一个锁。类型int、float等的标量不是对象。只能通过其封闭对象锁定标量字段。个别字段不能标记为同步。锁定只能应用于方法中的字段。但是,正如§2.2.7.4中所述,字段可以声明为volatile,这将影响围绕其使用的原子性、可见性和排序属性。

类似地,持有标量元素的数组对象拥有锁,但是它们各自的标量元素没有锁。(此外,没有办法将数组元素声明为volatile。)锁定一个对象数组并不会自动锁定它的所有元素。没有在单个原子操作中同时锁定多个对象的构造。

类实例是对象。如下所述,与类对象关联的锁在静态同步方法中使用。

同步方法和块

有两种基于synchronized关键字的语法形式:块和方法。块同步采用要锁定的对象的参数。这允许任何方法锁定任何对象。同步块最常见的参数是这样的。

块同步被认为比方法同步更基本。声明:

synchronized void f() { /* body */ }
is equivalent to:
void f() { synchronized(this) { /* body */ } }

synchronized关键字不被认为是方法签名的一部分。因此,当子类重写超类方法时,synchronized修饰符不会自动继承,而且接口中的方法不能声明为synchronized。另外,构造函数不能被限定为synchronized(尽管可以在构造函数中使用块同步)。

子类中的同步实例方法使用与其超类中的同步实例方法相同的锁。但是内部类方法中的同步是独立于其外部类的。但是,非静态内部类方法可以通过以下代码块锁定其包含的类,例如OuterClass:

synchronized(OuterClass.this) { /* body */ }.

获取和释放锁

锁定遵循一个内置的默认释放协议,仅通过使用synchronized关键字来控制。所有的锁定都是块结构的。在进入同步方法或块时获取锁,在退出时释放锁,即使退出是由于异常而发生的。你不能忘记打开锁。

锁是按线程而不是按调用操作的。如果锁是空闲的,或者线程已经拥有锁,则执行同步的线程将通过,否则将阻塞。(这种可重入或递归锁定与POSIX线程中使用的默认策略不同。)在其他效果中,这允许一个同步方法对同一对象上的另一个同步方法进行自调用,而不会冻结。

一个同步的方法或块只对同一目标对象上的其他同步的方法和块遵守默认释放协议。不同步的方法仍然可以在任何时候执行,即使同步的方法正在进行中。换句话说,synchronized并不等同于原子性,但是可以使用同步来实现原子性。

当一个线程释放一个锁时,另一个线程可能会获得它(可能是同一个线程,如果它碰到另一个同步方法)。但是不能保证任何被阻塞的线程中的哪一个会在下一个或什么时候获得锁。(特别是,没有公平的保证,参见§3.4.1.5。)没有一种机制可以发现某个线程是否持有某个给定的锁。

正如§2.2.7中所讨论的,除了控制锁之外,synchronized还具有同步底层内存系统的副作用。

静态

锁定一个对象并不会自动保护对该对象的类或其任何超类的静态字段的访问。相反,通过同步的静态方法和块来保护对静态字段的访问。静态同步使用与声明静态方法所在的类相关联的类对象所拥有的锁。类C的静态锁也可以通过以下方法在实例方法中访问:

synchronized(C.class) { /* body */ }

与每个类关联的静态锁与任何其他类(包括它的超类)的静态锁无关。在试图保护超类中声明的静态字段的子类中添加新的静态同步方法是无效的。而是使用显式块版本。

使用这种形式的结构也是不好的做法:

synchronized(getClass()) { /* body */ } // Do not use

这将锁定实际的类,它可能不同于定义需要保护的静态字段的类的(子类)。

在类装入和初始化期间,JVM从内部获取并释放类对象的锁。除非您正在编写一个特殊的类装入器,或者在静态初始化序列期间持有多个锁,否则这些内部机制无法干扰对类对象上同步的普通方法和块的使用。没有其他内部JVM操作能够独立地获取您创建和使用的类的任何对象的任何锁。然而,如果你子类化java.*类,您应该了解这些类中使用的锁定策略。

监视器

就像每个对象都有一个锁一样(参见§2.2.1),每个对象都有一个等待集,该等待集仅由wait、notify、notifyAll和Thread.interrupt方法操作。同时拥有锁和等待集的实体通常称为监视器(尽管几乎每种语言对细节的定义都有所不同)。任何对象都可以用作监视器。

每个对象的等待集由JVM在内部维护。每个集合持有对象上的等待阻塞的线程,直到相应的通知被调用或等待被释放。

由于等待集与锁的交互方式,仅当在其目标上持有同步锁时,才可以调用wait、notify和notifyAll方法。合规性通常无法在编译时验证。如果不遵从,将导致这些操作在运行时抛出IllegalMonitorStateException。

这些方法的作用如下:

等待

等待调用会导致以下操作:

  • 如果当前线程被中断,则该方法立即退出,抛出InterruptedException。否则,当前线程将被阻塞。
  • JVM将线程放置在与目标对象关联的内部和其他不可访问的等待集中。
  • 释放目标对象的同步锁,但保留线程持有的所有其他锁。即使是在目标对象上嵌套的同步调用所持有的可重入锁,也可以获得完整的版本。稍后恢复时,锁状态将完全恢复。

通知

通知调用会导致以下操作:

  • 如果存在一个线程,则JVM将从与目标对象关联的内部等待集中删除一个任意选择的线程,比如T。当等待集包含多个线程时,不能保证选择哪个等待线程——参见§3.4.1.5。
  • T必须重新获得目标对象的同步锁,这将始终导致它阻塞至少直到调用notify的线程释放锁。如果其他线程先获得锁,它将继续阻塞。
  • 然后从它的等待点恢复T。

通知所有

notifyAll的工作方式与notify相同,不同之处在于,对于对象的等待集中的所有线程,这些步骤同时发生(实际上同时发生)。但是,因为它们必须获得锁,所以线程一次只能继续一个。

中断

如果线程在等待中被挂起时调用Thread.interrupt,则应用相同的通知机制,但在重新获取锁之后,该方法将抛出InterruptedException,并将线程的中断状态设置为false。如果中断和通知几乎同时发生,则无法保证哪个操作具有优先级,因此两种结果都是可能的。(未来对JLS的修订可能会引入对这些结果的确定性保证。)

时控的等待

wait方法的定时版本wait(long msecs)和wait(long msecs, int nanosecs)使用参数指定所需的最大等待时间。它们的操作方式与非定时版本相同,但如果在时间限制之前没有通知等待,则会自动释放它。没有状态指示区分通过通知返回的等待和超时。与直觉相反,wait(0)和wait(0,0)都有一个特殊的含义,它与普通的非限时wait()是等价的。

由于线程争用、调度策略和计时器粒度等原因,定时等待可能在请求绑定之后恢复任意数量的时间。(粒度没有保证。大多数JVM实现都观察到,对于参数小于1ms的情况,响应时间在1-20ms范围内。)

Thread.sleep(long msecs)方法使用一个定时等待,但不占用当前对象的同步锁。它的作用就好像它被定义为:

    if (msecs != 0)  {
      Object s = new Object(); 
      synchronized(s) { s.wait(msecs); }
    }

当然,系统不需要以这种方式实现睡眠。注意sleep(0)暂停至少没有时间,不管这意味着什么。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值