Java 中文官方教程 2022 版(九)

2024-05-24 15:30:30 浏览数 (1)

链接,符号或其他

原文:docs.oracle.com/javase/tutorial/essential/io/links.html

如前所述,java.nio.file包,特别是Path类是“链接感知”的。每个Path方法都会检测遇到符号链接时该做什么,或者提供一个选项,使您能够配置遇到符号链接时的行为。

到目前为止的讨论一直是关于符号或链接,但一些文件系统也支持硬链接。硬链接比符号链接更受限制,具体如下:

  • 链接的目标必须存在。
  • 通常不允许在目录上创建硬链接。
  • 硬链接不允许跨分区或卷。因此,它们不能存在于不同文件系统之间。
  • 一个硬链接看起来和行为都像一个普通文件,所以它们可能很难找到。
  • 从所有方面来看,硬链接与原始文件是相同的实体。它们具有相同的文件权限、时间戳等。所有属性都是相同的。

由于这些限制,硬链接不像符号链接那样经常使用,但Path方法与硬链接无缝配合。

有几种方法专门处理链接,并在以下部分中介绍:

  • 创建符号链接
  • 创建硬链接
  • 检测符号链接
  • 查找链接的目标

创建符号链接

如果你的文件系统支持,你可以使用createSymbolicLink(Path, Path, FileAttribute<?>)方法创建一个符号链接。第二个Path参数表示目标文件或目录,可能存在也可能不存在。以下代码片段创建了一个带有默认权限的符号链接:

代码语言:javascript复制
Path newLink = ...;
Path target = ...;
try {
    Files.createSymbolicLink(newLink, target);
} catch (IOException x) {
    System.err.println(x);
} catch (UnsupportedOperationException x) {
    // Some file systems do not support symbolic links.
    System.err.println(x);
}

FileAttributes vararg 使您能够指定在创建链接时原子设置的初始文件属性。但是,这个参数是为将来使用而设计的,目前尚未实现。

创建硬链接

你可以使用createLink(Path, Path)方法创建一个到现有文件的硬(或常规)链接。第二个Path参数定位现有文件,它必须存在,否则会抛出NoSuchFileException。以下代码片段展示了如何创建链接:

代码语言:javascript复制
Path newLink = ...;
Path existingFile = ...;
try {
    Files.createLink(newLink, existingFile);
} catch (IOException x) {
    System.err.println(x);
} catch (UnsupportedOperationException x) {
    // Some file systems do not
    // support adding an existing
    // file to a directory.
    System.err.println(x);
}

检测符号链接

要确定Path实例是否是符号链接,可以使用isSymbolicLink(Path)方法。以下代码片段展示了如何:

代码语言:javascript复制
Path file = ...;
boolean isSymbolicLink =
    Files.isSymbolicLink(file);

欲了解更多信息,请参阅管理元数据。

查找链接的目标

通过使用readSymbolicLink(Path)方法,您可以获取符号链接的目标,如下所示:

代码语言:javascript复制
Path link = ...;
try {
    System.out.format("Target of link"  
        " '%s' is '%s'%n", link,
        Files.readSymbolicLink(link));
} catch (IOException x) {
    System.err.println(x);
}

如果Path不是一个符号链接,该方法会抛出NotLinkException

遍历文件树

原文:docs.oracle.com/javase/tutorial/essential/io/walk.html

您是否需要创建一个应用程序,递归访问文件树中的所有文件?也许您需要删除树中的每个.class文件,或者找到在过去一年中未被访问的每个文件。您可以通过FileVisitor接口实现这一点。

本节涵盖以下内容:

  • FileVisitor 接口
  • 启动过程
  • 创建 FileVisitor 时的注意事项
  • 控制流程
  • 示例

FileVisitor 接口

要遍历文件树,首先需要实现一个FileVisitorFileVisitor指定了在遍历过程的关键点上所需的行为:当访问文件时,在访问目录之前,在访问目录之后,或者当发生故障时。该接口有四个方法对应于这些情况:

  • preVisitDirectory – 在访问目录条目之前调用。
  • postVisitDirectory – 在访问目录中的所有条目之后调用。如果遇到任何错误,特定异常将传递给该方法。
  • visitFile – 在访问文件时调用。文件的BasicFileAttributes被传递给该方法,或者您可以使用 file attributes 包来读取特定的属性集。例如,您可以选择读取文件的DosFileAttributeView来确定文件是否设置了“hidden”位。
  • visitFileFailed – 当无法访问文件时调用。特定异常被传递给该方法。您可以选择是否抛出异常,将其打印到控制台或日志文件等。

如果您不需要实现所有四个FileVisitor方法,而是扩展SimpleFileVisitor类,而不是实现FileVisitor接口。这个类实现了FileVisitor接口,访问树中的所有文件,并在遇到错误时抛出IOError。您可以扩展这个类,并仅覆盖您需要的方法。

这是一个扩展SimpleFileVisitor以打印文件树中所有条目的示例。它打印条目,无论条目是常规文件、符号链接、目录还是其他类型的“未指定”文件。它还打印每个文件的字节大小。遇到的任何异常都会打印到控制台。

FileVisitor方法以粗体显示:

代码语言:javascript复制
import static java.nio.file.FileVisitResult.*;

public static class PrintFiles
    extends SimpleFileVisitor<Path> {

    // Print information about
    // each type of file.
    @Override
    public FileVisitResult visitFile(Path file,
                                   BasicFileAttributes attr) {
        if (attr.isSymbolicLink()) {
            System.out.format("Symbolic link: %s ", file);
        } else if (attr.isRegularFile()) {
            System.out.format("Regular file: %s ", file);
        } else {
            System.out.format("Other: %s ", file);
        }
        System.out.println("("   attr.size()   "bytes)");
        return CONTINUE;
    }

    // Print each directory visited.
    @Override
    public FileVisitResult postVisitDirectory(Path dir,
                                          IOException exc) {
        System.out.format("Directory: %s%n", dir);
        return CONTINUE;
    }

    // If there is some error accessing
    // the file, let the user know.
    // If you don't override this method
    // and an error occurs, an IOException 
    // is thrown.
    @Override
    public FileVisitResult visitFileFailed(Path file,
                                       IOException exc) {
        System.err.println(exc);
        return CONTINUE;
    }
}

启动过程

一旦您实现了您的FileVisitor,如何启动文件遍历?Files类中有两个walkFileTree方法。

  • walkFileTree(Path, FileVisitor)
  • walkFileTree(Path, Set<FileVisitOption>, int, FileVisitor)

第一个方法只需要一个起始点和您的FileVisitor的实例。您可以按以下方式调用PrintFiles文件访问者:

代码语言:javascript复制
Path startingDir = ...;
PrintFiles pf = new PrintFiles();
Files.walkFileTree(startingDir, pf);

第二个walkFileTree方法还允许您额外指定访问级别的限制和一组FileVisitOption枚举。如果您希望确保此方法遍历整个文件树,您可以为最大深度参数指定Integer.MAX_VALUE

您可以指定FileVisitOption枚举FOLLOW_LINKS,表示应该跟随符号链接。

此代码片段显示了如何调用四参数方法:

代码语言:javascript复制
import static java.nio.file.FileVisitResult.*;

Path startingDir = ...;

EnumSet<FileVisitOption> opts = EnumSet.of(FOLLOW_LINKS);

Finder finder = new Finder(pattern);
Files.walkFileTree(startingDir, opts, Integer.MAX_VALUE, finder);

创建FileVisitor时的注意事项

文件树以深度优先方式遍历,但不能假设子目录的访问顺序。

如果您的程序将更改文件系统,您需要仔细考虑如何实现您的FileVisitor

例如,如果您正在编写递归删除,您首先删除目录中的文件,然后再删除目录本身。在这种情况下,您在postVisitDirectory中删除目录。

如果您正在编写递归复制,您需要在preVisitDirectory中创建新目录,然后尝试将文件复制到其中(在visitFiles中)。如果您想要保留源目录的属性(类似于 UNIX 的cp -p命令),您需要在文件被复制后,在postVisitDirectory中执行此操作。Copy示例展示了如何做到这一点。

如果您正在编写文件搜索,您可以在visitFile方法中执行比较。此方法找到所有符合您条件的文件,但不会找到目录。如果您想要找到文件和目录,您还必须在preVisitDirectorypostVisitDirectory方法中执行比较。Find示例展示了如何做到这一点。

你需要决定是否要遵循符号链接。例如,如果你正在删除文件,跟随符号链接可能不明智。如果你正在复制文件树,你可能希望允许它。默认情况下,walkFileTree不会遵循符号链接。

对于文件,会调用visitFile方法。如果你指定了FOLLOW_LINKS选项,并且你的文件树有一个指向父目录的循环链接,循环目录将在visitFileFailed方法中报告,带有FileSystemLoopException。以下代码片段显示了如何捕获循环链接,并来自于Copy示例:

代码语言:javascript复制
@Override
public FileVisitResult
    visitFileFailed(Path file,
        IOException exc) {
    if (exc instanceof FileSystemLoopException) {
        System.err.println("cycle detected: "   file);
    } else {
        System.err.format("Unable to copy:"   " %s: %s%n", file, exc);
    }
    return CONTINUE;
}

这种情况只会在程序遵循符号链接时发生。

