八、探索基本 API:第二部分

标准类库的 java.lang 包提供了许多基本的 API,这些 API 是为了支持语言特性而设计的。在前一章中,你已经遇到了数学、字符串管理和包的 API。在这一章中,我将向你介绍那些与基本类型包装类、线程和系统功能相关的基本库 API。

探索原始类型包装类

java.lang 包包括布尔、字节、字符、双精度、浮点、整数、长、短。这些类被称为原始类型包装类,因为它们的实例将自己包装在原始类型的值周围。

注意原始类型包装类也被称为值类

Java 提供这八个基本类型包装类有两个原因:

  • 集合框架(在第 9 章中讨论)提供了只能存储对象的列表、集合和映射;它们不能存储原始类型的值。您将基元类型值存储在基元类型包装类实例中,并将该实例存储在集合中。
  • 这些类提供了一个很好的地方来将有用的常量(如 MAX_VALUE 和 MIN_VALUE )和类方法(如 Integer 的 parseInt() 方法和 Character 的 isDigit() 、 isLetter() 和 toUpperCase() 方法)与原语类型关联起来。

在这一节中,我将向您介绍每一个原始类型包装类和一个名为号的 java.lang 类。

布尔值

Boolean 是最小的原始类型包装类。这个类声明了三个常量,包括真和假,它们表示预先创建的布尔对象。它还声明了一对用于初始化一个布尔对象的构造函数:

  • 布尔(布尔值)将布尔对象初始化为值。
  • 布尔(字符串 s) 将的文本转换为真或假值,并将该值存储在布尔对象中。

第二个构造函数将的值与真值进行比较。因为比较不区分大小写,所以这四个字母的任何大写/小写组合(例如 true 、 true 或 tRue )都会导致 TRUE 被存储在对象中。否则,构造函数在对象中存储 false。

注意 Boolean 的构造函数由 Boolean value()补充,返回包装后的布尔值。

Boolean 还声明或覆盖以下方法:

  • int compareTo(布尔 b) 将当前布尔对象与 b 进行比较,以确定它们的相对顺序。当当前对象包含与 b 相同的布尔值时,该方法返回 0;当当前对象包含 true 且 b 包含 false 时,该方法返回正值;当当前对象包含 false 且 b 包含 true 时,该方法返回负值。
  • Boolean equals(Object o)将当前 Boolean 对象与 o 进行比较,当 o 不为 null, o 类型为 Boolean ,且两个对象包含相同的布尔值时,返回 true。
  • 静态布尔 getBoolean(字符串名) 当由名标识的系统属性(本章稍后讨论)存在且等于 true 时,返回 true。
  • int hashCode() 返回一个合适的哈希码,允许布尔对象用于基于哈希的集合(在第 9 章中讨论)。
  • 静态布尔解析布尔(String s) 解析 s ,当 s 等于【true】【TRUE】【TRUE】或任何其他大写/小写组合时返回 TRUE。否则,此方法返回 false。(解析将一个字符序列分解成有意义的成分,称为记号。)
  • String toString() 当当前布尔实例包含 true 时,返回【true】;否则,该方法返回“false”。
  • 静态字符串 toString(布尔 b) 当 b 包含 true 时,返回“true”;否则,该方法返回“false”。
  • 静态布尔值 Of(boolean b) 当 b 包含真时返回真当 b 包含假时返回假。
  • 静态布尔值 Of(String s) 当 s 等于“真”、“真”、“真”或任何其他大写/小写组合时,返回真。否则,该方法返回假。

注意初入布尔类的人经常认为 getBoolean() 返回一个布尔对象的真/假值。然而, getBoolean() 返回基于布尔值的系统属性的值——我将在本章后面讨论系统属性。如果需要返回一个 Boolean 对象的真/假值,请使用 booleanValue() 方法 来代替。

使用真和假通常比创建布尔对象更好。例如,假设您需要一个方法,当该方法的 double 参数为负时,该方法返回包含 true 的布尔对象,当该参数为零或正时,该方法返回 false。你可以像下面这样声明你的方法 isNegative() 方法 :

Boolean isNegative(double d)
{
   return new Boolean(d < 0);
}

尽管这个方法很简洁,但它不必要地创建了一个布尔对象。当频繁调用该方法时,会创建许多消耗堆空间的布尔对象。当堆空间不足时,垃圾收集器会运行并降低应用的速度,从而影响性能。

下面的例子揭示了一种更好的编码方式 isNegative() :

Boolean isNegative(double d)
{
   return (d < 0) ? Boolean.TRUE : Boolean.FALSE;
}

该方法通过返回预先创建的真或假对象来避免创建布尔对象。

提示你应该努力创建尽可能少的对象。您的应用不仅内存占用更少,而且性能更好,因为垃圾收集器不会像以前那样频繁运行。

性格;角色;字母

Character 是最大的原始类型包装类,包含许多常量、一个构造函数、许多方法和一对嵌套类 ( 子集和 UnicodeBlock )。

注意 字符的复杂性来源于 Java 对 Unicode(http://en.wikipedia.org/wiki/Unicode)的支持。为了简洁起见,我忽略了大部分字符与 Unicode 相关的复杂性,这超出了本章的范围。

Character 声明了一个单独的 Character(char value) 构造函数,您可以用它将一个 Character 对象初始化为值。这个构造函数由 char charValue() 补充,它返回包装的字符值。

当您开始编写应用时,您可能会编写一些表达式,如 ch>= ' 0 '&&ch<= ' 9 '(测试 ch 以查看它是否包含一个数字)和 ch>= ' A '&&ch<= ' Z '(测试 ch 以查看它是否包含一个大写字母)。您应该避免这样做,原因有三:

  • 在表达式中引入 bug 太容易了。例如,ch>' 0 '&&ch<= ' 9 '引入了一个比较中不包括 '0' 的微妙 bug。
  • 这些表达式不能很好地描述他们正在测试的内容。
  • 表达式偏向于拉丁数字(0–9)和字母(A–Z 和 A–Z)。它们没有考虑在其他语言中有效的数字和字母。例如, '\u0beb' 是一个字符文字,代表泰米尔语中的一个数字。

Character 声明了几个比较和转换类方法来解决这些问题。这些方法包括以下内容:

  • 静态布尔 isDigit(char ch) 当 ch 包含一个数字(通常是 0 到 9,但也包括其他字母中的数字)时,返回 true。
  • 静态布尔 isLetter(char ch) 当 ch 包含一个字母(通常是 A–Z 或 A–Z,但也包括其他字母表中的字母)时返回 true。
  • 当 c h 包含一个字母或数字(通常是 A-Z、A-Z 或 0-9,但也包括其他字母表中的字母或数字)时,静态布尔 isLetterOrDigit(char ch) 返回 true。
  • 静态布尔 is lower case(char ch)当 ch 包含小写字母时返回 true。
  • 静态布尔 isUpperCase(char ch) 当 ch 包含大写字母时返回 true。
  • 静态布尔值 isWhitespace(char ch) 当 ch 包含空白字符(通常是空格、水平制表符、回车符或换行符)时,返回 true。
  • static char to lower case(char ch)返回与 ch 的大写字母对应的小写字母;否则,该方法返回 ch 的值。
  • 静态 char toupper case(char ch)返回 ch 的小写字母的大写等值;否则,该方法返回 ch 的值。

例如, isDigit(ch) 优于 ch>= ' 0 '&&ch<= ' 9 ',因为它避免了错误的来源,可读性更好,并且对于非拉丁数字(例如, '\u0beb' )以及拉丁数字返回 true。

浮点和双精度

Float 和 Double 分别在 Float 和 Double 对象中存储浮点和双精度浮点值。这些类声明下列常量:

  • MAX_VALUE 标识可表示为 float 或 double 的最大值。
  • MIN_VALUE 标识可表示为 float 或 double 的最小值。
  • NaN 代表 0.0F/0.0F 为浮动和 0.0/0.0 为双。
  • NEGATIVE_INFINITY 将-infinity 表示为浮点型或双精度型。
  • 正 _ 无穷大 表示+无穷大为浮点型或双精度型。

Float 和 Double 也声明了以下用于初始化其对象的构造函数:

  • Float(浮点值) 将 Float 对象初始化为值。
  • Float(double value) 将 Float 对象初始化为 float 等价于值。
  • Float(String s) 将 s 的文本转换为浮点值,并将该值存储在 Float 对象中。
  • Double(双精度值) 将 Double 对象初始化为值。
  • Double(String s)将 s 的文本转换为双精度浮点值,并将该值存储在 Double 对象中。

Float 的构造函数由 float floatValue() 补充,返回包装后的浮点值。类似地, Double 的构造函数由 double doubleValue() 补充,返回包装后的双精度浮点值。

Float 声明了除了 floatValue() 之外的几个实用方法。这些方法包括以下内容:

  • 静态 int floatToIntBits(浮点值) 将值转换为 32 位整数。
  • 静态布尔 isInfinite(float f) 当 f 的值为+infinity 或–infinity 时返回 true。当当前 Float 对象的值为+infinity 或-infinity 时,相关的布尔 is infinit()方法 返回 true。
  • 静态布尔 isNaN(float f) 当 f 的值为 NaN 时返回 true。当当前 Float 对象的值为 NaN 时,相关的布尔 isNaN() 方法返回 true。
  • 静态浮点 parseFloat(String s) 解析 s ,返回与 s 的浮点值的文本表示等价的浮点值,或者在该表示无效(例如包含字母)时抛出 Java . lang . numberformatexception。

Double 声明了几个实用方法以及 doubleValue() 。这些方法包括以下内容:

  • 静态 long doubleToLongBits(双精度值) 将值转换为长整型。
  • 静态布尔 isInfinite(double d) 当 d 的值为+infinity 或-infinity 时返回 true。当当前 Double 对象的值为+infinity 或-infinity 时,相关的布尔 is infinit()方法返回 true。
  • 静态布尔 isNaN(double d) 当 d 的值为 NaN 时返回 true。当当前 Double 对象的值为 NaN 时,相关的 public boolean isNaN() 方法返回 true。
  • 静态 double parse double(String s)解析 s ,返回与 s 的双精度浮点值的文本表示等效的双精度浮点值,或者在该表示无效时抛出 NumberFormatException 。

float pointbits()和 doubleToIntBits() 方法用于实现 equals() 和 hashCode() 方法,这些方法必须考虑 float 和 double 字段。 floatToIntBits() 和 doubleToIntBits() 允许 equals() 和 hashCode() 正确响应以下情况:

  • 当 f1 和 f2 包含浮点时, equals() 必须返回 true。NaN (或 d1 和 d2 包含双。南)。如果 equals() 以类似于 f1 . float value()= = F2 . float value()(或 D1 . double value()= = D2 . double value())的方式实现,该方法将返回 false,因为 NaN 不等于任何值,包括它本身。
  • 当 f1 包含+0.0 而 f2 包含-0.0 时(或反之亦然),或者 d1 包含+0.0 而 d2 包含-0.0 时(或反之亦然),必须返回 false。如果 equals() 以类似于 f1 . float value()= = F2 . float value()(或 D1 . double value()= = D2 . double value())的方式实现,该方法将返回 true,因为+0.0 = = 0.0 返回 true。

这些要求是基于散列的集合(在第 9 章中讨论)正常工作所必需的。清单 8-1 展示了它们如何影响 Float 和 Double s equals()方法。

清单 8-1 。演示了 NaN 上下文中的 Float 的 equals() 方法 和+/-0.0 上下文中的 Double 的 equals() 方法

public class FloatDoubleDemo
{
   public static void main(String[] args)
   {
      Float f1 = new Float(Float.NaN);
      System.out.println(f1.floatValue());
      Float f2 = new Float(Float.NaN);
      System.out.println(f2.floatValue());
      System.out.println(f1.equals(f2));
      System.out.println(Float.NaN == Float.NaN);
      System.out.println();
      Double d1 = new Double(+0.0);
      System.out.println(d1.doubleValue());
      Double d2 = new Double(-0.0);
      System.out.println(d2.doubleValue());
      System.out.println(d1.equals(d2));
      System.out.println(+0.0 == -0.0);
   }
}

编译清单 8-1(【FloatDoubleDemo.java】T2【javac】)并运行这个应用( java FloatDoubleDemo )。下面的输出证明了 Float 的 equals() 方法正确处理 NaN, Double 的 equals() 方法正确处理+/-0.0:

NaN
NaN
true
false

0.0
-0.0
false
true

提示当你想测试一个 float 或 double 值是否等于+无穷大或无穷大(但不是两者都等于)时,不要使用 isInfinite() 。而是通过=与 NEGATIVE_INFINITY 或 POSITIVE_INFINITY 进行比较。例如, f == Float。负无穷大。

你会发现 parseFloat() 和 parseDouble() 在很多上下文中都很有用。例如,清单 8-2 使用 parseDouble() 将命令行参数解析成 double s。

清单 8-2 。将命令行参数解析为双精度浮点值

public class Calc
{
   public static void main(String[] args)
   {
      if (args.length != 3)
      {
         System.err.println("usage: java Calc value1 op value2");
         System.err.println("op is one of +, -, x, or /");
         return;
      }
      try
      {
         double value1 = Double.parseDouble(args[0]);
         double value2 = Double.parseDouble(args[2]);
         if (args[1].equals("+"))
            System.out.println(value1 + value2);
         else
         if (args[1].equals("-"))
            System.out.println(value1 - value2);
         else
         if (args[1].equals("x"))
            System.out.println(value1 * value2);
         else
         if (args[1].equals("/"))
            System.out.println(value1 / value2);
         else
            System.err.println("invalid operator: " + args[1]);
      }
      catch (NumberFormatException nfe)
      {
         System.err.println("Bad number format: " + nfe.getMessage());
      }
   }
}

指定 java Calc 10E+3 + 66.0 来试用 Calc 应用。这个应用通过输出 10066.0 来响应。如果您指定了 java Calc 10E+3 + A ,您将会看到错误的数字格式:对于输入字符串:【A】作为输出,这是对第二个 parseDouble() 方法调用抛出的 NumberFormatException 对象的响应。

尽管 NumberFormatException 描述了一个未检查的异常,尽管未检查的异常因为代表编码错误而经常不被处理,但是 NumberFormatException 在这个例子中不符合这个模式。这个异常不是由编码错误引起的;它源于有人向应用传递了非法的数字参数,这是无法通过正确的编码来避免的。也许 NumberFormatException 应该被实现为一个检查异常。

整数、长整型、短整型和字节型

Integer 、 Long 、 Short 和 Byte 分别在 Integer 、 Long 、 Short 和 Byte 对象中存储 32 位、64 位、16 位和 8 位的整数值。

每个类声明 MAX_VALUE 和 MIN_VALUE 常量,这些常量标识可以由其关联的原语类型表示的最大值和最小值。这些类还声明了以下用于初始化其对象的构造函数:

  • Integer(int value) 初始化 Integer 对象为值。
  • Integer(String s) 将 s 的文本转换为 32 位整数值,并将该值存储在 Integer 对象中。
  • Long(长值) 将 Long 对象初始化为值。
  • Long(String s) 将 s 的文本转换为 64 位整数值,并将该值存储在 Long 对象中。
  • Short(短值) 将 Short 对象初始化为值。
  • Short(String s) 将 s 的文本转换为 16 位整数值,并将该值存储在 Short 对象中。
  • 字节(字节值 ) 将字节对象初始化为值。
  • Byte(String s) 将 s 的文本转换为 8 位整数值,并将该值存储在 Byte 对象中。

整数的构造函数由 int intValue() , Long 的构造函数由 long longValue() , Short 的构造函数由 short shortValue() 补充, Byte 的构造函数由 Byte value()补充。这些方法返回包装的整数。

这些类声明了各种有用的面向整数的方法。例如, Integer 声明了以下类方法,用于根据特定的表示形式(二进制、十六进制、八进制和十进制)将 32 位整数转换为字符串:

  • 静态字符串 toBinaryString(int i) 返回一个字符串对象,包含 i 的二进制表示。例如,integer . tobinary String(255)返回一个包含 11111111 的字符串对象。
  • 静态字符串 toHexString(int i) 返回一个字符串对象,包含 i 的十六进制表示。例如, Integer.toHexString(255) 返回一个包含 ff 的字符串对象。
  • 静态字符串 toOctalString(int i) 返回一个字符串对象,包含 i 的八进制表示。例如, toOctalString(64) 返回一个包含 100 的字符串对象。
  • 静态字符串 toString(int i) 返回一个字符串对象,包含 i 的十进制表示。例如, toString(255) 返回一个包含 255 的字符串对象。

在二进制字符串前面加上零通常很方便,这样就可以在列中对齐多个二进制字符串。例如,您可能希望创建一个显示以下对齐输出的应用:

11110001
+
00000111
--------
11111000

可惜,【tobinary string()并没有让你完成这个任务。例如,integer . tobinary String(7)返回一个包含 111 而不是 00000111 的字符串对象。清单 8-3 的 toAlignedBinaryString()方法解决了这个疏忽。

清单 8-3 。对齐二进制字符串

public class AlignBinaryString
{
   public static void main(String[] args)
   {
      System.out.println(toAlignedBinaryString(7, 8));
      System.out.println(toAlignedBinaryString(255, 16));
      System.out.println(toAlignedBinaryString(255, 7));
   }

   static String toAlignedBinaryString(int i, int numBits)
   {
      String result = Integer.toBinaryString(i);
      if (result.length() > numBits)
         return null; // cannot fit result into numBits columns
      int numLeadingZeros = numBits - result.length();
      StringBuilder sb = new StringBuilder();
      for (int j = 0; j < numLeadingZeros; j++)
         sb.append('0');
      return sb.toString() + result;
   }
}

toAlignedBinaryString() 方法有两个参数:第一个参数指定要转换成二进制字符串的 32 位整数,第二个参数指定要容纳该字符串的位列数。

在调用 toBinaryString() 返回 i 的不带前导零的等价二进制字符串后, toAlignedBinaryString() 验证该字符串的数字是否能符合 numBits 指定的位列数。如果它们不匹配,这个方法返回 null。

继续, toAlignedBinaryString() 计算前置到结果的前导“0”的数量,然后使用 for 循环创建一串前导零。此方法通过返回结果字符串前面的前导零字符串来结束。

当您运行此应用时,它会生成以下输出:

00000111
0000000011111111
null

编号

每个浮点型、双精度型、整型、长型、短型、字节型除了自己的xValue()方法外,还提供了其他类的xValue()方法。例如, Float 提供了 doubleValue() , intValue() , longValue() , shortValue() , byteValue() 以及 floatValue() 。

这六个方法都是 Number 的成员,它是 Float 、 Double 、 Integer 、 Long 、 Short 和 Byte — Number 的 floatValue() 、 doubleValue() 、 intValue() 和 Number 也是 java.math.BigDecimal 和 java.math.BigInteger 的超类(还有一些并发相关的类;参见第十章)。

Number 的存在是为了简化对一组 Number 子类对象的迭代。例如,可以声明一个 Java . util . list类型的变量,并将其初始化为 Java . util . ArrayList的实例。然后,您可以在集合中存储一组 Number subclass 对象,并通过多态地调用一个子类方法来迭代这个集合。

探索线程

应用通过线程执行,这些线程是应用代码的独立执行路径。当多个线程正在执行时,每个线程的路径可以不同于其他线程的路径。例如,一个线程可能执行 switch 语句的一个案例,而另一个线程可能执行该语句的另一个案例。

注意应用使用线程来提高性能。一些应用可以只使用默认主线程(执行 main() 方法的线程)来执行它们的任务,但是其他应用需要额外的线程来在后台执行时间密集型任务,以便它们保持对用户的响应。

虚拟机为每个线程提供了自己的方法调用堆栈,以防止线程相互干扰。独立的堆栈让线程能够跟踪它们要执行的下一条指令,这些指令可能因线程而异。堆栈还为线程提供自己的方法参数、局部变量和返回值的副本。

Java 通过其线程 API 支持线程。这个 API 由 java.lang 包中的一个接口( Runnable )和四个类(线程、线程组、 ThreadLocal 和 InheritableThreadLocal )组成。在探索了可运行和线程(并且在本次探索中提到了线程组之后,在本节中,我将探索线程同步、线程本地和可继承线程本地。

注意 Java 5 引入了 java.util.concurrent 包作为低级线程 API 的高级替代。(我将在第 10 章中讨论这个包。)尽管 java.util.concurrent 是处理线程的首选 API,但您也应该对线程有所了解,因为它在简单的线程场景中很有帮助。此外,您可能需要分析其他人依赖于线程的源代码。

可运行线程

Java 提供了 Runnable 接口来标识那些为线程提供代码的对象,线程通过这个接口的唯一的 void run() 方法 来执行——线程不接收任何参数,也不返回值。类实现了 Runnable 来提供这段代码,其中一个类就是线程。

线程为底层操作系统的线程架构提供一致的接口。(操作系统通常负责创建和管理线程。)线程使得将代码与线程相关联以及启动和管理那些线程成为可能。每个线程实例关联一个单独的线程。

线程声明了几个用于初始化线程对象的构造函数。其中一些构造函数采用了 Runnable 参数:你可以提供代码来运行,而不必扩展线程。其他构造函数不采用 Runnable 参数:你必须扩展线程并覆盖它的 run() 方法 来提供运行的代码。

例如, Thread(Runnable runnable) 将一个新的 Thread 对象初始化为指定的 runnable ,其代码将被执行。相反, Thread() 不会将线程初始化为 Runnable 参数。相反,您的 Thread 子类提供了一个调用 Thread() 的构造函数,并且该子类还覆盖了 Thread 的 run() 方法。

在没有显式名称参数的情况下,每个构造函数都会给线程对象分配一个唯一的默认名称(以线程- 开始)。名字使得区分线程成为可能。与选择默认名称的前两个构造函数不同, Thread(String threadName) 允许您指定自己的线程名称。

Thread 也声明了启动和管理线程的方法。表 8-1 描述了许多更有用的方法。

表 8-1 。线程方法

方法 描述
静态线程当前线程()T3】 返回与调用这个方法的线程相关联的线程对象。
String getName() 返回与这个线程对象相关联的名称。
螺纹。状态 getState()状态 T3】 返回与这个线程对象相关的线程的状态。状态由线程标识。将 enum 状态设置为阻塞(等待获取锁,稍后讨论)、新(已创建但未启动)、可运行(正在执行)、终止(线程已死亡)、 TIMED_WAITING (等待经过指定的时间量)或等待(无限期等待)。
无效中断() 在这个线程对象中设置中断状态标志。如果相关线程被阻塞或正在等待,清除该标志,并通过抛出 Java . lang . interrupted exception 类的实例来唤醒线程。
静布尔中断() 当与此线程对象相关联的线程有挂起的中断请求时,返回 true。清除中断状态标志。
boolean is live() 返回 true 表示这个线程对象的关联线程是活动的而不是死的。一个线程的生命周期从它在 start() 方法中实际启动之前,到它离开 run() 方法之后,在这一点上它就死了。
布尔 isDaemon() 当与这个线程对象相关联的线程是一个守护线程时返回 true,这个线程充当一个用户线程(非守护线程)的助手,并且当应用的最后一个非守护线程终止时自动终止,以便应用可以退出。
布尔 is interrupted()T3】 当与此线程对象相关的线程有挂起的中断请求时,返回 true。
void join()T3】 在这个线程对象上调用这个方法的线程等待与这个对象相关的线程死亡。当这个线程对象的中断()方法被调用时,这个方法抛出中断异常。
无效加入(长毫) 在这个线程对象上调用这个方法的线程等待与这个对象相关联的线程死亡,或者直到经过了毫秒毫秒,以先发生的为准。当这个线程对象的 interrupt() 方法被调用时,这个方法抛出 InterruptedException 。
void setdaemon(boolean isdaemon)T2] 当 isDaemon 为真时,将此线程对象的关联线程标记为守护线程。当线程尚未创建和启动时,该方法抛出 Java . lang . illegalthreadstateexception。
空集名(字符串线程名) 将 threadName 的值赋给这个线程对象,作为其关联线程的名称。
静虚空睡眠(长时间) 暂停与这个线程对象相关的线程时间毫秒。当这个线程对象的 interrupt() 方法在线程休眠时被调用时,这个方法抛出 InterruptedException 。
虚空开始() 创建并启动这个线程对象的关联线程。当线程先前被启动并且正在运行或者已经死亡时,该方法抛出 IllegalThreadStateException。

清单 8-4 通过一个 main() 方法 向您介绍线程 API,该方法演示了 Runnable 、 Thread(Runnable runnable) 、 currentThread() 、 getName() 和 start() 。

清单 8-4 。一对计数线

public class CountingThreads
{
   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         String name = Thread.currentThread().getName();
                         int count = 0;
                         while (true)
                            System.out.println(name + ": " + count++);
                      }
                   };
      Thread thdA = new Thread(r);
      Thread thdB = new Thread(r);
      thdA.start();
      thdB.start();
   }
}