控制流程

也许你想要遍历文件树查找特定目录,并且在找到后希望进程终止。也许你想要跳过特定目录。

FileVisitor方法返回一个FileVisitResult值。你可以通过在FileVisitor方法中返回的值来中止文件遍历过程或控制是否访问目录:

  • CONTINUE – 表示文件遍历应该继续。如果preVisitDirectory方法返回CONTINUE,则会访问该目录。
  • TERMINATE – 立即中止文件遍历。在返回此值后不会调用更多的文件遍历方法。
  • SKIP_SUBTREE – 当preVisitDirectory返回此值时,指定的目录及其子目录将被跳过。这个分支将从树中“剪掉”。
  • SKIP_SIBLINGS – 当preVisitDirectory返回此值时,指定的目录不会被访问,postVisitDirectory不会被调用,也不会访问更多未访问的兄弟节点。如果从postVisitDirectory方法返回,不会访问更多的兄弟节点。基本上,在指定的目录中不会发生更多的事情。

在这段代码片段中,任何名为SCCS的目录都会被跳过:

代码语言:javascript复制
import static java.nio.file.FileVisitResult.*;

public FileVisitResult
     preVisitDirectory(Path dir,
         BasicFileAttributes attrs) {
    (if (dir.getFileName().toString().equals("SCCS")) {
         return SKIP_SUBTREE;
    }
    return CONTINUE;
}

在这段代码片段中,一旦找到特定文件,文件名就会被打印到标准输出,并且文件遍历会终止:

代码语言:javascript复制
import static java.nio.file.FileVisitResult.*;

// The file we are looking for.
Path lookingFor = ...;

public FileVisitResult
    visitFile(Path file,
        BasicFileAttributes attr) {
    if (file.getFileName().equals(lookingFor)) {
        System.out.println("Located file: "   file);
        return TERMINATE;
    }
    return CONTINUE;
}

示例

以下示例演示了文件遍历机制:

  • Find – 递归查找符合特定通配符模式的文件和目录。此示例在查找文件中讨论。
  • Chmod – 递归更改文件树上的权限(仅适用于 POSIX 系统)。
  • Copy – 递归复制文件树。
  • WatchDir – 演示了监视目录中已创建、删除或修改的文件的机制。使用-r选项调用此程序会监视整个树的更改。有关文件通知服务的更多信息,请参见监视目录的更改。

查找文件

原文:docs.oracle.com/javase/tutorial/essential/io/find.html

如果你曾经使用过 shell 脚本,你很可能使用过模式匹配来定位文件。事实上,你可能已经广泛使用了它。如果你还没有使用过,模式匹配使用特殊字符创建模式,然后文件名可以与该模式进行比较。例如,在大多数 shell 脚本中,星号,*,匹配任意数量的字符。例如,以下命令列出当前目录中以.html结尾的所有文件:

代码语言:javascript复制
% ls *.html

java.nio.file包为这一有用功能提供了编程支持。每个文件系统实现都提供了一个PathMatcher。你可以通过在FileSystem类中使用getPathMatcher(String)方法来检索文件系统的PathMatcher。以下代码片段获取默认文件系统的路径匹配器:

代码语言:javascript复制
String pattern = ...;
PathMatcher matcher =
    FileSystems.getDefault().getPathMatcher("glob:"   pattern);

传递给getPathMatcher的字符串参数指定语法风格和要匹配的模式。本示例指定了glob语法。如果你不熟悉 glob 语法,请参阅什么是 Glob。

Glob 语法易于使用和灵活,但如果你喜欢,也可以使用正则表达式,或regex语法。有关正则表达式的更多信息,请参阅正则表达式课程。一些文件系统实现可能支持其他语法。

如果你想使用其他形式的基于字符串的模式匹配,你可以创建自己的PathMatcher类。本页中的示例使用 glob 语法。

一旦你创建了PathMatcher实例,你就可以准备好根据它匹配文件。PathMatcher接口有一个方法,matches,它接受一个Path参数并返回一个布尔值:它要么匹配模式,要么不匹配。以下代码片段查找以.java.class结尾的文件并将这些文件打印到标准输出:

代码语言:javascript复制
PathMatcher matcher =
    FileSystems.getDefault().getPathMatcher("glob:*.{java,class}");

Path filename = ...;
if (matcher.matches(filename)) {
    System.out.println(filename);
}

递归模式匹配

搜索与特定模式匹配的文件与遍历文件树密切相关。有多少次你知道一个文件在某处在文件系统上,但在哪里?或者也许你需要找到文件树中具有特定文件扩展名的所有文件。

Find示例正是如此。Find类似于 UNIX 的find实用程序,但功能更简化。你可以扩展这个示例以包含其他功能。例如,find实用程序支持-prune标志来排除搜索中的整个子树。你可以通过在preVisitDirectory方法中返回SKIP_SUBTREE来实现该功能。要实现-L选项,即跟随符号链接,你可以使用四个参数的walkFileTree方法,并传入FOLLOW_LINKS枚举(但请确保在visitFile方法中测试循环链接)。

要运行 Find 应用程序,请使用以下格式:

代码语言:javascript复制
% java Find <path> -name "<glob_pattern>"

模式被放置在引号内,以防止 shell 解释任何通配符。例如:

代码语言:javascript复制
% java Find . -name "*.html"

这里是Find示例的源代码:

代码语言:javascript复制
/**
 * Sample code that finds files that match the specified glob pattern.
 * For more information on what constitutes a glob pattern, see
 * https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob
 *
 * The file or directories that match the pattern are printed to
 * standard out.  The number of matches is also printed.
 *
 * When executing this application, you must put the glob pattern
 * in quotes, so the shell will not expand any wild cards:
 *              java Find . -name "*.java"
 */

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
import static java.nio.file.FileVisitResult.*;
import static java.nio.file.FileVisitOption.*;
import java.util.*;

public class Find {

    public static class Finder
        extends SimpleFileVisitor<Path> {

        private final PathMatcher matcher;
        private int numMatches = 0;

        Finder(String pattern) {
            matcher = FileSystems.getDefault()
                    .getPathMatcher("glob:"   pattern);
        }

        // Compares the glob pattern against
        // the file or directory name.
        void find(Path file) {
            Path name = file.getFileName();
            if (name != null && matcher.matches(name)) {
                numMatches  ;
                System.out.println(file);
            }
        }

        // Prints the total number of
        // matches to standard out.
        void done() {
            System.out.println("Matched: "
                  numMatches);
        }

        // Invoke the pattern matching
        // method on each file.
        @Override
        public FileVisitResult visitFile(Path file,
                BasicFileAttributes attrs) {
            find(file);
            return CONTINUE;
        }

        // Invoke the pattern matching
        // method on each directory.
        @Override
        public FileVisitResult preVisitDirectory(Path dir,
                BasicFileAttributes attrs) {
            find(dir);
            return CONTINUE;
        }

        @Override
        public FileVisitResult visitFileFailed(Path file,
                IOException exc) {
            System.err.println(exc);
            return CONTINUE;
        }
    }

    static void usage() {
        System.err.println("java Find <path>"  
            " -name "<glob_pattern>"");
        System.exit(-1);
    }

    public static void main(String[] args)
        throws IOException {

        if (args.length < 3 || !args[1].equals("-name"))
            usage();

        Path startingDir = Paths.get(args[0]);
        String pattern = args[2];

        Finder finder = new Finder(pattern);
        Files.walkFileTree(startingDir, finder);
        finder.done();
    }
}

递归遍历文件树的内容在遍历文件树中有详细介绍。

监视目录更改

原文:docs.oracle.com/javase/tutorial/essential/io/notification.html

你是否曾经发现自己正在编辑一个文件,使用 IDE 或另一个编辑器,并且出现一个对话框通知您文件系统中的一个打开文件已更改并需要重新加载?或者,就像 NetBeans IDE 一样,应用程序悄悄地更新文件而不通知您。以下示例对话框显示了使用免费编辑器jEdit时的通知外观:

示例 jEdit 对话框显示:以下文件已被另一个程序更改。示例 jEdit 对话框显示:以下文件已被另一个程序更改。

jEdit 对话框显示检测到修改的文件

要实现此功能,称为文件更改通知,程序必须能够检测到文件系统上相关目录发生的变化。一种方法是轮询文件系统以查找更改,但这种方法效率低下。它不适用于具有数百个打开文件或目录需要监视的应用程序。

java.nio.file包提供了一个文件更改通知 API,称为 Watch Service API。此 API 使您能够向观察服务注册目录(或目录)。在注册时,您告诉服务您感兴趣的事件类型:文件创建、文件删除或文件修改。当服务检测到感兴趣的事件时,它会转发给注册的进程。注册的进程有一个专用于监视其注册事件的线程(或线程池)。当事件发生时,根据需要进行处理。

本节涵盖以下内容:

  • 观察服务概述
  • 试一试
  • 创建 Watch Service 并注册事件
  • 处理事件
  • 获取文件名
  • 何时使用和不使用此 API

观察服务概述

WatchService API 相当低级,允许您自定义它。您可以直接使用它,或者您可以选择在此机制之上创建一个高级 API,以使其适合您的特定需求。

下面是实现观察服务所需的基本步骤:

  • 为文件系统创建一个WatchService“观察者”。
  • 对于要监视的每个目录,请将其注册到观察者中。在注册目录时,指定要接收通知的事件类型。您为每个注册的目录收到一个WatchKey实例。
  • 实现一个无限循环以等待传入事件。当事件发生时,键被标记并放入观察者队列中。
  • 从观察者队列中检索键。您可以从键中获取文件名。
  • 检索键的每个待处理事件(可能有多个事件)并根据需要处理。
  • 重置键,并恢复等待事件。
  • 关闭服务:当线程退出或调用其closed方法关闭服务时,监视服务将退出。

WatchKeys是线程安全的,可以与java.nio.concurrent包一起使用。您可以为此目的专门分配一个线程池。

试一试

由于此 API 更为高级,请在继续之前先尝试一下。将WatchDir示例保存到您的计算机上,并对其进行编译。创建一个将传递给示例的test目录。WatchDir使用单个线程处理所有事件,因此在等待事件时会阻止键盘输入。要么在单独的窗口中运行程序,要么在后台运行,如下所示:

代码语言:javascript复制
java WatchDir test &

test目录中创建、删除和编辑文件。当发生任何这些事件时,将在控制台上打印消息。完成后,删除test目录,WatchDir退出。或者,如果您愿意,也可以手动终止进程。

您还可以通过指定-r选项来监视整个文件树。当您指定-r时,WatchDir遍历文件树,将每个目录注册到监视服务中。

创建监视服务并注册事件

第一步是通过FileSystem类中的newWatchService方法创建一个新的WatchService,如下所示:

代码语言:javascript复制
WatchService watcher = FileSystems.getDefault().newWatchService();

接下来,向监视服务注册一个或多个对象。任何实现了Watchable接口的对象都可以注册。Path类实现了Watchable接口,因此要监视的每个目录都被注册为一个Path对象。

与任何Watchable一样,Path类实现了两个register方法。本页使用了两个参数版本的register(WatchService, WatchEvent.Kind<?>...)。(三个参数版本接受一个WatchEvent.Modifier,目前尚未实现。)

在向监视服务注册对象时,您需要指定要监视的事件类型。支持的StandardWatchEventKinds事件类型如下:

  • ENTRY_CREATE – 创建目录条目。
  • ENTRY_DELETE – 删除目录条目。
  • ENTRY_MODIFY – 修改目录条目。
  • OVERFLOW – 表示事件可能已丢失或被丢弃。您无需注册OVERFLOW事件即可接收它。

以下代码片段显示了如何为所有三种事件类型注册Path实例:

代码语言:javascript复制
import static java.nio.file.StandardWatchEventKinds.*;

Path dir = ...;
try {
    WatchKey key = dir.register(watcher,
                           ENTRY_CREATE,
                           ENTRY_DELETE,
                           ENTRY_MODIFY);
} catch (IOException x) {
    System.err.println(x);
}

处理事件

事件处理循环中事件的顺序如下:

  1. 获取一个监视键。提供了三种方法:
    • poll – 如果可用,则返回一个排队的键。如果不可用,则立即返回null值。
    • poll(long, TimeUnit) – 如果有排队的键可用,则返回一个。如果没有立即可用的排队键,则程序将等待指定的时间。TimeUnit参数确定指定的时间是纳秒、毫秒还是其他时间单位。
    • take – 返回一个排队的键。如果没有可用的排队键,此方法将等待。
  2. 处理键的待处理事件。您从pollEvents方法中获取WatchEventsList
  3. 使用kind方法检索事件的类型。无论键注册了什么事件,都有可能收到OVERFLOW事件。您可以选择处理溢出或忽略它,但应该对其进行测试。
  4. 检索与事件关联的文件名。文件名存储为事件的上下文,因此使用context方法来检索它。
  5. 处理键的事件后,需要通过调用reset将键放回ready状态。如果此方法返回false,则键不再有效,循环可以退出。这一步非常重要。如果未调用reset,则此键将不会接收到进一步的事件。

观察键具有状态。在任何给定时间,其状态可能是以下之一:

  • Ready表示键已准备好接受事件。创建时,键处于准备状态。
  • Signaled表示有一个或多个事件排队。一旦键被标记,它就不再处于准备状态,直到调用reset方法。
  • Invalid表示键不再活动。当发生以下事件之一时,会出现此状态:
    • 进程通过使用cancel方法显式取消键。
    • 目录变得无法访问。
    • 观察服务已经被关闭。

这里是一个事件处理循环的示例。它取自于 Email 示例,该示例监视一个目录,等待新文件出现。当新文件可用时,通过使用 probeContentType(Path) 方法来检查它是否是一个 text/plain 文件。意图是将 text/plain 文件发送到一个别名,但具体实现细节留给读者。

Watch service API 特定的方法用粗体显示:

代码语言:javascript复制
for (;;) {

    // wait for key to be signaled
    WatchKey key;
    try {
        key = watcher.take();
    } catch (InterruptedException x) {
        return;
    }

    for (WatchEvent<?> event: key.pollEvents()) {
        WatchEvent.Kind<?> kind = event.kind();

        // This key is registered only
        // for ENTRY_CREATE events,
        // but an OVERFLOW event can
        // occur regardless if events
        // are lost or discarded.
        if (kind == OVERFLOW) {
            continue;
        }

        // The filename is the
        // context of the event.
        WatchEvent<Path> ev = (WatchEvent<Path>)event;
        Path filename = ev.context();

        // Verify that the new
        //  file is a text file.
        try {
            // Resolve the filename against the directory.
            // If the filename is "test" and the directory is "foo",
            // the resolved name is "test/foo".
            Path child = dir.resolve(filename);
            if (!Files.probeContentType(child).equals("text/plain")) {
                System.err.format("New file '%s'"  
                    " is not a plain text file.%n", filename);
                continue;
            }
        } catch (IOException x) {
            System.err.println(x);
            continue;
        }

        // Email the file to the
        //  specified email alias.
        System.out.format("Emailing file %s%n", filename);
        //Details left to reader....
    }

    // Reset the key -- this step is critical if you want to
    // receive further watch events.  If the key is no longer valid,
    // the directory is inaccessible so exit the loop.
    boolean valid = key.reset();
    if (!valid) {
        break;
    }
}

检索文件名

文件名是从事件上下文中检索的。Email 示例使用以下代码检索文件名:

代码语言:javascript复制
WatchEvent<Path> ev = (WatchEvent<Path>)event;
Path filename = ev.context();

当你编译 Email 示例时,会生成以下错误:

代码语言:javascript复制
Note: Email.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

这个错误是由将 WatchEvent<T> 强制转换为 WatchEvent<Path> 的代码行引起的。WatchDir 示例通过创建一个抑制未经检查警告的实用 cast 方法来避免这个错误,如下所示:

代码语言:javascript复制
@SuppressWarnings("unchecked")
static <T> WatchEvent<T> cast(WatchEvent<?> event) {
    return (WatchEvent<Path>)event;
}

如果你对 @SuppressWarnings 语法不熟悉,请参见 Annotations。

何时使用和不使用这个 API

Watch Service API 适用于需要通知文件更改事件的应用程序。它非常适合任何可能有许多打开文件并需要确保文件与文件系统同步的应用程序,比如编辑器或 IDE。它也非常适合监视目录的应用服务器,也许等待 .jsp.jar 文件的出现,以便部署它们。

这个 API 是为了索引硬盘而设计的。大多数文件系统实现都原生支持文件更改通知。Watch Service API 利用了这种支持(如果可用)。然而,当文件系统不支持这种机制时,Watch Service 将轮询文件系统,等待事件发生。

其他有用的方法

原文:docs.oracle.com/javase/tutorial/essential/io/misc.html

本课程中未涵盖的一些有用方法在此处介绍。本节涵盖以下内容:

  • 确定 MIME 类型
  • 默认文件系统
  • 路径字符串分隔符
  • 文件系统的文件存储器

确定 MIME 类型

要确定文件的 MIME 类型,您可能会发现probeContentType(Path)方法很有用。例如:

代码语言:javascript复制
try {
    String type = Files.probeContentType(filename);
    if (type == null) {
        System.err.format("'%s' has an"   " unknown filetype.%n", filename);
    } else if (!type.equals("text/plain") {
        System.err.format("'%s' is not"   " a plain text file.%n", filename);
        continue;
    }
} catch (IOException x) {
    System.err.println(x);
}

注意,如果无法确定内容类型,probeContentType会返回 null。

此方法的实现高度依赖于平台,并不是绝对可靠的。内容类型由平台的默认文件类型检测器确定。例如,如果检测器根据.class扩展名确定文件的内容类型为application/x-java,可能会被欺骗。

如果默认的方法不符合您的需求,您可以提供自定义的FileTypeDetector

电子邮件示例使用probeContentType方法。

默认文件系统

要检索默认文件系统,请使用getDefault方法。通常,此FileSystems方法(注意是复数形式)链接到FileSystem方法之一(注意是单数形式),如下所示:

代码语言:javascript复制
PathMatcher matcher =
    FileSystems.getDefault().getPathMatcher("glob:*.*");

路径字符串分隔符

POSIX 文件系统的路径分隔符是正斜杠/,Microsoft Windows 的路径分隔符是反斜杠。其他文件系统可能使用其他分隔符。要检索默认文件系统的Path分隔符,可以使用以下方法之一:

代码语言:javascript复制
String separator = File.separator;
String separator = FileSystems.getDefault().getSeparator();

getSeparator方法也用于检索任何可用文件系统的路径分隔符。

文件系统的文件存储器

文件系统有一个或多个文件存储器来保存其文件和目录。文件存储器代表底层存储设备。在 UNIX 操作系统中,每个挂载的文件系统都由一个文件存储器表示。在 Microsoft Windows 中,每个卷都由一个文件存储器表示:C:D:等等。

要检索文件系统的所有文件存储器列表,可以使用getFileStores方法。此方法返回一个Iterable,允许您使用增强的 for 语句遍历所有根目录。

代码语言:javascript复制
for (FileStore store: FileSystems.getDefault().getFileStores()) {
   ...
}

如果要检索特定文件所在的文件存储器,请使用Files类中的getFileStore方法,如下所示:

代码语言:javascript复制
Path file = ...;
FileStore store= Files.getFileStore(file);

DiskUsage示例使用getFileStores方法。

传统文件 I/O 代码

原文:docs.oracle.com/javase/tutorial/essential/io/legacy.html

与旧代码的互操作性

在 Java SE 7 发布之前,java.io.File 类是文件 I/O 的机制,但它有一些缺点。

  • 许多方法在失败时不会抛出异常,因此无法获得有用的错误消息。例如,如果文件删除失败,程序将收到“删除失败”,但不知道是因为文件不存在、用户没有权限还是其他问题。
  • rename 方法在各个平台上的工作不一致。
  • 没有对符号链接的真正支持。
  • 需要更多对元数据的支持,如文件权限、文件所有者和其他安全属性。
  • 访问文件元数据效率低下。
  • 许多 File 方法不具备可扩展性。在服务器上请求大型目录列表可能导致挂起。大型目录也可能导致内存资源问题,导致拒绝服务。
  • 不可能编写可靠的代码,可以递归遍历文件树,并在存在循环符号链接时做出适当响应。

也许您有使用 java.io.File 的旧代码,并希望最小影响地利用 java.nio.file.Path 功能。

java.io.File 类提供了 toPath 方法,将旧式 File 实例转换为 java.nio.file.Path 实例,如下所示:

代码语言:javascript复制
Path input = file.toPath();

然后,您可以利用 Path 类提供的丰富功能集。

例如,假设您有一些删除文件的代码:

代码语言:javascript复制
file.delete();

您可以修改此代码以使用 Files.delete 方法,如下所示:

代码语言:javascript复制
Path fp = file.toPath();
Files.delete(fp);

相反,Path.toFile 方法为 Path 对象构造一个 java.io.File 对象。

将 java.io.File 功能映射到 java.nio.file

由于 Java SE 7 发布中的文件 I/O 实现已完全重新架构,因此不能将一个方法替换为另一个方法。如果您想使用 java.nio.file 包提供的丰富功能,最简单的解决方案是使用前一节中建议的 File.toPath 方法。但是,如果您不想使用该方法或该方法不符合您的需求,您必须重写文件 I/O 代码。

两个 API 之间没有一对一对应关系,但以下表格给出了 java.io.File API 中的功能在 java.nio.file API 中的映射,并告诉您可以在哪里获取更多信息。

java.io.File 功能

java.nio.file 功能

教程覆盖范围

java.io.File

java.nio.file.Path

Path 类

java.io.RandomAccessFile

SeekableByteChannel 功能。

随机访问文件

File.canRead、canWrite、canExecute

Files.isReadable、Files.isWritable 和 Files.isExecutable。在 UNIX 文件系统上,使用 管理元数据(文件和文件存储属性) 包来检查九个文件权限。

检查文件或目录 管理元数据

File.isDirectory()、File.isFile() 和 File.length()

Files.isDirectory(Path, LinkOption...)、Files.isRegularFile(Path, LinkOption...) 和 Files.size(Path)

管理元数据

File.lastModified() 和 File.setLastModified(long)

Files.getLastModifiedTime(Path, LinkOption...) 和 Files.setLastMOdifiedTime(Path, FileTime)

管理元数据

设置各种属性的 File 方法:setExecutable、setReadable、setReadOnly、setWritable

这些方法被 Files 方法 setAttribute(Path, String, Object, LinkOption...) 替代。

管理元数据

new File(parent, "newfile")

parent.resolve("newfile")

路径操作

File.renameTo

Files.move

移动文件或目录

File.delete

Files.delete

删除文件或目录

File.createNewFile

Files.createFile

创建文件

File.deleteOnExit

由 createFile 方法中指定的 DELETE_ON_CLOSE 选项替代。

创建文件

| File.createTempFile | Files.createTempFile(Path, String, FileAttributes<?>)Files.createTempFile(Path, String, String, FileAttributes<?>) | 创建文件 通过流 I/O 创建和写入文件

通过通道 I/O 读写文件 |

File.exists

Files.exists 和 Files.notExists

验证文件或目录的存在性

File.compareTo and equals

Path.compareTo and equals

比较两个路径

File.getAbsolutePath and getAbsoluteFile

Path.toAbsolutePath

转换路径

| File.getCanonicalPath and getCanonicalFile | Path.toRealPathnormalize | 转换路径 (toRealPath) 从路径中删除冗余部分 (normalize)

|

File.toURI

Path.toURI

转换路径

File.isHidden

Files.isHidden

检索路径信息

File.list and listFiles

Path.newDirectoryStream

列出目录内容

File.mkdir 和 mkdirs

Files.createDirectory

创建目录

File.listRoots

FileSystem.getRootDirectories

列出文件系统的根目录

File.getTotalSpace、File.getFreeSpace、File.getUsableSpace

FileStore.getTotalSpace、FileStore.getUnallocatedSpace、FileStore.getUsableSpace、FileStore.getTotalSpace

文件存储属性

摘要

原文:docs.oracle.com/javase/tutorial/essential/io/summary.html

java.io 包包含许多类,您的程序可以使用这些类来读取和写入数据。大多数类实现顺序访问流。顺序访问流可以分为两组:那些读取和写入字节的流以及读取和写入 Unicode 字符的流。每个顺序访问流都有其特长,例如从文件中读取或写入数据,过滤读取或写入的数据,或者序列化对象。

java.nio.file 包提供了广泛的文件和文件系统 I/O 支持。这是一个非常全面的 API,但关键入口点如下:

  • Path 类具有操作路径的方法。
  • Files 类具有文件操作的方法,例如移动、复制、删除,以及检索和设置文件属性的方法。
  • FileSystem 类具有各种方法用于获取有关文件系统的信息。

关于 NIO.2 的更多信息可以在 OpenJDK: NIO 项目网站上找到。该网站包括 NIO.2 提供的超出本教程范围的功能资源,例如多播、异步 I/O 和创建自己的文件系统实现。

问题和练习:基本 I/O

原文:docs.oracle.com/javase/tutorial/essential/io/QandE/questions.html

问题

1. 你会使用什么类和方法来读取大文件末尾附近已知位置的几个数据片段?

2. 在调用format时,如何最好地指示一个新行?

3. 如何确定文件的 MIME 类型?

4. 您会使用什么方法来确定文件是否是符号链接?

练习

1. 编写一个示例,计算文件中特定字符(如e)出现的次数。可以在命令行指定字符。您可以使用xanadu.txt作为输入文件。

2. 文件datafile以一个告诉你同一文件中一个int数据偏移量的long开头。编写一个程序获取这个int数据。这个int数据是什么?

检查你的答案。

课程:并发编程

原文:docs.oracle.com/javase/tutorial/essential/concurrency/index.html

计算机用户认为他们的系统可以同时执行多项任务是理所当然的。他们认为他们可以在一个文字处理器中继续工作,同时其他应用程序可以下载文件,管理打印队列和流式传输音频。甚至单个应用程序通常也被期望同时执行多项任务。例如,流式传输音频应用程序必须同时从网络上读取数字音频,解压缩它,管理播放和更新显示。即使文字处理器也应该始终准备好响应键盘和鼠标事件,无论它是在重新格式化文本还是更新显示。能够执行这些操作的软件被称为并发软件。

Java 平台从头开始就设计用于支持并发编程,在 Java 编程语言和 Java 类库中具有基本的并发支持。自 5.0 版本以来,Java 平台还包括高级并发 API。本课程介绍了平台的基本并发支持,并总结了java.util.concurrent包中的一些高级 API。

进程和线程

原文:docs.oracle.com/javase/tutorial/essential/concurrency/procthread.html

在并发编程中,有两个基本的执行单位:进程线程。在 Java 编程语言中,并发编程主要涉及线程。然而,进程也很重要。

计算机系统通常有许多活动进程和线程。即使在只有一个执行核心的系统中,因此在任何给定时刻只有一个线程实际执行,也是如此。单核心的处理时间通过操作系统的时间片特性在进程和线程之间共享。

现在越来越普遍的是计算机系统具有多个处理器或具有多个执行核心的处理器。这极大地增强了系统对进程和线程并发执行的能力 — 但即使在简单系统上,没有多个处理器或执行核心,也可以实现并发。

进程

一个进程有一个独立的执行环境。一个进程通常有一个完整的,私有的基本运行时资源集;特别是,每个进程都有自己的内存空间。

进程通常被视为与程序或应用程序同义。然而,用户所看到的单个应用程序实际上可能是一组协作的进程。为了促进进程之间的通信,大多数操作系统支持进程间通信(IPC)资源,如管道和套接字。IPC 不仅用于同一系统上进程之间的通信,还用于不同系统上的进程。

大多数 Java 虚拟机的实现作为一个单独的进程运行。Java 应用程序可以使用ProcessBuilder对象创建额外的进程。多进程应用程序超出了本课程的范围。

线程

线程有时被称为轻量级进程。进程和线程都提供执行环境,但创建一个新线程所需的资源比创建一个新进程少。

线程存在于一个进程中 — 每个进程至少有一个。线程共享进程的资源,包括内存和打开的文件。这样做可以实现高效的,但潜在的有问题的通信。

多线程执行是 Java 平台的一个重要特性。每个应用程序至少有一个线程 — 或者多个,如果计算“系统”线程,执行诸如内存管理和信号处理等任务。但从应用程序员的角度来看,你从一个称为主线程的线程开始。这个线程有能力创建额外的线程,我们将在下一节中演示。

线程对象

原文:docs.oracle.com/javase/tutorial/essential/concurrency/threads.html

每个线程都与类Thread的实例相关联。使用Thread对象创建并发应用程序有两种基本策略。

  • 要直接控制线程的创建和管理,只需在应用程序需要启动异步任务时实例化Thread即可。
  • 要将线程管理与应用程序的其余部分抽象出来,将应用程序的任务传递给一个执行器

本节介绍了Thread对象的使用。执行器与其他高级并发对象一起讨论。

定义和启动线程

原文:docs.oracle.com/javase/tutorial/essential/concurrency/runthread.html

创建 Thread 实例的应用程序必须提供将在该线程中运行的代码。 有两种方法可以做到这一点:

提供一个 Runnable 对象. Runnable 接口定义了一个名为 run 的方法,用于包含在线程中执行的代码。 Runnable 对象被传递给 Thread 构造函数,就像 HelloRunnable 示例中那样:

代码语言:javascript复制
public class HelloRunnable implements Runnable {

    public void run() {
        System.out.println("Hello from a thread!");
    }

    public static void main(String args[]) {
        (new Thread(new HelloRunnable())).start();
    }

}

子类 Thread. Thread 类本身实现了 Runnable,尽管它的 run 方法什么也不做。 应用程序可以子类化 Thread,提供自己的 run 实现,就像 HelloThread 示例中那样:

代码语言:javascript复制
public class HelloThread extends Thread {

    public void run() {
        System.out.println("Hello from a thread!");
    }

    public static void main(String args[]) {
        (new HelloThread()).start();
    }

}

请注意,这两个示例都调用了 Thread.start 来启动新线程。

你应该使用哪种习语?第一个习语使用了一个 Runnable 对象,更通用,因为 Runnable 对象可以是 Thread 以外的类的子类。 第二个习语在简单应用程序中更容易使用,但受到任务类必须是 Thread 的后代的限制。 本课程重点介绍第一种方法,它将 Runnable 任务与执行任务的 Thread 对象分开。 这种方法不仅更灵活,而且适用于后面介绍的高级线程管理 API。

Thread 类定义了一些对线程管理有用的方法。 这些方法包括 static 方法,提供有关调用方法的线程的信息或影响其状态。 其他方法是从参与管理线程和 Thread 对象的其他线程调用的。 我们将在以下部分中检查其中一些方法。

暂停执行与睡眠

原文:docs.oracle.com/javase/tutorial/essential/concurrency/sleep.html

Thread.sleep会导致当前线程暂停执行一段指定的时间。这是一种有效的方式,可以让处理器时间可用于应用程序的其他线程或者可能在计算机系统上运行的其他应用程序。sleep方法也可以用于节奏控制,就像下面的示例中展示的那样,以及等待另一个线程,该线程的任务被理解为具有时间要求,就像稍后章节中的SimpleThreads示例一样。

提供了两个重载版本的sleep:一个指定以毫秒为单位的睡眠时间,另一个指定以纳秒为单位的睡眠时间。然而,这些睡眠时间不能保证是精确的,因为它们受到底层操作系统提供的设施的限制。此外,睡眠时间可以被中断,我们将在稍后的章节中看到。无论如何,你不能假设调用sleep会精确地暂停线程指定的时间段。

SleepMessages示例使用sleep以四秒的间隔打印消息:

代码语言:javascript复制
public class SleepMessages {
    public static void main(String args[])
        throws InterruptedException {
        String importantInfo[] = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };

        for (int i = 0;
             i < importantInfo.length;
             i  ) {
            //Pause for 4 seconds
            Thread.sleep(4000);
            //Print a message
            System.out.println(importantInfo[i]);
        }
    }
}

注意,main声明了它会throws InterruptedException。这是一个异常,当另一个线程在sleep处于活动状态时中断当前线程时会抛出。由于这个应用程序没有定义另一个线程来引起中断,所以它不会去捕获InterruptedException

中断

原文:docs.oracle.com/javase/tutorial/essential/concurrency/interrupt.html

中断是对线程的指示,告诉它应该停止当前操作并执行其他操作。程序员需要决定线程如何响应中断,但通常线程会终止。这是本课程强调的用法。

一个线程通过在要中断的线程的Thread对象上调用interrupt来发送中断。为了使中断机制正常工作,被中断的线程必须支持自身的中断。

支持中断

一个线程如何支持自身的中断?这取决于它当前正在做什么。如果线程频繁调用抛出InterruptedException的方法,它只需在捕获异常后从run方法返回。例如,假设SleepMessages示例中的中央消息循环在线程的Runnable对象的run方法中。那么可以修改如下以支持中断:

代码语言:javascript复制
for (int i = 0; i < importantInfo.length; i  ) {
    // Pause for 4 seconds
    try {
        Thread.sleep(4000);
    } catch (InterruptedException e) {
        // We've been interrupted: no more messages.
        return;
    }
    // Print a message
    System.out.println(importantInfo[i]);
}

许多抛出InterruptedException的方法,如sleep,设计为在接收到中断时取消当前操作并立即返回。

如果一个线程长时间不调用抛出InterruptedException的方法会怎样?那么它必须定期调用Thread.interrupted,如果接收到中断则返回true。例如:

代码语言:javascript复制
for (int i = 0; i < inputs.length; i  ) {
    heavyCrunch(inputs[i]);
    if (Thread.interrupted()) {
        // We've been interrupted: no more crunching.
        return;
    }
}

在这个简单的例子中,代码只是检测中断并在接收到中断时退出线程。在更复杂的应用程序中,抛出InterruptedException可能更合理:

代码语言:javascript复制
if (Thread.interrupted()) {
    throw new InterruptedException();
}

这使得中断处理代码可以集中在catch子句中。

中断状态标志

中断机制是通过一个称为中断状态的内部标志实现的。调用Thread.interrupt会设置这个标志。当线程通过调用静态方法Thread.interrupted检查中断时,中断状态会被清除。非静态的isInterrupted方法用于一个线程查询另一个线程的中断状态,不会改变中断状态标志。

按照惯例,任何通过抛出InterruptedException退出的方法在这样做时会清除中断状态。然而,另一个线程调用interrupt可能会立即再次设置中断状态。

加入

原文:docs.oracle.com/javase/tutorial/essential/concurrency/join.html

join方法允许一个线程等待另一个线程的完成。如果t是一个当前正在执行的线程的Thread对象,

代码语言:javascript复制
t.join();

会导致当前线程暂停执行,直到t的线程终止。join的重载允许程序员指定等待时间。然而,与sleep一样,join依赖于操作系统的时间控制,因此你不应该假设join会等待与你指定的时间完全相同。

sleep一样,join在收到InterruptedException时会退出。

简单线程示例

原文:docs.oracle.com/javase/tutorial/essential/concurrency/simple.html

以下示例汇集了本节中的一些概念。SimpleThreads 包含两个线程。第一个是每个 Java 应用程序都有的主线程。主线程从 Runnable 对象 MessageLoop 创建一个新线程,并等待其完成。如果 MessageLoop 线程花费太长时间才能完成,主线程会中断它。

MessageLoop 线程会打印一系列消息。如果在打印完所有消息之前被中断,MessageLoop 线程会打印一条消息然后退出。

代码语言:javascript复制
public class SimpleThreads {

    // Display a message, preceded by
    // the name of the current thread
    static void threadMessage(String message) {
        String threadName =
            Thread.currentThread().getName();
        System.out.format("%s: %s%n",
                          threadName,
                          message);
    }

    private static class MessageLoop
        implements Runnable {
        public void run() {
            String importantInfo[] = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
            };
            try {
                for (int i = 0;
                     i < importantInfo.length;
                     i  ) {
                    // Pause for 4 seconds
                    Thread.sleep(4000);
                    // Print a message
                    threadMessage(importantInfo[i]);
                }
            } catch (InterruptedException e) {
                threadMessage("I wasn't done!");
            }
        }
    }

    public static void main(String args[])
        throws InterruptedException {

        // Delay, in milliseconds before
        // we interrupt MessageLoop
        // thread (default one hour).
        long patience = 1000 * 60 * 60;

        // If command line argument
        // present, gives patience
        // in seconds.
        if (args.length > 0) {
            try {
                patience = Long.parseLong(args[0]) * 1000;
            } catch (NumberFormatException e) {
                System.err.println("Argument must be an integer.");
                System.exit(1);
            }
        }

        threadMessage("Starting MessageLoop thread");
        long startTime = System.currentTimeMillis();
        Thread t = new Thread(new MessageLoop());
        t.start();

        threadMessage("Waiting for MessageLoop thread to finish");
        // loop until MessageLoop
        // thread exits
        while (t.isAlive()) {
            threadMessage("Still waiting...");
            // Wait maximum of 1 second
            // for MessageLoop thread
            // to finish.
            t.join(1000);
            if (((System.currentTimeMillis() - startTime) > patience)
                  && t.isAlive()) {
                threadMessage("Tired of waiting!");
                t.interrupt();
                // Shouldn't be long now
                // -- wait indefinitely
                t.join();
            }
        }
        threadMessage("Finally!");
    }
}

同步

原文:docs.oracle.com/javase/tutorial/essential/concurrency/sync.html

线程主要通过共享对字段和对象引用字段引用的访问来进行通信。这种形式的通信非常高效,但可能导致两种错误:线程干扰内存一致性错误。防止这些错误所需的工具是同步

然而,同步可能引入线程争用,当两个或更多线程尝试同时访问同一资源导致 Java 运行时执行一个或多个线程更慢,甚至暂停它们的执行时发生。饥饿和活锁是线程争用的形式。有关更多信息,请参阅 Liveness 部分。

本节涵盖以下主题:

  • 线程干扰描述了当多个线程访问共享数据时引入错误的情况。
  • 内存一致性错误描述了由共享内存不一致视图引起的错误。
  • 同步方法描述了一种简单的习语,可以有效地防止线程干扰和内存一致性错误。
  • 隐式锁和同步描述了一种更通用的同步习语,并描述了同步是基于隐式锁的。
  • 原子访问讨论了无法被其他线程干扰的操作的一般概念。

线程干扰

原文:docs.oracle.com/javase/tutorial/essential/concurrency/interfere.html

考虑一个简单的名为 Counter 的类

代码语言:javascript复制
class Counter {
    private int c = 0;

    public void increment() {
        c  ;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

Counter 被设计成每次调用 increment 都会将 c 加 1,每次调用 decrement 都会从 c 减 1。然而,如果从多个线程引用 Counter 对象,线程之间的干扰可能会阻止预期的操作发生。

当两个操作在不同线程中运行,但作用于相同数据时,干扰就会发生。这意味着这两个操作由多个步骤组成,步骤序列会交叉。

对于 Counter 实例的操作似乎不可能交错,因为对 c 的操作都是单个简单语句。然而,即使是简单语句也可以被虚拟机翻译为多个步骤。我们不会检查虚拟机执行的具体步骤 — 知道单个表达式 c 可以分解为三个步骤就足够了:

  1. 检索当前值 c
  2. 递增检索到的值 1。
  3. 将递增后的值存储回 c

表达式 c-- 可以以相同方式分解,只是第二步是减少而不是增加。

假设线程 A 大约在同一时间调用 increment,而线程 B 调用 decrement。如果 c 的初始值为 0,它们交错的操作可能会按照这个顺序进行:

  1. 线程 A:检索 c。
  2. 线程 B:检索 c。
  3. 线程 A:递增检索到的值;结果为 1。
  4. 线程 B:减少检索到的值;结果为 -1。
  5. 线程 A:将结果存储在 c 中;c 现在为 1。
  6. 线程 B:将结果存储在 c 中;c 现在为 -1。

线程 A 的结果丢失,被线程 B 覆盖。这种特定的交错只是一种可能性。在不同情况下,可能会丢失线程 B 的结果,或者根本没有错误。由于它们是不可预测的,线程干扰 bug 可能很难检测和修复。

内存一致性错误

原文:docs.oracle.com/javase/tutorial/essential/concurrency/memconsist.html

内存一致性错误发生在不同线程对应该是相同数据的不一致视图时。内存一致性错误的原因复杂,超出了本教程的范围。幸运的是,程序员不需要详细了解这些原因。所需的只是避免它们的策略。

避免内存一致性错误的关键在于理解happens-before关系。这种关系简单地保证了一个特定语句的内存写入对另一个特定语句是可见的。为了看到这一点,考虑以下示例。假设定义并初始化了一个简单的int字段:

代码语言:javascript复制
int counter = 0;

counter字段在两个线程 A 和 B 之间共享。假设线程 A 增加counter

代码语言:javascript复制
counter  ;

然后,不久之后,线程 B 打印出counter

代码语言:javascript复制
System.out.println(counter);

如果这两个语句在同一个线程中执行,可以安全地假设打印出的值为"1"。但如果这两个语句在不同的线程中执行,打印出的值可能是"0",因为不能保证线程 A 对counter的更改对线程 B 可见,除非程序员在这两个语句之间建立了一个 happens-before 关系。

有几种动作会创建先于关系。其中之一是同步,我们将在接下来的部分中看到。

我们已经看到了两个创建先于关系的动作。

  • 当一个语句调用Thread.start时,与该语句具有先于关系的每个语句也与新线程执行的每个语句具有先于关系。导致创建新线程的代码的效果对新线程可见。
  • 当一个线程终止并导致另一个线程中的Thread.join返回时,那么终止线程执行的所有语句与成功加入后面的所有语句之间存在先于关系。线程中代码的效果现在对执行加入的线程可见。

有关创建先于关系的动作列表,请参考Java java.util.concurrent包的摘要页面。。

同步方法

原文:docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html

Java 编程语言提供了两种基本的同步习语:同步方法同步语句。其中更复杂的同步语句将在下一节中描述。本节讨论的是同步方法。

要使方法同步,只需在其声明中添加synchronized关键字:

代码语言:javascript复制
public class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c  ;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }
}