根据清单 8-4 ,执行 main() 的默认主线程首先实例化一个实现 Runnable 的匿名类。然后它创建两个线程对象,将每个对象初始化为 runnable,并调用线程的 start() 方法 来创建并启动两个线程。完成这些任务后,主线程退出 main() 并死亡。

两个启动的线程中的每一个都执行 runnable 的 run() 方法。它调用线程的 currentThread() 方法以获取其关联的线程实例,使用该实例调用线程的 getName() 方法以返回其名称,将 count 初始化为 0,并进入一个无限循环,在该循环中,它输出 name 和 count ,并在每次执行时递增 count

提示要停止一个没有结束的应用,在 Windows 平台上同时按下 Ctrl 和 C 键,或者在非 Windows 平台上执行相同的操作。

当我在 64 位 Windows 7 平台上运行这个应用时,我观察到两个线程在执行过程中交替出现。一次运行的部分输出如下所示:

Thread-0: 0
Thread-0: 1
Thread-1: 0
Thread-0: 2
Thread-1: 1
Thread-0: 3
Thread-1: 2
Thread-0: 4
Thread-1: 3
Thread-0: 5
Thread-1: 4
Thread-0: 6
Thread-1: 5
Thread-0: 7
Thread-1: 6
Thread-1: 7
Thread-1: 8
Thread-1: 9
Thread-1: 10
Thread-1: 11
Thread-1: 12

注意我执行了 Java count threads>output . txt 将输出捕获到 output.txt 中,然后呈现了之前这个文件的部分内容。将输出捕获到文件可能会显著影响输出,否则如果没有捕获输出,将会观察到输出。因为我在本节中展示了捕获的线程输出,所以在您的平台上执行应用时要记住这一点。另外,请注意,您平台的线程架构可能会影响可观察到的结果。我已经在 64 位 Windows 7 平台上测试了本节中的每个示例。

当计算机拥有足够多的处理器和/或处理器内核时,计算机的操作系统会为每个处理器或内核分配一个单独的线程,以便线程同时执行(同时)。当计算机没有足够的处理器和/或内核时,线程必须等待轮到它使用共享的处理器/内核。

*操作系统使用一个调度器(http://en . Wikipedia . org/wiki/Scheduling _(computing来决定一个等待线程何时执行。下表列出了三种不同的调度程序:

注意虽然前面的输出表明第一个线程( Thread-0 )开始执行,但是千万不要假设与 Thread 对象相关联的线程是第一个执行的线程,该对象的 start() 方法被首先调用。尽管这可能适用于某些调度程序,但可能不适用于其他调度程序。

一个多级反馈队列和许多其他线程调度器考虑了优先级(线程相对重要性)的概念。他们经常将抢占式调度(优先级较高的线程抢占——中断并运行,而不是——优先级较低的线程)与循环调度(优先级相等的线程被给予相等的时间片,这些时间片被称为时间片,轮流执行)。

线程通过其 void set priority(int priority)方法支持优先级(将该线程对象的线程优先级设置为优先级,其范围为线程。最小优先级到线程。最大优先级 — 线程。NORMAL_PRIORITY 标识默认优先级)和 int getPriority() 方法(返回当前优先级)。

注意使用 setPriority() 方法会影响应用跨平台的可移植性,因为不同的调度程序可以用不同的方式处理优先级的变化。例如,一个平台的调度程序可能会延迟低优先级线程的执行,直到高优先级线程完成。这种延迟会导致无限期推迟饥饿,因为优先级较低的线程在无限期等待执行时会“饥饿”,这会严重损害应用的性能。另一个平台的调度程序可能不会无限期地延迟较低优先级的线程,从而提高应用的性能。

清单 8-5 重构 清单 8-4 的 main() 方法,给每个线程一个非默认的名字,并在输出 name 和 count 后让每个线程休眠。

清单 8-5 。重温一对计数线程

public class CountingThreads
{
   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         String name = Thread.currentThread().getName();
                         int count = 0;
                         while (true)
                         {
                            System.out.println(name + ": " + count++);
                            try
                            {
                               Thread.sleep(100);
                            }
                            catch (InterruptedException ie)
                            {
                            }
                         }
                      }
                   };
      Thread thdA = new Thread(r);
      thdA.setName("A");
      Thread thdB = new Thread(r);
      thdB.setName("B");
      thdA.start();
      thdB.start();
   }
}

清单 8-5 揭示线程 A 和 B 执行 thread . sleep(100); 休眠 100 毫秒。这种休眠会导致每个线程更频繁地执行,如以下部分输出所示:

A: 0
B: 0
A: 1
B: 1
B: 2
A: 2
B: 3
A: 3
B: 4
A: 4
B: 5
A: 5
B: 6
A: 6
B: 7
A: 7

一个线程偶尔会启动另一个线程来执行冗长的计算、下载大文件或执行其他一些耗时的活动。在完成其他任务后,启动了工作线程的线程准备好处理工作线程的结果,并等待工作线程完成和终止。

可以通过使用 while 循环来等待工作线程死亡,该循环在工作线程的线程对象上重复调用线程的 isAlive() 方法 ,并在该方法返回 true 时休眠一段时间。然而,清单 8-6 展示了一个不太冗长的替代方案: join() 方法 。

清单 8-6 。将默认主线程与后台线程结合

public class JoinDemo
{
   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         System.out.println("Worker thread is simulating " +
                                            "work by sleeping for 5 seconds.");
                         try
                         {
                            Thread.sleep(5000);
                         }
                         catch (InterruptedException ie)
                         {
                         }
                         System.out.println("Worker thread is dying.");
                      }
                   };
      Thread thd = new Thread(r);
      thd.start();
      System.out.println("Default main thread is doing work.");
      try
      {
         Thread.sleep(2000);
      }
      catch (InterruptedException ie)
      {
      }
      System.out.println("Default main thread has finished its work.");
      System.out.println("Default main thread is waiting for worker thread " +
                         "to die.");
      try
      {
         thd.join();
      }
      catch (InterruptedException ie)
      {
      }
      System.out.println("Main thread is dying.");
   }
}

清单 8-6 展示了默认的主线程启动一个工作线程,执行一些工作,然后通过工作线程的 thd 对象调用 join() 来等待工作线程死亡。当您运行此应用时,您将发现类似如下的输出(消息顺序可能会有所不同):

Default main thread is doing work.
Worker thread is simulating work by sleeping for 5 seconds.
Default main thread has finished its work.
Default main thread is waiting for worker thread to die.
Worker thread is dying.
Main thread is dying.

每个线程对象都属于某个线程组对象;线程声明了一个 Thread group getThreadGroup()方法 返回这个对象。您应该忽略线程组,因为它们并不那么有用。如果你需要逻辑分组线程对象,你应该使用数组或者集合。

注意各种线程组方法有缺陷。例如,int enumerate(Thread[]threads)在其 threads 数组参数太小而无法存储其 Thread 对象时,不会在其枚举中包含所有活动线程。虽然您可能认为可以使用来自 int activeCount() 方法的返回值来适当地调整这个数组的大小,但是不能保证这个数组足够大,因为 activeCount() 的返回值随着线程的创建和死亡而波动。

然而,您仍然应该了解线程组,因为它在处理线程执行时抛出的异常方面做出了贡献。清单 8-7 通过呈现一个 run() 方法试图将一个整数除以 0,这导致抛出一个 Java . lang . arithmetic exception 实例,为学习异常处理打下了基础。

清单 8-7 。从 run() 方法中抛出异常

public class ExceptionThread
{
   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         int x = 1 / 0; // Line 10
                      }
                   };
      Thread thd = new Thread(r);
      thd.start();
   }
}

运行这个应用,您将看到一个异常跟踪,它标识了抛出的算术异常 :

Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
                at ExceptionThread$1.run(ExceptionThread.java:10)
                at java.lang.Thread.run(Unknown Source)

当从 run() 方法中抛出异常时,线程终止,并发生以下活动:

  • 虚拟机寻找线程的实例。通过线程的 void setUncaughtExceptionHandler 安装。UncaughtExceptionHandler eh) 方法。当找到这个处理程序时,它将执行传递给实例的 void uncaughtException(Thread t,Throwable e) 方法,其中 t 标识抛出异常的线程的 Thread 对象,而 e 标识抛出的异常或错误——可能是抛出了一个 Java . lang . outofmemory error 实例。如果该方法抛出异常/错误,虚拟机将忽略该异常/错误。
  • 假设 setUncaughtExceptionHandler()没有被调用来安装一个处理程序,虚拟机将控制权传递给关联的 ThreadGroup 对象的 uncaughtException(Thread t,Throwable e) 方法。假设线程组没有被扩展,并且其 uncaughtException() 方法没有被覆盖来处理异常,当父线程组存在时, uncaughtException() 将控制传递给父线程组对象的 uncaughtException() 方法。否则,它检查是否已经安装了默认的未捕获异常处理程序(通过线程的静态 void setDefaultUncaughtExceptionHandler(线程。UncaughtExceptionHandler 处理程序)方法)。如果已经安装了一个默认的未捕获异常处理程序,那么它的 uncaughtException() 方法将使用相同的两个参数来调用。否则, uncaughtException() 检查其 Throwable 参数,以确定它是否是 java.lang.ThreadDeath 的实例。如果是,则不做任何特殊处理。否则,如清单 8-7 的异常消息所示,使用 Throwable 参数的 printStackTrace() 方法,从线程的 getName() 方法返回的包含线程名称和堆栈回溯的消息将被打印到标准错误流中。

清单 8-8 演示了线程的 setUncaughtExceptionHandler()和 setDefaultUncaughtExceptionHandler()方法。

清单 8-8 。演示未捕获的异常处理程序

public class ExceptionThread
{
   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         int x = 1 / 0;
                      }
                   };
      Thread thd = new Thread(r);
      Thread.UncaughtExceptionHandler uceh;
      uceh = new Thread.UncaughtExceptionHandler()
             {
                @Override
                public void uncaughtException(Thread t, Throwable e)
                {
                   System.out.println("Caught throwable " + e + " for thread "
                                      + t);
                }
             };
      thd.setUncaughtExceptionHandler(uceh);
      uceh = new Thread.UncaughtExceptionHandler()
             {
                @Override
                public void uncaughtException(Thread t, Throwable e)
                {
                   System.out.println("Default uncaught exception handler");
                   System.out.println("Caught throwable " + e + " for thread "
                                      + t);
                }
             };
      thd.setDefaultUncaughtExceptionHandler(uceh);
      thd.start();
   }
}

当您运行此应用时,您将观察到以下输出:

Caught throwable java.lang.ArithmeticException: / by zero for thread Thread[Thread-0,5,main]

您也不会看到默认的未捕获异常处理程序的输出,因为默认处理程序没有被调用。要看到那个输出,必须注释掉 thd . setuncaughtexceptionhandler(uceh);。如果还注释掉 thd . setdefaultuncaughtexceptionhandler(uceh);,你会看到清单 8-7 的输出。

注意 线程声明了几个不推荐使用的方法,包括 stop() (停止一个正在执行的线程)。这些方法已被弃用,因为它们不安全。不要使用这些被否决的方法。(我将在本章的后面向你展示如何安全地停止一个线程。)此外,您应该避免使用 static void yield() 方法,该方法旨在将执行从当前线程切换到另一个线程,因为它会影响可移植性并损害应用性能。尽管 yield() 可能会切换到某些平台上的另一个线程(这可以提高性能),但 yield() 可能只会返回到其他平台上的当前线程(这会损害性能,因为 yield() 调用只是浪费了时间)。 线程同步

在整个执行过程中,每个线程都与其他线程相隔离,因为每个线程都有自己的方法调用堆栈。然而,当线程访问和操作共享数据时,它们仍然会相互干扰。这种干扰会破坏共享数据,这种破坏会导致应用失败。

例如,考虑一个丈夫和妻子共同使用的支票账户。假设夫妻双方同时决定清空这个账户,而不知道对方也在做同样的事情。清单 8-9 展示了这个场景。

清单 8-9 。一个有问题的支票账户

public class CheckingAccount
{
   private int balance;

   public CheckingAccount(int initialBalance)
   {
      balance = initialBalance;
   }

   public boolean withdraw(int amount)
   {
      if (amount <= balance)
      {
         try
         {
            Thread.sleep((int) (Math.random() * 200));
         }
         catch (InterruptedException ie)
         {
         }
         balance -= amount;
         return true;
      }
      return false;
   }

   public static void main(String[] args)
   {
      final CheckingAccount ca = new CheckingAccount(100);
      Runnable r = new Runnable()
                   {
                      public void run()
                      {
                         String name = Thread.currentThread().getName();
                         for (int i = 0; i < 10; i++)
                             System.out.println (name + " withdraws $10: " +
                                                 ca.withdraw(10));
                      }
                   };
      Thread thdHusband = new Thread(r);
      thdHusband.setName("Husband");
      Thread thdWife = new Thread(r);
      thdWife.setName("Wife");
      thdHusband.start();
      thdWife.start();
   }
}

这个应用允许提取比账户中可用金额更多的钱。例如,以下输出显示,当只有 100 美元可用时,提取了 110 美元:

Wife withdraws $10: true
Husband withdraws $10: true
Husband withdraws $10: true
Wife withdraws $10: true
Wife withdraws $10: true
Husband withdraws $10: true
Wife withdraws $10: true
Wife withdraws $10: true
Wife withdraws $10: true
Husband withdraws $10: true
Husband withdraws $10: false
Husband withdraws $10: false
Husband withdraws $10: false
Husband withdraws $10: false
Husband withdraws $10: false
Husband withdraws $10: false
Wife withdraws $10: true
Wife withdraws $10: false
Wife withdraws $10: false
Wife withdraws $10: false

提取的钱比可用于提取的钱多的原因是在丈夫和妻子线程之间存在竞争条件。

注意竞争条件是指多个线程同时或几乎同时更新同一个对象。对象的一部分存储由一个线程写入的值,对象的另一部分存储由另一个线程写入的值。

竞争条件的存在是因为检查取款金额以确保其少于余额中出现的金额并从余额中扣除该金额的操作不是原子(不可分割)操作。(虽然原子是可分的,但 atomic 通常用来指不可分的东西。)

注意thread . sleep()方法调用睡眠时间不定(最长可达 199 毫秒),这样您可以观察到提取的钱比可提取的钱多。如果没有这个方法调用,您可能需要执行应用数百次(或更多次)才能看到这个问题,因为调度程序可能很少会在 amount < = balance 表达式和 balance -= amount 之间暂停线程;表达式语句—代码快速执行。

考虑以下场景:

  • 丈夫线程执行取款()的金额< =余额表达式,返回 true。调度程序暂停丈夫线程,让妻子线程执行。
  • 老婆线程执行取款()的金额< =余额表达式,返回 true。
  • 妻子线程执行撤回。调度程序暂停妻子线程,让丈夫线程执行。
  • 丈夫线程执行撤回。

这个问题可以通过同步对 retract()的访问来解决,这样一次只有一个线程可以在这个方法中执行。通过在方法的返回类型之前向方法头添加保留字 synchronized ,可以在方法级别同步访问,例如,synchronized boolean retract(int amount)。

正如我稍后演示的,您还可以通过指定 synchronized(object){/ synchronized statements /}来同步对语句块的访问,其中 object 是一个任意的对象引用。在执行离开方法/块之前,任何线程都不能进入同步的方法或块;这就是所谓的互斥

同步是根据监视器和锁实现的。一个监视器是一个并发结构,用于控制对一个临界区的访问,这是一个必须自动执行的代码区域。它在源代码级别被标识为同步方法或同步块。

是一个令牌,在监视器允许线程在监视器的临界区内执行之前,该线程必须获得该令牌。当线程退出监视器时,令牌被自动释放,以便给另一个线程一个获取令牌并进入监视器的机会。

注意一个已经获得锁的线程在调用线程的 sleep() 方法之一时不会释放这个锁。

进入同步实例方法的线程获取与调用该方法的对象相关联的锁。进入同步类方法的线程获取与该类的 java.lang.Class 对象相关联的锁。最后,进入同步块的线程获得与该块的控制对象相关联的锁。

提示 线程声明一个静态布尔 holdsLock(Object o) 方法,当调用线程持有对象 o 上的监视器锁时,该方法返回 true。你会发现这个方法在断言语句中很方便,比如 assert thread . holds lock(o);。