如果countSynchronizedCounter的一个实例,则使这些方法同步会产生两个效果:

  • 首先,不可能让同一对象上的两次同步方法调用交错。当一个线程正在为对象执行同步方法时,所有调用同一对象的同步方法的其他线程都会被阻塞(暂停执行),直到第一个线程完成对象的操作。
  • 其次,当一个同步方法退出时,它会自动与同一对象的任何后续同步方法的调用建立 happens-before 关系。这确保了对对象状态的更改对所有线程都是可见的。

请注意,构造函数不能被同步 — 使用synchronized关键字与构造函数是语法错误。同步构造函数没有意义,因为只有创建对象的线程在构造对象时应该访问它。


警告: 在构建一个将在多个线程之间共享的对象时,一定要非常小心,确保对象的引用不会过早“泄漏”。例如,假设你想要维护一个名为instancesList,其中包含类的每个实例。你可能会诱惑地在构造函数中添加以下行:

代码语言:javascript复制
instances.add(this);

但是其他线程可以使用instances来访问对象,而在对象构造完成之前。


同步方法为防止线程干扰和内存一致性错误提供了一种简单的策略:如果一个对象对多个线程可见,那么对该对象的变量的所有读取或写入都通过synchronized方法进行。 (一个重要的例外:final字段,在对象构造后无法修改,可以通过非同步方法安全地读取,一旦对象构造完成)这种策略是有效的,但在后面的课程中我们将看到它可能会出现 liveness 问题。