同步的需求通常是微妙的。例如,清单 8-10 的 ID 工具类声明了一个 getNextID() 方法 ,该方法返回一个唯一的基于 long 的 ID,可能会在生成唯一的文件名时使用。尽管您可能不这么认为,但此方法可能会导致数据损坏并返回重复值。

清单 8-10 。用于返回唯一 id 的工具类

class ID
{
   private static long nextID = 0;
   static long getNextID()
   {
      return nextID++;
   }
}

getNextID() 有两个不同步问题。因为 32 位虚拟机实现需要两步来更新一个 64 位长的整数,所以将 1 加到 nextID 不是原子的:调度程序可能会中断一个只更新了一半 nextID 的线程,这会破坏这个变量的内容。

注意: long 和 double 类型的变量在 32 位虚拟机上的非同步上下文中被写入时会遭到破坏。对于类型为 boolean 、 byte 、 char 、 float 、 int 或 short 的变量,不会出现这个问题;每种类型占用 32 位或更少。

假设多线程调用 getNextID() 。因为 postincrement ( ++ )分两步读取和写入 nextID 字段,所以多个线程可能会检索相同的值。例如,线程 A 执行 ++ ,读取 nextID ,但在被调度器中断之前不递增其值。线程 B 现在执行并读取相同的值。

这两个问题都可以通过同步访问 nextID 来解决,这样只有一个线程可以执行这个方法的代码。所需要做的就是将 synchronized 添加到方法头的方法返回类型之前,例如,static synchronized int get nextid()。

同步也用于线程之间的通信。例如,你可以设计自己的机制来停止一个线程(因为你不能使用线程的不安全 stop() 方法来完成这个任务)。清单 8-11 展示了如何完成这项任务。

清单 8-11 。试图停止线程

public class ThreadStopping
{
   public static void main(String[] args)
   {
      class StoppableThread extends Thread
      {
         private boolean stopped = false;

         @Override
         public void run()
         {
            while(!stopped)
              System.out.println("running");
         }

         void stopThread()
         {
            stopped = true;
         }
      }
      StoppableThread thd = new StoppableThread();
      thd.start();
      try
      {
         Thread.sleep(1000); // sleep for 1 second
      }
      catch (InterruptedException ie)
      {
      }
      thd.stopThread();
   }
}

清单 8-11 引入了一个 main() 方法,它带有一个名为 StoppableThread 的局部类,该局部类子类化 Thread 。 StoppableThread 声明了一个初始化为 false 的 stopped 字段,一个将该字段设置为 true 的 stopThread() 方法,以及一个 run() 方法,该方法的无限循环在每次循环迭代中检查 stopped 以查看其值是否已更改为 true 。

实例化 StoppableThread 后,默认主线程启动与这个线程对象关联的线程。然后它休眠一秒钟,并在死亡前调用 stoppeablethread 的 stop() 方法。当您在单处理器/单核机器上运行这个应用时,您可能会看到应用停止了。当应用在多处理器计算机或具有多个内核的单处理器计算机上运行时,您可能看不到这种停止。出于性能原因,每个处理器或内核可能都有自己的高速缓存(本地化的高速内存),并有自己的停止副本。当一个线程修改这个字段的副本时,另一个线程的停止的副本不会改变。

清单 8-12 重构清单 8-11 以保证应用能在各种机器上正确运行。

清单 8-12 。在多处理器/多核机器上保证停止

public class ThreadStopping
{
   public static void main(String[] args)
   {
      class StoppableThread extends Thread
      {
         private boolean stopped = false;

         @Override
         public void run()
         {
            while(!isStopped())
              System.out.println("running");
         }

         synchronized void stopThread()
         {
            stopped = true;
         }

         private synchronized boolean isStopped()
         {
            return stopped;
         }
      }
      StoppableThread thd = new StoppableThread();
      thd.start();
      try
      {
         Thread.sleep(1000); // sleep for 1 second
      }
      catch (InterruptedException ie)
      {
      }
      thd.stopThread();
   }
}

清单 8-12 的 stopThread() 和 ISS stop()方法 同步支持线程通信(在调用 stopThread() 的默认主线程和在 run() 内部执行的启动线程之间)。当线程进入这些方法之一时,它保证访问 stopped 字段的单个共享副本(不是缓存副本)。

同步是支持互斥或者互斥结合线程通信所必需的。然而,当唯一的目的是在线程之间通信时,存在同步的替代方案。这种选择是保留字易变的,这在清单 8-13 中有演示。

清单 8-13 。线程通信同步的可变替代

public class ThreadStopping
{
   public static void main(String[] args)
   {
      class StoppableThread extends Thread
      {
         private volatile boolean stopped = false;

         @Override
         public void run()
         {
            while(!stopped)
              System.out.println("running");
         }

         void stopThread()
         {
            stopped = true;
         }
      }
      StoppableThread thd = new StoppableThread();
      thd.start();
      try
      {
         Thread.sleep(1000); // sleep for 1 second
      }
      catch (InterruptedException ie)
      {
      }
      thd.stopThread();
   }
}

清单 8-13 声明停止为易变;访问该字段的线程将始终访问单个共享副本(而不是多处理器/多核机器上的缓存副本)。除了生成不太冗长的代码之外, volatile 可能会提供比同步更好的性能。

当一个字段被声明为易变时,它也不能被声明为最终。如果你依赖于波动性的语义(意思),你仍然可以从 final 字段中得到这些。要了解更多信息,请查阅 Brian Goetz 的“Java 理论和实践:修复 Java 内存模型,第 2 部分”文章(www.ibm.com/developerworks/library/j-jtp03304/)。

注意仅在线程通信上下文中使用 volatile 。此外,您只能在字段声明的上下文中使用该保留字。虽然您可以声明 double 和 long 字段 volatile ,但是您应该避免在 32 位虚拟机上这样做,因为访问一个 double 或 long 变量的值需要两次操作,并且需要通过同步互斥来安全地访问它们的值。

java.lang.Object 的 wait() 、 notify() 和 notifyAll() 方法支持一种线程通信形式,其中一个线程主动等待某个条件(继续执行的先决条件)出现,此时另一个线程通知等待线程它可以继续执行。 wait() 使其调用线程等待一个对象的监视器, notify() 和 notifyAll() 唤醒一个或所有等待监视器的线程。

注意因为 wait() 、 notify() 和 notifyAll() 方法依赖于锁,所以不能从同步方法或同步块的外部调用它们。如果您没有注意到这个警告,您将会遇到一个抛出的 Java . lang . illegalmonitorstateexception 类的实例。此外,一个已经获得锁的线程在调用对象的 wait() 方法之一时释放这个锁。

涉及条件的线程通信的一个经典例子是生产者线程和消费者线程之间的关系。生产者线程产生将由消费者线程消费的数据项。每个产生的数据项都存储在一个共享变量中。

假设线程没有通信,并且以不同的速度运行。生产者可能会生成一个新的数据项,并在消费者检索前一个数据项进行处理之前将其记录在共享变量中。此外,消费者可能会在生成新的数据项之前检索共享变量的内容。

为了克服这些问题,生产者线程必须等待,直到它被通知先前产生的数据项已经被消费,并且消费者线程必须等待,直到它被通知新的数据项已经被产生。清单 8-14 向您展示了如何通过 wait() 和 notify() 来完成这个任务。

清单 8-14 。生产者-消费者关系

public class PC
{
   public static void main(String[] args)
   {
      Shared s = new Shared();
      new Producer(s).start();
      new Consumer(s).start();
   }
}

class Shared
{
   private char c = '\u0000';
   private boolean writeable = true;

   synchronized void setSharedChar(char c)
   {
      while (!writeable)
         try
         {
            wait();
         }
         catch (InterruptedException e) {}
      this.c = c;
      writeable = false;
      notify();
   }

   synchronized char getSharedChar()
   {
      while (writeable)
         try
         {
            wait();
         }
         catch (InterruptedException e) {}
      writeable = true;
      notify();
      return c;
   }
}

class Producer extends Thread
{
   private Shared s;

   Producer(Shared s)
   {
      this.s = s;
   }

   @Override
   public void run()
   {
      for (char ch = 'A'; ch <= 'Z'; ch++)
      {
         synchronized(s)
         {
            s.setSharedChar(ch);
            System.out.println(ch + " produced by producer.");
         }
      }
   }
}
class Consumer extends Thread
{
   private Shared s;

   Consumer(Shared s)
   {
      this.s = s;
   }

   @Override
   public void run()
   {
      char ch;
      do
      {
         synchronized(s)
         {
            ch = s.getSharedChar();
            System.out.println(ch + " consumed by consumer.");
         }
      }
      while (ch != 'Z');
   }
}

这个应用创建了一个共享的对象和两个获取对象引用副本的线程。生产者调用对象的 setSharedChar() 方法 保存 26 个大写字母中的每一个;消费者调用对象的 getSharedChar() 方法 来获取每个字母。

可写实例字段跟踪两个条件:生产者等待消费者消费数据项,消费者等待生产者产生新的数据项。它有助于协调生产者和消费者的执行。下面的场景说明了这种协调,在该场景中,使用者首先执行:

  1. 消费者执行 s.getSharedChar() 来检索一封信。
  2. 在这个同步方法中,消费者调用 wait() ,因为 writeable 包含 true。消费者现在一直等到收到来自生产者的通知。
  3. 生产者最终执行 s . setsharedchar(ch);。
  4. 当生产者进入同步方法时(这是可能的,因为消费者在等待之前释放了 wait() 方法中的锁),生产者发现可写的值为真,并且不调用 wait() 。
  5. 生产者保存角色,将可写设置为假(这将导致生产者等待下一个 setSharedChar() 调用,此时消费者尚未消费角色),并调用 notify() 来唤醒消费者(假设消费者正在等待)。
  6. 生产者退出 setSharedChar(char c) 。
  7. 消费者醒来(并重新获得锁),将可写设置为真(这将导致消费者等待下一个 getSharedChar() 调用,此时生产者还没有产生一个字符),通知生产者唤醒该线程(假设生产者正在等待),并返回共享字符。

尽管同步工作正常,但您可能会观察到输出(在某些平台上)在消费消息之前显示多个生产消息。例如,您可能会看到由 producer 制作的 A。之后是由制片人制作的 B。,其次是 A 所消费的消费者。开始时应用的输出。

这种奇怪的输出顺序是由对 setSharedChar() 的调用及其伴随的 System.out.println() 方法调用不是原子的,以及对 getSharedChar() 的调用及其伴随的 System.out.println() 方法调用不是原子的。通过将这些方法调用对中的每一个包装在同步块中来纠正输出顺序,该同步块在由 s 引用的共享对象上同步。

当您运行这个应用时,它的输出应该总是以相同的交替顺序出现,如下所示(为了简洁起见,只显示了前几行):

A produced by producer.
A consumed by consumer.
B produced by producer.
B consumed by consumer.
C produced by producer.
C consumed by consumer.
D produced by producer.
D consumed by consumer.

注意永远不要在循环之外调用 wait() 。循环测试条件(!可写或可写)在 wait() 调用之前和之后。在调用 wait() 之前测试条件可以确保的活性。如果该测试不存在,并且如果条件成立并且在调用 wait() 之前调用了 notify() ,则等待线程不太可能会被唤醒。调用 wait() 后重新测试条件确保安全。如果重新测试没有发生,并且如果在线程从 wait() 调用中唤醒后条件不成立(当条件不成立时,可能另一个线程偶然调用了 notify() ),线程将继续破坏锁的受保护不变量。

太多的同步可能会有问题。如果不小心的话,您可能会遇到这样的情况:锁被多个线程获取,没有一个线程持有自己的锁,而是持有其他线程所需的锁,没有一个线程能够进入并在以后退出其临界区以释放其持有的锁,因为其他线程持有该临界区的锁。清单 8-15 的非典型例子演示了这个场景,它被称为死锁

清单 8-15 。僵局的病态案例

public class DeadlockDemo
{
   private Object lock1 = new Object();
   private Object lock2 = new Object();

   public void instanceMethod1()
   {
      synchronized(lock1)
      {
         synchronized(lock2)
         {
            System.out.println("first thread in instanceMethod1");
            // critical section guarded first by
            // lock1 and then by lock2
         }
      }
   }

   public void instanceMethod2()
   {
      synchronized(lock2)
      {
         synchronized(lock1)
         {
            System.out.println("second thread in instanceMethod2");
            // critical section guarded first by
            // lock2 and then by lock1
         }
      }
   }

   public static void main(String[] args)
   {
      final DeadlockDemo dld = new DeadlockDemo();
      Runnable r1 = new Runnable()
                    {
                       @Override
                       public void run()
                       {
                          while(true)
                          {
                             dld.instanceMethod1();
                             try
                             {
                                Thread.sleep(50);
                             }
                             catch (InterruptedException ie)
                             {
                             }
                          }
                       }
                    };
      Thread thdA = new Thread(r1);
      Runnable r2 = new Runnable()
                    {
                       @Override
                       public void run()
                       {
                          while(true)
                          {
                             dld.instanceMethod2();
                             try
                             {
                                Thread.sleep(50);
                             }
                             catch (InterruptedException ie)
                             {
                             }
                          }
                        }
                    };
      Thread thdB = new Thread(r2);
      thdA.start();
      thdB.start();
   }
}

清单 8-15 的线程 A 和线程 B 分别在不同的时间调用 instanceMethod1() 和 instanceMethod2() ,。考虑以下执行顺序:

  1. 线程 A 调用 instanceMethod1() ,获取分配给 lock1 引用对象的锁,并进入其外部临界段(但尚未获取分配给 lock2 引用对象的锁)。
  2. 线程 B 调用 instanceMethod2() ,获取分配给 lock2 引用对象的锁,并进入其外部临界段(但尚未获取分配给 lock1 引用对象的锁)。
  3. 线程 A 试图获取与锁 2 相关联的锁。虚拟机强制线程在内部临界区之外等待,因为线程 B 持有该锁。
  4. 线程 B 试图获取与 lock1 关联的锁。虚拟机强制线程在内部临界区之外等待,因为线程 A 持有该锁。
  5. 两个线程都无法继续,因为另一个线程持有所需的锁。您遇到了死锁情况,程序(至少在两个线程的上下文中)冻结了。

尽管前面的例子清楚地标识了死锁状态,但是检测死锁通常并不容易。例如,您的代码可能包含不同类之间的以下循环关系(在几个源文件中):

  • 类 A 的同步方法调用类 B 的同步方法。
  • B 类的同步方法调用 C 类的同步方法。
  • C 类的同步方法调用 A 类的同步方法。

如果线程 A 调用类 A 的 synchronized 方法,而线程 B 调用类 C 的 synchronized 方法,那么当线程 B 试图调用类 A 的 synchronized 方法,而线程 A 仍在该方法内部时,线程 B 将会阻塞。线程 A 将继续执行,直到它调用类 C 的 synchronized 方法,然后阻塞。死锁结果。

注意 Java 语言和虚拟机都没有提供防止死锁的方法,所以这个负担就落在了你的身上。防止死锁发生的最简单方法是避免同步方法或同步块调用另一个同步方法/块。虽然这个建议防止了死锁的发生,但是它是不切实际的,因为您的一个同步方法/块可能需要调用 Java API 中的一个同步方法,并且这个建议是多余的,因为被调用的同步方法/块可能不会调用任何其他同步方法/块,所以不会发生死锁。

有时您会希望将每线程数据(比如用户 ID)与一个线程相关联。虽然您可以使用局部变量来完成这项任务,但是您只能在局部变量存在时才能这样做。您可以使用实例字段将这些数据保存更长时间,但是这样您就必须处理同步问题。幸运的是,Java 提供了 ThreadLocal 作为一个简单(并且非常方便)的选择。

ThreadLocal 类的每个实例描述了一个线程本地变量 ,这个变量为每个访问该变量的线程提供一个单独的存储槽。您可以将线程局部变量视为一个多时隙变量,其中每个线程可以在同一个变量中存储不同的值。每个线程只看到自己的值,不知道其他线程在这个变量中有自己的值。

ThreadLocal 一般被声明为 ThreadLocal < T > ,其中 T 标识存储在变量中的值的类型。该类声明了以下构造函数和方法:

  • ThreadLocal() 创建新的线程局部变量。
  • T get() 返回调用线程存储槽中的值。如果线程调用该方法时条目不存在, get() 调用 initialValue() 。
  • T initialValue() 创建调用线程的存储槽,并在该槽中存储一个初始值(默认值)。初始值默认为 null。你必须子类化 ThreadLocal 并覆盖这个保护的方法来提供一个更合适的初始值。
  • void remove() 删除调用线程的存储槽。如果这个方法后面跟有 get() ,中间没有 set() , get() 调用 initialValue() 。
  • void set(T value) 将调用线程的存储槽的值设置为值。

清单 8-16 展示了如何使用 ThreadLocal 将不同的用户 id 与两个线程关联起来。

清单 8-16 。不同线程的不同用户 id

public class ThreadLocalDemo
{
   private static volatile ThreadLocal<String> userID =
      new ThreadLocal<String>();

   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         String name = Thread.currentThread().getName();
                         if (name.equals("A"))
                            userID.set("foxtrot");
                         else
                            userID.set("charlie");
                         System.out.println(name + " " + userID.get());
                      }
                   };
      Thread thdA = new Thread(r);
      thdA.setName("A");
      Thread thdB = new Thread(r);
      thdB.setName("B");
      thdA.start();
      thdB.start();
   }
}

在实例化 ThreadLocal 并将引用分配给名为 userID 的 volatile 类字段(该字段为 volatile ,因为它被不同的线程访问,这可能在多处理器/多核机器上执行),默认主线程创建另外两个线程,在 userID 中存储不同的字符串对象并输出它们的对象。

当您运行此应用时,您将观察到以下输出(可能不是这个顺序):

A foxtrot
B charlie

存储在线程局部变量中的值是不相关的。当一个新线程被创建时,它获得一个包含 initialValue() 值的新存储槽。也许你更愿意将一个值从一个父线程(一个创建另一个线程的线程)传递给一个子线程(被创建的线程)。您可以使用 InheritableThreadLocal 来完成这项任务。

inheritable thread local 是 ThreadLocal 的子类。除了声明一个 InheritableThreadLocal() 构造函数,这个类还声明了下面的 protected 方法:

  • T child value(T parent value)在创建子线程时,根据父线程的值计算子线程的初始值。在子线程启动之前,从父线程调用此方法。该方法返回传递给 parentValue 的参数,并且应该在需要另一个值时被覆盖。

清单 8-17 展示了如何使用 InheritableThreadLocal 将父线程的整数对象传递给子线程。

清单 8-17 。将对象从父线程传递到子线程

public class InheritableThreadLocalDemo
{
   private static volatile InheritableThreadLocal<Integer> intVal =
      new InheritableThreadLocal<Integer>();

   public static void main(String[] args)
   {
      Runnable rP = new Runnable()
                    {
                       @Override
                       public void run()
                       {
                          intVal.set(new Integer(10));
                          Runnable rC = new Runnable()
                                        {
                                           @Override
                                           public void run()
                                           {
                                              Thread thd;
                                              thd = Thread.currentThread();
                                              String name = thd.getName();
                                              System.out.println(name + " " +
                                                                 intVal.get());
                                           }
                                        };
                          Thread thdChild = new Thread(rC);
                          thdChild.setName("Child");
                          thdChild.start();
                       }
                    };
      new Thread(rP).start();
   }
}

在实例化 InheritableThreadLocal 并将其分配给一个名为 intVal 的 volatile 类字段后,默认主线程创建一个父线程,在 intVal 中存储一个包含 10 的 Integer 对象。父线程创建一个子线程,该子线程访问 intVal 并检索其父线程的整数对象。

当您运行此应用时,您将观察到以下输出:

Child 10

探索系统功能

java.lang 包包括四个面向系统的类:系统、运行时、进程和进程构建器。这些类使您可以获得运行应用的系统的信息(如环境变量值)并执行各种系统任务(如执行另一个应用)。为了简洁起见,在本节中,我只向您介绍前三个类。