内在锁和同步

原文:docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html

同步建立在一个称为内在锁监视器锁的内部实体周围。内在锁在同步的两个方面发挥作用:强制对对象状态的独占访问和建立对可见性至关重要的 happens-before 关系。

每个对象都有一个与之关联的内在锁。按照惯例,需要独占和一致访问对象字段的线程在访问这些字段之前必须获取对象的内在锁,然后在完成后释放内在锁。线程在获取锁和释放锁之间被认为拥有内在锁。只要一个线程拥有内在锁,其他线程就无法获取相同的锁。当另一个线程尝试获取锁时,它将被阻塞。

当一个线程释放一个内在锁时,该操作与后续获取相同锁的任何操作建立 happens-before 关系。

同步方法中的锁

当一个线程调用一个同步方法时,它会自动获取该方法对象的内在锁,并在方法返回时释放它。即使返回是由未捕获的异常引起的,锁也会被释放。

你可能会想知道当调用静态同步方法时会发生什么,因为静态方法与类相关联,而不是对象。在这种情况下,线程会获取与类相关联的Class对象的内在锁。因此,对类的静态字段的访问受到一个与类的任何实例的锁不同的锁的控制。

同步语句

创建同步代码的另一种方法是使用synchronized 语句。与同步方法不同,同步语句必须指定提供内在锁的对象:

代码语言:javascript复制
public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount  ;
    }
    nameList.add(name);
}

在这个例子中,addName方法需要同步对lastNamenameCount的更改,但也需要避免同步调用其他对象的方法。(从同步代码调用其他对象的方法可能会导致在 Liveness 部分描述的问题。)如果没有同步语句,就必须有一个单独的、非同步的方法,唯一目的是调用nameList.add

同步语句也有助于通过细粒度同步提高并发性。例如,假设类MsLunch有两个实例字段,c1c2,它们永远不会同时使用。所有这些字段的更新必须同步,但没有理由阻止对 c1 的更新与对 c2 的更新交错—这样做会通过创建两个仅用于提供锁的对象来减少并发性。

代码语言:javascript复制
public class MsLunch {
    private long c1 = 0;
    private long c2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void inc1() {
        synchronized(lock1) {
            c1  ;
        }
    }

    public void inc2() {
        synchronized(lock2) {
            c2  ;
        }
    }
}

使用这种习语要非常小心。你必须绝对确定交错访问受影响字段是安全的。

可重入同步

请记住,一个线程不能获取另一个线程拥有的锁。但一个线程可以获取它已经拥有的锁。允许一个线程多次获取相同的锁使可重入同步成为可能。这描述了一种情况,即同步代码直接或间接地调用一个也包含同步代码的方法,并且两组代码使用相同的锁。如果没有可重入同步,同步代码将不得不采取许多额外的预防措施,以避免一个线程导致自己被阻塞。