注意 ProcessBuilder 是运行时的一个方便的替代品,用于创建应用进程并管理它们的属性。要了解关于该类的更多信息,请查看“Java 的 ProcessBuilder 入门:从 Java 程序与 Linux 交互的简单工具类”(http://singztechmusings . WordPress . com/2011/06/21/Getting-Started-with-javas-process builder-A-sample-Utility-Class-to-Interact-with-Linux-from-Java-Program/)。

系统

系统 是一个在、 out 和 err 类字段中声明的工具类,这些字段分别引用当前的标准输入、标准输出和标准误差流。第一个字段的类型是 Java . io . inputstream,最后两个字段的类型是 java.io.PrintStream 。(我会在第 11 章正式介绍这些类。)

System 还声明了提供对当前时间(以毫秒为单位)、系统属性值、环境变量值和其他类型的系统信息的访问的类方法。此外,它声明了支持将一个数组复制到另一个数组、请求垃圾收集等系统任务的类方法。

表 8-2 描述了系统的一些方法。

表 8-2。 系统方法

方法 描述
void arraycopy(Object src,int srcPos,Object dest,int destPos,int length) 将从零基偏移量 srcPos 开始的 src 数组中由长度指定的元素数量复制到从零基偏移量 destPos 开始的 dest 数组中。当 src 或 dest 为 null 时,该方法抛出 Java . lang . nullpointerexception,当复制导致访问数组边界之外的数据时抛出 Java . lang . indexoutofboundsexception,当 src 数组中的元素无法存储到 dest 中时抛出 Java . lang . arraystoreexception
长电流时间毫秒() 返回自 1970 年 1 月 1 日 00:00:00 UTC(协调世界时—参见http://en.wikipedia.org/wiki/Coordinated_Universal_Time)以来的当前系统时间,单位为毫秒。
void gc() 通知虚拟机现在是运行垃圾收集器的好时机。这只是一个提示;不能保证垃圾收集器会运行。
String getEnv(字符串名称) 返回由名标识的环境变量的值。
String getProperty(字符串名称) 返回由名称标识的系统属性(特定于平台的属性,如版本号)的值,如果不存在该属性,则返回 null。在 Android 环境中有用的系统属性的例子包括 file.separator 、 java.class.path 、 java.home 、 java.io.tmpdir 、 java.library.path 、 line.separator 、 os.arch 、 os.name 、 path.separator 和【T24】
void runFinalization() 通知虚拟机现在是执行任何未完成的对象终结的好时机。这只是一个提示;不保证会执行未完成的对象终结。
void seterr(printstream err) 设置标准误差装置指向 err 。
void setIn(InputStream in) 将标准输入设备设置为指向中的。
无效抵销(打印流输出) 将标准输出设备设置为指向 out 。

注意 系统声明安卓不支持的安全管理器 getSecurityManager()和 void setSecurityManager(security manager sm)方法。在 Android 设备上,前一个方法总是返回 null,后一个方法总是抛出一个 Java . lang . security exception 类的实例。关于后一种方法,其文档指出“安全管理器不提供执行不可信代码的安全环境,并且在 Android 上不受支持。不受信任的代码无法安全地隔离在 Android 上的单个虚拟机中。”

清单 8-18 演示了 arraycopy() 、 currentTimeMillis() 和 getProperty() 方法。

清单 8-18 。用系统方法做实验

public class SystemDemo
{
   public static void main(String[] args)
   {
      int[] grades = { 86, 92, 78, 65, 52, 43, 72, 98, 81 };
      int[] gradesBackup = new int[grades.length];
      System.arraycopy(grades, 0, gradesBackup, 0, grades.length);
      for (int i = 0; i < gradesBackup.length; i++)
         System.out.println(gradesBackup[i]);
      System.out.println("Current time: " + System.currentTimeMillis());
      String[] propNames =
      {
         "file.separator",
         "java.class.path",
         "java.home",
         "java.io.tmpdir",
         "java.library.path",
         "line.separator",
         "os.arch",
         "os.name",
         "path.separator",
         "user.dir"
      };
      for (int i = 0; i < propNames.length; i++)
         System.out.println(propNames[i] + ": " +
                            System.getProperty(propNames[i]));
   }
}

清单 8-18 的 main() 方法从演示 arraycopy() 开始。它使用这个方法将一个 grades 数组的内容复制到一个 gradesBackup 数组。

提示array copy()方法是将一个数组复制到另一个数组的最快的便携方法。此外,当您编写一个类,它的方法返回一个对内部数组的引用时,您应该使用 arraycopy() 创建一个数组的副本,然后返回该副本的引用。这样,您可以防止客户端直接操作(并且可能搞砸)内部数组。

main() 接下来调用 currentTimeMillis() 以毫秒值的形式返回当前时间。因为这个值不是人类可读的,你可能想要使用 java.util.Date 类(在第 10 章中讨论过)。 Date() 构造函数调用 current time millis(),其 toString() 方法将该值转换为可读的日期和时间。

main() 通过在 for 循环中演示 getProperty() 得出结论。这个循环遍历所有的表 8-2 的属性名,输出每个名称和值。

编译清单 8-18:【SystemDemo.java】贾瓦茨 T4。然后执行以下命令行:

java SystemDemo

当我在我的平台上运行这个应用时,它会生成以下输出:

86
92
78
65
52
43
72
98
81
Current time: 1353115138889
file.separator: \
java.class.path: .;C:\Program Files (x86)\QuickTime\QTSystem\QTJava.zip
java.home: C:\Program Files\Java\jre7
java.io.tmpdir: C:\Users\Owner\AppData\Local\Temp\
java.library.path: C:\Windows\system32;C:\Windows\Sun\Java\bin;C:\Windows\system32;C:\Windows;c:\Program Files (x86)\AMD APP\bin\x86_64;c:\Program Files (x86)\AMD APP\bin\x86;C:\Program Files\Common Files\Microsoft Shared\Windows Live;C:\Program Files (x86)\Common Files\Microsoft Shared\Windows Live;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Program Files (x86)\ATI Technologies\ATI.ACE\Core-Static;C:\Program Files (x86)\Windows Live\Shared;C:\Program Files\java\jdk1.7.0_06\bin;C:\Program Files (x86)\Borland\BCC55\bin;C:\android;C:\android\tools;C:\android\platform-tools;C:\Program Files (x86)\apache-ant-1.8.2\bin;C:\Program Files (x86)\QuickTime\QTSystem\;.
line.separator:

os.arch: amd64
os.name: Windows 7
path.separator: ;
user.dir: C:\prj\dev\ljfad2\ch08\code\SystemDemo

注意 line.separator 存储的是实际的行分隔符字符,而不是其表示形式(如 \r\n ),这也是为什么 line . separator:后面会出现一个空行的原因。

当您调用 System.in.read() 时,输入来源于在中分配给的 InputStream 实例所标识的源。类似地,当您调用 System.out.print() 或 System.err.println() 时,输出将被发送到分别分配给 out 或 err 的 PrintStream 实例所标识的目的地。

提示在 Android 设备上,首先在命令行执行 adb logcat ,可以查看发送到标准输出和标准错误的内容。 adb 是 Android SDK 中包含的工具之一。

Java 在中初始化,在标准输入设备重定向到文件时引用键盘或文件。类似地,Java 初始化 out / err 以在标准输出/错误设备重定向到文件时引用屏幕或文件。您可以通过调用 setIn() 、 setOut() 和 setErr() 来指定输入源、输出目的地和错误目的地——参见清单 8-19

清单 8-19 。以编程方式指定标准输入设备源和标准输出/错误设备目标

import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;

public class RedirectIO
{
   public static void main(String[] args) throws IOException
   {
      if (args.length != 3)
      {
         System.err.println("usage: java RedirectIO stdinfile stdoutfile stderrfile");
         return;
      }

      System.setIn(new FileInputStream(args[0]));
      System.setOut(new PrintStream(args[1]));
      System.setErr(new PrintStream(args[2]));

      int ch;
      while ((ch = System.in.read()) != -1)
         System.out.print((char) ch);

      System.err.println("Redirected error output");
   }
}

清单 8-19 展示了一个重定向应用,它让你指定(通过命令行参数)一个文件的名称,从该文件中 System.in.read() 获得其内容,以及 System.out.print() 和 System.err.println() 将它们的内容发送到的文件的名称。然后,它继续将标准输入复制到标准输出,然后演示将内容输出到标准错误。

注意 FileInputStream 提供对存储在由 args[0] 标识的文件中的输入字节序列的访问。类似地, PrintStream 提供对由 args[1] 和 args[2] 标识的文件的访问,这些文件将存储字节的输出和错误序列。

编译清单 8-19:【RedirectIO.java】T2【贾瓦茨。然后执行以下命令行:

java RedirectIO RedirectIO.java out.txt err.txt

该命令行不会在屏幕上产生可视输出。而是将 RedirectIO.java 的内容复制到 out.txt 中。它还将重定向的错误输出存储在 err.txt 中。

运行时间和流程

运行时为 Java 应用提供对其运行环境的访问。这个类的实例是通过调用它的运行时 getRuntime() 类方法获得的。

注意运行时类只有一个实例。

运行时声明了几个方法,这些方法也在系统中声明。例如,运行时声明了一个 void gc() 方法。在幕后,系统通过首先获取运行时实例,然后通过该实例调用该方法,来遵从其运行时对应方。比如系统的静态 void gc() 方法执行 Runtime.getRuntime()。GC();。

运行时也声明了没有系统对应的方法。下面的列表描述了其中的一些方法:

  • int available processors()返回虚拟机可用的处理器数量。此方法返回的最小值是 1。
  • long freeMemory() 返回虚拟机提供给应用的可用内存量(以字节为单位)。
  • long maxMemory() 返回虚拟机可以使用的最大内存量(以字节为单位)(或 Long。MAX_VALUE 无限制时)。
  • long total memory()返回虚拟机可用的内存总量(以字节为单位)。该数量可能会随着时间的推移而变化,具体取决于托管虚拟机的环境。

清单 8-20 展示了这些方法。

清单 8-20 。试验运行时方法

public class RuntimeDemo
{
   public static void main(String[] args)
   {
      Runtime rt = Runtime.getRuntime();
      System.out.println("Available processors: " + rt.availableProcessors());
      System.out.println("Free memory: "+ rt.freeMemory());
      System.out.println("Maximum memory: " + rt.maxMemory());
      System.out.println("Total memory: " + rt.totalMemory());
   }
}

编译清单 8-20:【RuntimeDemo.java】贾瓦茨 T4。然后执行以下命令行:

java RuntimeDemo

当我在我的平台上运行这个应用时,我观察到以下结果:

Available processors: 2
Free memory: 123997936
Maximum memory: 1849229312
Total memory: 124649472

一些运行时的方法专用于执行其他应用。例如,进程 exec(字符串程序)在单独的本地进程中执行名为程序的程序。新进程继承了方法调用者的环境,并且返回一个进程对象以允许与新进程通信。发生 I/O 错误时,抛出 IOException 。

提示 ProcessBuilder 是配置流程属性和运行流程的一种方便的替代方法。例如,Process p = new Process builder(" my command "," myArg ")。start();。

表 8-3 描述了过程的方法。

表 8-3。 处理方法

方法 描述
虚空毁灭() 终止调用进程并关闭任何关联的流。
int exit value()T2 返回由这个进程对象(新进程)表示的本机进程的退出值。当本机进程尚未终止时,抛出 IllegalThreadStateException 。
input stream getrststream() 返回一个输入流,该输入流连接到由这个进程对象表示的本地进程的标准错误流。该流从这个进程对象表示的进程的错误输出中获取数据。
input stream getinpertstream() 返回一个输入流,该输入流连接到由这个进程对象表示的本地进程的标准输出流。该流从这个进程对象所代表的进程的标准输出中获取数据。
输出流 getutputstream() 返回一个输出流,该输出流连接到由这个进程对象表示的本地进程的标准输入流。流的输出通过管道进入由这个进程对象表示的进程的标准输入。
int wait for()T3】 使调用线程等待与这个进程对象相关联的本地进程终止。返回进程的退出值。按照惯例,0 表示正常终止。当当前线程在等待时被另一个线程中断,这个方法抛出 InterruptedException 。

清单 8-21 演示了 exec(字符串程序)和三个进程的方法。

清单 8-21 。执行另一个应用并显示其标准输出/错误内容

import java.io.InputStream;
import java.io.IOException;

public class Exec
{
   public static void main(String[] args)
   {
      if (args.length != 1)
      {
         System.err.println("usage: java Exec program");
         return;
      }
      try
      {
         Process p = Runtime.getRuntime().exec(args[0]);
         // Obtaining process standard output.
         InputStream is = p.getInputStream();
         int _byte;
         while ((_byte = is.read()) != -1)
            System.out.print((char) _byte);
         // Obtaining process standard error.
         is = p.getErrorStream();
         while ((_byte = is.read()) != -1)
            System.out.print((char) _byte);
         System.out.println("Exit status: " + p.waitFor());
      }
      catch (InterruptedException ie)
      {
         assert false; // should never happen
      }
      catch (IOException ioe)
      {
         System.err.println("I/O error: " + ioe.getMessage());
      }
   }
}

在确认已经指定了一个命令行参数之后,清单 8-21 的 main() 方法试图运行这个参数所标识的应用。 IOException 在找不到应用或者发生其他 I/O 错误时抛出。

假设一切正常,调用 getInputStream() 以获得对输入流的引用,该输入流用于输入新调用的应用写入其标准输出流的字节(如果有的话)。这些字节随后被输出。

接下来, main() 调用 getErrorStream() 以获得对输入流的引用,该输入流用于输入新调用的应用写入其标准错误流的字节(如果有的话)。这些字节随后被输出。

注意为了防止混淆,请记住进程的 getInputStream() 方法用于读取新进程写入其输出流的字节,而进程的 getErrorStream() 方法用于读取新进程写入其错误流的字节。

最后, main() 调用 waitFor() 进行阻塞,直到新进程退出。如果新进程是基于 GUI 的应用,则该方法不会返回,直到您显式终止新进程。对于简单的基于命令行的应用, Exec 应该立即终止。

编译清单 8-21:【Exec.java】T2【贾瓦茨。然后执行识别应用的命令行,比如 java 应用启动器:

java Exec java

您应该观察到 java 的用法信息,后面跟有下面一行:

Exit status: 1

注意由于一些本机平台为标准输入和输出流提供有限的缓冲区大小,如果不能及时写入新进程的输入流或读取其输出流,可能会导致新进程阻塞甚至死锁。

练习

以下练习旨在测试您对第 8 章内容的理解:

  1. 什么是原始类型包装类?
  2. 识别 Java 的原始类型包装类。
  3. Java 为什么要提供原语类型包装类?
  4. 是非判断:字节是最小的原始类型包装类。
  5. 为什么要用 Character 类方法,而不是用 ch>= ' 0 '&&ch<= ' 9 '这样的表达式来判断一个字符是不是一个数字,一个字母,等等?
  6. 如何确定 double 变量 d 包含+无穷大还是-无穷大?
  7. 识别作为字节、字符和其他原始类型包装类的超类的类。
  8. 定义线程。
  9. Runnable 接口的用途是什么?
  10. 线程类的用途是什么?
  11. 是非判断:一个线程对象与多个线程相关联。
  12. 定义竞争条件。
  13. 什么是线程同步?
  14. 同步是如何实现的?
  15. 同步是如何工作的?
  16. 是非判断:类型为 long 或 double 的变量在 32 位虚拟机上不是原子的。
  17. 保留字挥发的目的是什么?
  18. 是非判断:对象的 wait() 方法可以从同步方法或块的外部调用。
  19. 定义死锁。
  20. ThreadLocal 类的用途是什么?
  21. InheritableThreadLocal 与 ThreadLocal 有何不同?
  22. 识别本章前面讨论的四个 java.lang 包系统类。
  23. 调用什么系统方法将一个数组复制到另一个数组?
  24. exec(字符串程序)方法完成什么?
  25. 进程的 getInputStream() 方法完成什么?
  26. 创建一个接受两个参数的 MultiPrint 应用:文本和一个表示计数的整数值。这个应用应该打印文本的副本,每行一份。
  27. 修改清单 8-4 的计数线程应用,将两个启动的线程标记为守护线程。运行结果应用时会发生什么?
  28. 修改清单 8-4 的计数线程应用,增加当用户按回车键时停止两个线程计数的逻辑。新的 StopCountingThreads 应用的默认主线程应该在终止前调用 System.in.read() ,并在该方法调用返回后将 true 赋给名为 stopped 的变量。在每次循环迭代开始时,每个计数线程都应该测试这个变量,看它是否包含 true,只有当变量包含 false 时才继续循环。
  29. 创建一个 EVDump 应用,将所有环境变量(不是系统属性)转储到标准输出。

摘要

java.lang 包包括布尔、字节、字符、双精度、浮点、整数、长、短。这些类被称为原始类型包装类,因为它们的实例将自己包装在原始类型的值周围。

Java 提供了这八个原始类型包装类,这样原始类型的值可以存储在集合中,比如列表、集合和映射。此外,这些类提供了一个将有用的常量和类方法与基本类型相关联的好地方。

应用通过线程执行,这些线程是应用代码的独立执行路径。虚拟机为每个线程提供了自己的方法调用堆栈,以防止线程相互干扰。

Java 通过其线程 API 支持线程。这个 API 由 java.lang 包中的一个接口( Runnable )和四个类(线程、线程组、 ThreadLocal 和 InheritableThreadLocal )组成。 ThreadGroup 不如这些其他类型有用。

在整个执行过程中,每个线程都与其他线程相隔离,因为每个线程都有自己的方法调用堆栈。然而,当线程访问和操作共享数据时,它们仍然会相互干扰。这种干扰会破坏共享数据,导致应用失败。

通过使用线程同步可以避免损坏,这样一次只有一个线程可以在临界区内执行,临界区是一个必须以原子方式执行的代码区域。它在源代码级别被标识为同步方法或同步块。

通过在方法返回类型之前向方法头添加保留字 synchronized ,可以在方法级别同步访问。还可以通过指定对象 ) { /同步语句/ } 来同步对语句块的访问。

同步是根据监视器和锁实现的。一个监视器是一个并发结构,用于控制对一个临界区的访问。是一个令牌,在监视器允许线程在监视器的临界区内执行之前,线程必须获取这个令牌。

同步是支持互斥或者互斥结合线程通信所必需的。然而,当唯一的目的是在线程之间通信时,存在同步的替代方案。这个备选项是保留字易变字。

对象的 wait() 、 notify() 和 notifyAll() 方法支持一种线程通信形式,其中一个线程主动等待某个条件(继续执行的先决条件)出现,此时另一个线程通知等待的线程它可以继续执行。 wait() 使其调用线程等待一个对象的监视器, notify() 和 notifyAll() 唤醒一个或所有等待监视器的线程。

太多的同步可能会有问题。如果不小心的话,您可能会遇到这样的情况:锁被多个线程获取,没有一个线程持有自己的锁,而是持有其他线程所需的锁,没有一个线程能够进入并在以后退出其临界区以释放其持有的锁,因为其他线程持有该临界区的锁。这种情况被称为死锁

有时您会希望将每线程数据与一个线程相关联。虽然您可以使用局部变量来完成这项任务,但是您只能在局部变量存在时才能这样做。您可以使用实例字段将这些数据保存更长时间,但是这样您就必须处理同步问题。Java 提供了 ThreadLocal 类作为简单(并且非常方便)的替代。

每个 ThreadLocal 实例描述一个线程本地变量,该变量为每个访问该变量的线程提供一个单独的存储槽。可以把线程局部变量想象成一个多时隙变量,其中每个线程可以在同一个变量中存储不同的值。每个线程只看到自己的值,不知道其他线程在这个变量中有自己的值。

存储在线程局部变量中的值是不相关的。当一个新线程被创建时,它获得一个包含 initialValue() 值的新存储槽。然而,通过使用 InheritableThreadLocal 类,您可以将一个值从一个父线程(一个创建另一个线程的线程)传递给一个子线程(一个被创建的线程)。

java.lang 包包含四个面向系统的类:系统、运行时、进程和进程构建器。这些类使您可以获取运行应用的系统的信息,并执行各种系统任务。

System 是一个工具类,它在、 out 和 err 类字段中声明了,这些字段分别引用当前的标准输入、标准输出和标准误差流。第一个字段属于类型 InputStream ,最后两个字段属于类型 PrintStream 。

System 还声明了提供对当前时间(以毫秒为单位)、系统属性值、环境变量值和其他类型的系统信息的访问的类方法。此外,它声明了支持系统任务的类方法,例如将一个数组复制到另一个数组。

运行时为 Java 应用提供对其运行环境的访问。这个类的实例是通过调用它的运行时 getRuntime() 类方法获得的。然后您可以调用各种环境访问方法,包括在系统中声明的方法。

一些运行时的方法执行其他应用。例如,进程 exec(字符串程序)在单独的本机进程中执行程序。新进程继承了方法调用方的环境;返回一个进程对象,以允许与新进程进行通信。

本章完成了我对 Java 基本 API 的介绍。在第 9 章中,我开始通过关注集合框架和经典集合 API 来探索 Java 的实用 API。*