原子访问

原文:docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html

在编程中,原子操作是一种有效地一次性完成的操作。原子操作不能在中途停止:它要么完全发生,要么根本不发生。在原子操作完成之前,不会看到任何副作用。

我们已经看到增量表达式,比如c ,并不描述原子操作。即使是非常简单的表达式也可以定义可以分解为其他操作的复杂操作。然而,有一些操作是可以指定为原子操作的:

  • 对于引用变量和大多数基本变量(除了longdouble之外的所有类型),读取和写入是原子的。
  • 对于所有声明为volatile的变量,读取和写入都是原子的(包括longdouble变量)。

原子操作不能交错,因此可以在不担心线程干扰的情况下使用它们。然而,这并不消除同步原子操作的所有需求,因为内存一致性错误仍然可能发生。使用volatile变量可以减少内存一致性错误的风险,因为对volatile变量的任何写入都会与随后对该变量的读取建立 happens-before 关系。这意味着对volatile变量的更改始终对其他线程可见。更重要的是,这也意味着当线程读取volatile变量时,它不仅看到volatile的最新更改,还看到导致更改的代码的副作用。

使用简单的原子变量访问比通过同步代码访问这些变量更有效,但需要程序员更加小心,以避免内存一致性错误。额外的努力是否值得取决于应用程序的大小和复杂性。

java.util.concurrent包中的一些类提供了不依赖于同步的原子方法。我们将在高级并发对象部分讨论它们。

活跃性

原文:docs.oracle.com/javase/tutorial/essential/concurrency/liveness.html

并发应用程序按时执行的能力被称为其liveness。本节描述了最常见的活跃性问题,死锁,并简要描述了另外两种活跃性问题,饥饿和活锁。

死锁

原文:docs.oracle.com/javase/tutorial/essential/concurrency/deadlock.html

死锁 描述了两个或更多线程永远被阻塞,彼此等待的情况。这里有一个例子。

阿方索和加斯顿是朋友,也是极信奉礼貌的人。一个严格的礼貌规则是,当你向朋友鞠躬时,你必须保持鞠躬的姿势,直到你的朋友有机会回礼。不幸的是,这个规则没有考虑到两个朋友可能同时向对方鞠躬的可能性。这个示例应用程序,死锁,模拟了这种可能性:

代码语言:javascript复制
public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s"
                  "  has bowed to me!%n", 
                this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s"
                  " has bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.bow(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.bow(alphonse); }
        }).start();
    }
}

死锁 运行时,当它们尝试调用 bowBack 时,两个线程都很可能被阻塞。由于每个线程都在等待另一个线程退出 bow,因此这两个阻塞永远不会结束。

饥饿和活锁

原文:docs.oracle.com/javase/tutorial/essential/concurrency/starvelive.html

饥饿和活锁问题比死锁问题要少见得多,但仍然是每个并发软件设计者可能会遇到的问题。

饥饿

饥饿描述了一个线程无法定期访问共享资源并且无法取得进展的情况。这种情况发生在"贪婪"线程长时间地使共享资源不可用时。例如,假设一个对象提供了一个经常需要很长时间才能返回的同步方法。如果一个线程频繁调用这个方法,其他也需要频繁同步访问同一对象的线程将经常被阻塞。

活锁

一个线程经常是作为对另一个线程动作的响应。如果另一个线程的动作也是对另一个线程动作的响应,那么可能会发生livelock。与死锁类似,活锁的线程无法取得进一步的进展。然而,这些线程并没有被阻塞 — 它们只是忙于相互响应而无法恢复工作。这就好比两个人试图在走廊里互相让对方通过:阿方斯向左移动让加斯通通过,而加斯通向右移动让阿方斯通过。看到他们仍然互相阻挡,阿方斯向右移动,而加斯通向左移动。他们仍然互相阻挡,所以…

保护块

原文:docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html

线程经常需要协调它们的动作。最常见的协调习语是保护块。这样的块开始于轮询一个条件,该条件必须在块可以继续之前为真。为了正确执行此操作,需要遵循一些步骤。

假设,例如guardedJoy是一个方法,必须在另一个线程设置共享变量joy之前才能继续。这样的方法理论上可以简单地循环,直到条件满足,但是该循环是浪费的,因为它在等待时持续执行。

代码语言:javascript复制
public void guardedJoy() {
    // Simple loop guard. Wastes
    // processor time. Don't do this!
    while(!joy) {}
    System.out.println("Joy has been achieved!");
}

一个更有效的保护块调用Object.wait来挂起当前线程。调用wait不会返回,直到另一个线程发出通知,表明可能发生了某个特殊事件,尽管不一定是该线程正在等待的事件:

代码语言:javascript复制
public synchronized void guardedJoy() {
    // This guard only loops once for each special event, which may not
    // be the event we're waiting for.
    while(!joy) {
        try {
            wait();
        } catch (InterruptedException e) {}
    }
    System.out.println("Joy and efficiency have been achieved!");
}

**注意:**始终在测试等待的条件的循环中调用wait。不要假设中断是为了您正在等待的特定条件,或者该条件仍然为真。


像许多暂停执行的方法一样,wait可能会抛出InterruptedException。在这个例子中,我们可以忽略这个异常,我们只关心joy的值。

为什么这个guardedJoy的版本是同步的?假设d是我们用来调用wait的对象。当一个线程调用d.wait时,它必须拥有d的内在锁,否则会抛出错误。在同步方法中调用wait是获取内在锁的简单方法。

当调用wait时,线程释放锁并暂停执行。在将来的某个时间,另一个线程将获得相同的锁并调用Object.notifyAll,通知所有等待该锁的线程发生了重要事件:

代码语言:javascript复制
public synchronized notifyJoy() {
    joy = true;
    notifyAll();
}

第二个线程释放锁后一段时间,第一个线程重新获取锁,并通过从wait调用返回来恢复执行。


**注意:**还有第二种通知方法,notify,它唤醒单个线程。因为notify不允许您指定被唤醒的线程,所以它只在大规模并行应用程序中有用,即具有大量线程的程序,所有线程都在做类似的工作。在这种应用程序中,您不关心哪个线程被唤醒。


让我们使用保护块来创建一个生产者-消费者应用程序。这种应用程序在两个线程之间共享数据:生产者创建数据,消费者对其进行处理。这两个线程使用共享对象进行通信。协调是必不可少的:消费者线程在生产者线程交付数据之前不得尝试检索数据,生产者线程在消费者尚未检索旧数据之前不得尝试交付新数据。

在这个例子中,数据是一系列文本消息,通过一个类型为Drop的对象共享:

代码语言:javascript复制
public class Drop {
    // Message sent from producer
    // to consumer.
    private String message;
    // True if consumer should wait
    // for producer to send message,
    // false if producer should wait for
    // consumer to retrieve message.
    private boolean empty = true;

    public synchronized String take() {
        // Wait until message is
        // available.
        while (empty) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = true;
        // Notify producer that
        // status has changed.
        notifyAll();
        return message;
    }

    public synchronized void put(String message) {
        // Wait until message has
        // been retrieved.
        while (!empty) {
            try { 
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = false;
        // Store message.
        this.message = message;
        // Notify consumer that status
        // has changed.
        notifyAll();
    }
}

生产者线程,在Producer中定义,发送一系列熟悉的消息。字符串"DONE"表示所有消息都已发送。为了模拟真实应用程序的不可预测性,生产者线程在消息之间暂停一段随机时间。

代码语言:javascript复制
import java.util.Random;

public class Producer implements Runnable {
    private Drop drop;

    public Producer(Drop drop) {
        this.drop = drop;
    }

    public void run() {
        String importantInfo[] = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };
        Random random = new Random();

        for (int i = 0;
             i < importantInfo.length;
             i  ) {
            drop.put(importantInfo[i]);
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
        drop.put("DONE");
    }
}

消费者线程,在Consumer中定义,简单地检索消息并打印出来,直到检索到"DONE"字符串为止。该线程还会暂停一段随机时间。

代码语言:javascript复制
import java.util.Random;

public class Consumer implements Runnable {
    private Drop drop;

    public Consumer(Drop drop) {
        this.drop = drop;
    }

    public void run() {
        Random random = new Random();
        for (String message = drop.take();
             ! message.equals("DONE");
             message = drop.take()) {
            System.out.format("MESSAGE RECEIVED: %s%n", message);
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
    }
}

最后,这是主线程,在ProducerConsumerExample中定义,启动生产者和消费者线程。

代码语言:javascript复制
public class ProducerConsumerExample {
    public static void main(String[] args) {
        Drop drop = new Drop();
        (new Thread(new Producer(drop))).start();
        (new Thread(new Consumer(drop))).start();
    }
}

注意: Drop 类是为了演示受保护的代码块而编写的。在尝试编写自己的数据共享对象之前,请查看 Java 集合框架中的现有数据结构,以避免重复造轮子。有关更多信息,请参考问题和练习部分。


不可变对象

原文:docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html

如果一个对象在构造后其状态不能改变,则被认为是不可变的。广泛接受的一种创建简单可靠代码的策略是最大程度地依赖不可变对象。

不可变对象在并发应用程序中特别有用。由于它们不能改变状态,因此它们不会受到线程干扰的破坏,也不会以不一致的状态被观察到。

程序员通常不愿使用不可变对象,因为他们担心创建一个新对象的成本,而不是就地更新对象。对象创建的影响经常被高估,可以通过一些与不可变对象相关的效率来抵消。这些效率包括由于垃圾回收而减少的开销,以及消除了为了保护可变对象免受破坏而需要的代码。

以下小节以一个实例是可变的类为例,并从中派生出一个实例是不可变的类。这样做,它们给出了这种转换的一般规则,并展示了不可变对象的一些优势。

一个同步类的示例

原文:docs.oracle.com/javase/tutorial/essential/concurrency/syncrgb.html

这个类,SynchronizedRGB,定义了代表颜色的对象。每个对象将颜色表示为三个代表主要颜色值的整数和一个给出颜色名称的字符串。

代码语言:javascript复制
public class SynchronizedRGB {

    // Values must be between 0 and 255.
    private int red;
    private int green;
    private int blue;
    private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public SynchronizedRGB(int red,
                           int green,
                           int blue,
                           String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public void set(int red,
                    int green,
                    int blue,
                    String name) {
        check(red, green, blue);
        synchronized (this) {
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.name = name;
        }
    }

    public synchronized int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public synchronized String getName() {
        return name;
    }

    public synchronized void invert() {
        red = 255 - red;
        green = 255 - green;
        blue = 255 - blue;
        name = "Inverse of "   name;
    }
}

必须小心使用SynchronizedRGB,以避免出现不一致的状态。例如,假设一个线程执行以下代码:

代码语言:javascript复制
SynchronizedRGB color =
    new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
int myColorInt = color.getRGB();      //Statement 1
String myColorName = color.getName(); //Statement 2

如果另一个线程在语句 1 之后但在语句 2 之前调用color.setmyColorInt的值将不匹配myColorName的值。为了避免这种结果,这两个语句必须绑定在一起:

代码语言:javascript复制
synchronized (color) {
    int myColorInt = color.getRGB();
    String myColorName = color.getName();
} 

这种不一致性只对可变对象有效 — 对于不可变版本的SynchronizedRGB不会有问题。

定义不可变对象的策略

原文:docs.oracle.com/javase/tutorial/essential/concurrency/imstrat.html

以下规则定义了创建不可变对象的简单策略。并非所有被记录为“不可变”的类都遵循这些规则。这并不一定意味着这些类的创建者粗心大意 — 他们可能有充分的理由相信他们的类的实例在构造后永远不会改变。然而,这种策略需要复杂的分析,不适合初学者。

  1. 不提供“setter”方法 — 修改字段或字段引用的对象的方法。
  2. 使所有字段都是finalprivate
  3. 不允许子类重写方法。这样做的最简单方法是将类声明为final。更复杂的方法是将构造函数设为private,并在工厂方法中构造实例。
  4. 如果实例字段包括对可变对象的引用,请不要允许更改这些对象:
    • 不要提供修改可变对象的方法。
    • 不共享对可变对象的引用。永远不要存储传递给构造函数的外部可变对象的引用;如果必要,创建副本,并存储对副本的引用。类似地,在必要时创建内部可变对象的副本,以避免在方法中返回原始对象。

将这种策略应用于SynchronizedRGB会产生以下步骤:

  1. 这个类中有两个 setter 方法。第一个set方法任意地转换对象,并且在类的不可变版本中没有位置。第二个invert方法可以通过创建一个新对象来适应,而不是修改现有对象。
  2. 所有字段已经是private;它们进一步被标记为final
  3. 类本身被声明为final
  4. 只有一个字段引用一个对象,而该对象本身是不可变的。因此,不需要防止改变“包含”可变对象状态的保护措施。

在这些更改之后,我们有了ImmutableRGB

代码语言:javascript复制
final public class ImmutableRGB {

    // Values must be between 0 and 255.
    final private int red;
    final private int green;
    final private int blue;
    final private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public ImmutableRGB(int red,
                        int green,
                        int blue,
                        String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public String getName() {
        return name;
    }

    public ImmutableRGB invert() {
        return new ImmutableRGB(255 - red,
                       255 - green,
                       255 - blue,
                       "Inverse of "   name);
    }
}

高级并发对象

原文:docs.oracle.com/javase/tutorial/essential/concurrency/highlevel.html

到目前为止,本课程已经专注于 Java 平台从一开始就存在的低级 API。这些 API 对于非常基本的任务是足够的,但对于更高级的任务需要更高级的构建块。这对于充分利用当今的多处理器和多核系统的大规模并发应用程序尤为重要。

在本节中,我们将介绍 Java 平台 5.0 版本引入的一些高级并发特性。这些特性大多数都是在新的java.util.concurrent包中实现的。Java 集合框架中还有新的并发数据结构。

  • 锁对象支持简化许多并发应用程序的锁定习语。
  • 执行器定义了一个用于启动和管理线程的高级 API。java.util.concurrent提供的执行器实现提供了适用于大规模应用程序的线程池管理。
  • 并发集合使得管理大量数据集变得更加容易,并且可以大大减少同步的需求。
  • 原子变量具有最小化同步和避免内存一致性错误的特性。
  • ThreadLocalRandom(在 JDK 7 中)提供了多线程有效生成伪随机数的功能。

锁对象

原文:docs.oracle.com/javase/tutorial/essential/concurrency/newlocks.html

同步代码依赖于一种简单的可重入锁。这种类型的锁易于使用,但有许多限制。更复杂的锁习语由 java.util.concurrent.locks 包支持。我们不会详细讨论此包,而是专注于其最基本的接口 Lock

Lock 对象的工作方式与同步代码中使用的隐式锁非常相似。与隐式锁一样,一次只有一个线程可以拥有 Lock 对象。Lock 对象还支持通过其关联的 Condition 对象实现 wait/notify 机制。

Lock 对象相对于隐式锁的最大优势在于其能够在尝试获取锁时撤销操作。如果指定了超时时间,tryLock 方法在锁不可用时或超时之前会撤销操作。lockInterruptibly 方法在获取锁之前如果另一个线程发送中断信号,则会撤销操作。

让我们使用 Lock 对象来解决我们在 Liveness 中看到的死锁问题。阿方索和加斯顿已经训练自己注意到朋友即将鞠躬的时刻。我们通过要求我们的 Friend 对象必须在继续鞠躬之前为两个参与者获取锁来模拟这种改进。这是改进模型的源代码,Safelock。为了展示这种习语的多功能性,我们假设阿方索和加斯顿如此迷恋他们新发现的安全鞠躬能力,以至于他们无法停止向彼此鞠躬:

代码语言:javascript复制
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;

public class Safelock {
    static class Friend {
        private final String name;
        private final Lock lock = new ReentrantLock();

        public Friend(String name) {
            this.name = name;
        }

        public String getName() {
            return this.name;
        }

        public boolean impendingBow(Friend bower) {
            Boolean myLock = false;
            Boolean yourLock = false;
            try {
                myLock = lock.tryLock();
                yourLock = bower.lock.tryLock();
            } finally {
                if (! (myLock && yourLock)) {
                    if (myLock) {
                        lock.unlock();
                    }
                    if (yourLock) {
                        bower.lock.unlock();
                    }
                }
            }
            return myLock && yourLock;
        }

        public void bow(Friend bower) {
            if (impendingBow(bower)) {
                try {
                    System.out.format("%s: %s has"
                          " bowed to me!%n", 
                        this.name, bower.getName());
                    bower.bowBack(this);
                } finally {
                    lock.unlock();
                    bower.lock.unlock();
                }
            } else {
                System.out.format("%s: %s started"
                      " to bow to me, but saw that"
                      " I was already bowing to"
                      " him.%n",
                    this.name, bower.getName());
            }
        }

        public void bowBack(Friend bower) {
            System.out.format("%s: %s has"  
                " bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    static class BowLoop implements Runnable {
        private Friend bower;
        private Friend bowee;

        public BowLoop(Friend bower, Friend bowee) {
            this.bower = bower;
            this.bowee = bowee;
        }

        public void run() {
            Random random = new Random();
            for (;;) {
                try {
                    Thread.sleep(random.nextInt(10));
                } catch (InterruptedException e) {}
                bowee.bow(bower);
            }
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new BowLoop(alphonse, gaston)).start();
        new Thread(new BowLoop(gaston, alphonse)).start();
    }
}

0 人点赞