如前所述,java.nio.file
包,特别是Path
类是“链接感知”的。每个Path
方法都会检测遇到符号链接时该做什么,或者提供一个选项,使您能够配置遇到符号链接时的行为。
到目前为止的讨论一直是关于符号或软链接,但一些文件系统也支持硬链接。硬链接比符号链接更受限制,具体如下:
由于这些限制,硬链接不像符号链接那样经常使用,但Path
方法与硬链接无缝配合。
有几种方法专门处理链接,并在以下部分中介绍:
如果你的文件系统支持,你可以使用createSymbolicLink(Path, Path, FileAttribute<?>)
方法创建一个符号链接。第二个Path
参数表示目标文件或目录,可能存在也可能不存在。以下代码片段创建了一个带有默认权限的符号链接:
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
。以下代码片段展示了如何创建链接:
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)
方法。以下代码片段展示了如何:
Path file = ...;
boolean isSymbolicLink =
Files.isSymbolicLink(file);
欲了解更多信息,请参阅管理元数据。
通过使用readSymbolicLink(Path)
方法,您可以获取符号链接的目标,如下所示:
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
。
您是否需要创建一个应用程序,递归访问文件树中的所有文件?也许您需要删除树中的每个.class
文件,或者找到在过去一年中未被访问的每个文件。您可以通过FileVisitor
接口实现这一点。
本节涵盖以下内容:
要遍历文件树,首先需要实现一个FileVisitor
。FileVisitor
指定了在遍历过程的关键点上所需的行为:当访问文件时,在访问目录之前,在访问目录之后,或者当发生故障时。该接口有四个方法对应于这些情况:
preVisitDirectory
– 在访问目录条目之前调用。
postVisitDirectory
– 在访问目录中的所有条目之后调用。如果遇到任何错误,特定异常将传递给该方法。
visitFile
– 在访问文件时调用。文件的BasicFileAttributes
被传递给该方法,或者您可以使用 file attributes 包来读取特定的属性集。例如,您可以选择读取文件的DosFileAttributeView
来确定文件是否设置了“hidden”位。
visitFileFailed
– 当无法访问文件时调用。特定异常被传递给该方法。您可以选择是否抛出异常,将其打印到控制台或日志文件等。
如果您不需要实现所有四个FileVisitor
方法,而是扩展SimpleFileVisitor
类,而不是实现FileVisitor
接口。这个类实现了FileVisitor
接口,访问树中的所有文件,并在遇到错误时抛出IOError
。您可以扩展这个类,并仅覆盖您需要的方法。
这是一个扩展SimpleFileVisitor
以打印文件树中所有条目的示例。它打印条目,无论条目是常规文件、符号链接、目录还是其他类型的“未指定”文件。它还打印每个文件的字节大小。遇到的任何异常都会打印到控制台。
FileVisitor
方法以粗体显示:
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
方法。
第一个方法只需要一个起始点和您的FileVisitor
的实例。您可以按以下方式调用PrintFiles
文件访问者:
Path startingDir = ...;
PrintFiles pf = new PrintFiles();
Files.walkFileTree(startingDir, pf);
第二个walkFileTree
方法还允许您额外指定访问级别的限制和一组FileVisitOption
枚举。如果您希望确保此方法遍历整个文件树,您可以为最大深度参数指定Integer.MAX_VALUE
。
您可以指定FileVisitOption
枚举FOLLOW_LINKS
,表示应该跟随符号链接。
此代码片段显示了如何调用四参数方法:
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
方法中执行比较。此方法找到所有符合您条件的文件,但不会找到目录。如果您想要找到文件和目录,您还必须在preVisitDirectory
或postVisitDirectory
方法中执行比较。Find
示例展示了如何做到这一点。
你需要决定是否要遵循符号链接。例如,如果你正在删除文件,跟随符号链接可能不明智。如果你正在复制文件树,你可能希望允许它。默认情况下,walkFileTree
不会遵循符号链接。
对于文件,会调用visitFile
方法。如果你指定了FOLLOW_LINKS
选项,并且你的文件树有一个指向父目录的循环链接,循环目录将在visitFileFailed
方法中报告,带有FileSystemLoopException
。以下代码片段显示了如何捕获循环链接,并来自于Copy
示例:
@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
的目录都会被跳过:
import static java.nio.file.FileVisitResult.*;
public FileVisitResult
preVisitDirectory(Path dir,
BasicFileAttributes attrs) {
(if (dir.getFileName().toString().equals("SCCS")) {
return SKIP_SUBTREE;
}
return CONTINUE;
}
在这段代码片段中,一旦找到特定文件,文件名就会被打印到标准输出,并且文件遍历会终止:
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
选项调用此程序会监视整个树的更改。有关文件通知服务的更多信息,请参见监视目录的更改。
如果你曾经使用过 shell 脚本,你很可能使用过模式匹配来定位文件。事实上,你可能已经广泛使用了它。如果你还没有使用过,模式匹配使用特殊字符创建模式,然后文件名可以与该模式进行比较。例如,在大多数 shell 脚本中,星号,*
,匹配任意数量的字符。例如,以下命令列出当前目录中以.html
结尾的所有文件:
% ls *.html
java.nio.file
包为这一有用功能提供了编程支持。每个文件系统实现都提供了一个PathMatcher
。你可以通过在FileSystem
类中使用getPathMatcher(String)
方法来检索文件系统的PathMatcher
。以下代码片段获取默认文件系统的路径匹配器:
String pattern = ...;
PathMatcher matcher =
FileSystems.getDefault().getPathMatcher("glob:" + pattern);
传递给getPathMatcher
的字符串参数指定语法风格和要匹配的模式。本示例指定了glob语法。如果你不熟悉 glob 语法,请参阅什么是 Glob。
Glob 语法易于使用和灵活,但如果你喜欢,也可以使用正则表达式,或regex语法。有关正则表达式的更多信息,请参阅正则表达式课程。一些文件系统实现可能支持其他语法。
如果你想使用其他形式的基于字符串的模式匹配,你可以创建自己的PathMatcher
类。本页中的示例使用 glob 语法。
一旦你创建了PathMatcher
实例,你就可以准备好根据它匹配文件。PathMatcher
接口有一个方法,matches
,它接受一个Path
参数并返回一个布尔值:它要么匹配模式,要么不匹配。以下代码片段查找以.java
或.class
结尾的文件并将这些文件打印到标准输出:
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 应用程序,请使用以下格式:
% java Find <path> -name "<glob_pattern>"
模式被放置在引号内,以防止 shell 解释任何通配符。例如:
% java Find . -name "*.html"
这里是Find
示例的源代码:
/**
* 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 对话框显示检测到修改的文件
要实现此功能,称为文件更改通知,程序必须能够检测到文件系统上相关目录发生的变化。一种方法是轮询文件系统以查找更改,但这种方法效率低下。它不适用于具有数百个打开文件或目录需要监视的应用程序。
java.nio.file
包提供了一个文件更改通知 API,称为 Watch Service API。此 API 使您能够向观察服务注册目录(或目录)。在注册时,您告诉服务您感兴趣的事件类型:文件创建、文件删除或文件修改。当服务检测到感兴趣的事件时,它会转发给注册的进程。注册的进程有一个专用于监视其注册事件的线程(或线程池)。当事件发生时,根据需要进行处理。
本节涵盖以下内容:
WatchService
API 相当低级,允许您自定义它。您可以直接使用它,或者您可以选择在此机制之上创建一个高级 API,以使其适合您的特定需求。
下面是实现观察服务所需的基本步骤:
WatchService
“观察者”。
WatchKey
实例。
closed
方法关闭服务时,监视服务将退出。
WatchKeys
是线程安全的,可以与java.nio.concurrent
包一起使用。您可以为此目的专门分配一个线程池。
由于此 API 更为高级,请在继续之前先尝试一下。将WatchDir
示例保存到您的计算机上,并对其进行编译。创建一个将传递给示例的test
目录。WatchDir
使用单个线程处理所有事件,因此在等待事件时会阻止键盘输入。要么在单独的窗口中运行程序,要么在后台运行,如下所示:
java WatchDir test &
在test
目录中创建、删除和编辑文件。当发生任何这些事件时,将在控制台上打印消息。完成后,删除test
目录,WatchDir
退出。或者,如果您愿意,也可以手动终止进程。
您还可以通过指定-r
选项来监视整个文件树。当您指定-r
时,WatchDir
遍历文件树,将每个目录注册到监视服务中。
第一步是通过FileSystem
类中的newWatchService
方法创建一个新的WatchService
,如下所示:
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
实例:
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);
}
事件处理循环中事件的顺序如下:
poll
– 如果可用,则返回一个排队的键。如果不可用,则立即返回null
值。
poll(long, TimeUnit)
– 如果有排队的键可用,则返回一个。如果没有立即可用的排队键,则程序将等待指定的时间。TimeUnit
参数确定指定的时间是纳秒、毫秒还是其他时间单位。
take
– 返回一个排队的键。如果没有可用的排队键,此方法将等待。
pollEvents
方法中获取WatchEvents
的List
。
kind
方法检索事件的类型。无论键注册了什么事件,都有可能收到OVERFLOW
事件。您可以选择处理溢出或忽略它,但应该对其进行测试。
context
方法来检索它。
reset
将键放回ready
状态。如果此方法返回false
,则键不再有效,循环可以退出。这一步非常重要。如果未调用reset
,则此键将不会接收到进一步的事件。
观察键具有状态。在任何给定时间,其状态可能是以下之一:
Ready
表示键已准备好接受事件。创建时,键处于准备状态。
Signaled
表示有一个或多个事件排队。一旦键被标记,它就不再处于准备状态,直到调用reset
方法。
Invalid
表示键不再活动。当发生以下事件之一时,会出现此状态:
这里是一个事件处理循环的示例。它取自于 Email
示例,该示例监视一个目录,等待新文件出现。当新文件可用时,通过使用 probeContentType(Path)
方法来检查它是否是一个 text/plain
文件。意图是将 text/plain
文件发送到一个别名,但具体实现细节留给读者。
Watch service API 特定的方法用粗体显示:
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
示例使用以下代码检索文件名:
WatchEvent<Path> ev = (WatchEvent<Path>)event;
Path filename = ev.context();
当你编译 Email
示例时,会生成以下错误:
Note: Email.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
这个错误是由将 WatchEvent<T>
强制转换为 WatchEvent<Path>
的代码行引起的。WatchDir
示例通过创建一个抑制未经检查警告的实用 cast
方法来避免这个错误,如下所示:
@SuppressWarnings("unchecked")
static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<Path>)event;
}
如果你对 @SuppressWarnings
语法不熟悉,请参见 Annotations。
Watch Service API 适用于需要通知文件更改事件的应用程序。它非常适合任何可能有许多打开文件并需要确保文件与文件系统同步的应用程序,比如编辑器或 IDE。它也非常适合监视目录的应用服务器,也许等待 .jsp
或 .jar
文件的出现,以便部署它们。
这个 API 不 是为了索引硬盘而设计的。大多数文件系统实现都原生支持文件更改通知。Watch Service API 利用了这种支持(如果可用)。然而,当文件系统不支持这种机制时,Watch Service 将轮询文件系统,等待事件发生。
本课程中未涵盖的一些有用方法在此处介绍。本节涵盖以下内容:
要确定文件的 MIME 类型,您可能会发现probeContentType(Path)
方法很有用。例如:
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
方法之一(注意是单数形式),如下所示:
PathMatcher matcher =
FileSystems.getDefault().getPathMatcher("glob:*.*");
POSIX 文件系统的路径分隔符是正斜杠/
,Microsoft Windows 的路径分隔符是反斜杠\
。其他文件系统可能使用其他分隔符。要检索默认文件系统的Path
分隔符,可以使用以下方法之一:
String separator = File.separator;
String separator = FileSystems.getDefault().getSeparator();
getSeparator
方法也用于检索任何可用文件系统的路径分隔符。
文件系统有一个或多个文件存储器来保存其文件和目录。文件存储器代表底层存储设备。在 UNIX 操作系统中,每个挂载的文件系统都由一个文件存储器表示。在 Microsoft Windows 中,每个卷都由一个文件存储器表示:C:
、D:
等等。
要检索文件系统的所有文件存储器列表,可以使用getFileStores
方法。此方法返回一个Iterable
,允许您使用增强的 for 语句遍历所有根目录。
for (FileStore store: FileSystems.getDefault().getFileStores()) {
...
}
如果要检索特定文件所在的文件存储器,请使用Files
类中的getFileStore
方法,如下所示:
Path file = ...;
FileStore store= Files.getFileStore(file);
DiskUsage
示例使用getFileStores
方法。
在 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
实例,如下所示:
Path input = file.toPath();
然后,您可以利用 Path
类提供的丰富功能集。
例如,假设您有一些删除文件的代码:
file.delete();
您可以修改此代码以使用 Files.delete
方法,如下所示:
Path fp = file.toPath();
Files.delete(fp);
相反,Path.toFile
方法为 Path
对象构造一个 java.io.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.toRealPath
或 normalize
| 转换路径 (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 和创建自己的文件系统实现。
原文:
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
示例中那样:
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
示例中那样:
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
以四秒的间隔打印消息:
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
方法中。那么可以修改如下以支持中断:
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
。例如:
for (int i = 0; i < inputs.length; i++) {
heavyCrunch(inputs[i]);
if (Thread.interrupted()) {
// We've been interrupted: no more crunching.
return;
}
}
在这个简单的例子中,代码只是检测中断并在接收到中断时退出线程。在更复杂的应用程序中,抛出InterruptedException
可能更合理:
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
对象,
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
线程会打印一条消息然后退出。
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
的类
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++
可以分解为三个步骤就足够了:
c
。
c
。
表达式 c--
可以以相同方式分解,只是第二步是减少而不是增加。
假设线程 A 大约在同一时间调用 increment
,而线程 B 调用 decrement
。如果 c
的初始值为 0
,它们交错的操作可能会按照这个顺序进行:
线程 A 的结果丢失,被线程 B 覆盖。这种特定的交错只是一种可能性。在不同情况下,可能会丢失线程 B 的结果,或者根本没有错误。由于它们是不可预测的,线程干扰 bug 可能很难检测和修复。
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/memconsist.html
内存一致性错误发生在不同线程对应该是相同数据的不一致视图时。内存一致性错误的原因复杂,超出了本教程的范围。幸运的是,程序员不需要详细了解这些原因。所需的只是避免它们的策略。
避免内存一致性错误的关键在于理解happens-before关系。这种关系简单地保证了一个特定语句的内存写入对另一个特定语句是可见的。为了看到这一点,考虑以下示例。假设定义并初始化了一个简单的int
字段:
int counter = 0;
counter
字段在两个线程 A 和 B 之间共享。假设线程 A 增加counter
:
counter++;
然后,不久之后,线程 B 打印出counter
:
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
关键字:
public class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
如果count
是SynchronizedCounter
的一个实例,则使这些方法同步会产生两个效果:
请注意,构造函数不能被同步 — 使用synchronized
关键字与构造函数是语法错误。同步构造函数没有意义,因为只有创建对象的线程在构造对象时应该访问它。
警告: 在构建一个将在多个线程之间共享的对象时,一定要非常小心,确保对象的引用不会过早“泄漏”。例如,假设你想要维护一个名为instances
的List
,其中包含类的每个实例。你可能会诱惑地在构造函数中添加以下行:
instances.add(this);
但是其他线程可以使用instances
来访问对象,而在对象构造完成之前。
同步方法为防止线程干扰和内存一致性错误提供了一种简单的策略:如果一个对象对多个线程可见,那么对该对象的变量的所有读取或写入都通过synchronized
方法进行。 (一个重要的例外:final
字段,在对象构造后无法修改,可以通过非同步方法安全地读取,一旦对象构造完成)这种策略是有效的,但在后面的课程中我们将看到它可能会出现 liveness 问题。
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html
同步建立在一个称为内在锁或监视器锁的内部实体周围。内在锁在同步的两个方面发挥作用:强制对对象状态的独占访问和建立对可见性至关重要的 happens-before 关系。
每个对象都有一个与之关联的内在锁。按照惯例,需要独占和一致访问对象字段的线程在访问这些字段之前必须获取对象的内在锁,然后在完成后释放内在锁。线程在获取锁和释放锁之间被认为拥有内在锁。只要一个线程拥有内在锁,其他线程就无法获取相同的锁。当另一个线程尝试获取锁时,它将被阻塞。
当一个线程释放一个内在锁时,该操作与后续获取相同锁的任何操作建立 happens-before 关系。
当一个线程调用一个同步方法时,它会自动获取该方法对象的内在锁,并在方法返回时释放它。即使返回是由未捕获的异常引起的,锁也会被释放。
你可能会想知道当调用静态同步方法时会发生什么,因为静态方法与类相关联,而不是对象。在这种情况下,线程会获取与类相关联的Class
对象的内在锁。因此,对类的静态字段的访问受到一个与类的任何实例的锁不同的锁的控制。
创建同步代码的另一种方法是使用synchronized 语句。与同步方法不同,同步语句必须指定提供内在锁的对象:
public void addName(String name) {
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}
在这个例子中,addName
方法需要同步对lastName
和nameCount
的更改,但也需要避免同步调用其他对象的方法。(从同步代码调用其他对象的方法可能会导致在 Liveness 部分描述的问题。)如果没有同步语句,就必须有一个单独的、非同步的方法,唯一目的是调用nameList.add
。
同步语句也有助于通过细粒度同步提高并发性。例如,假设类MsLunch
有两个实例字段,c1
和c2
,它们永远不会同时使用。所有这些字段的更新必须同步,但没有理由阻止对 c1 的更新与对 c2 的更新交错—这样做会通过创建两个仅用于提供锁的对象来减少并发性。
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++
,并不描述原子操作。即使是非常简单的表达式也可以定义可以分解为其他操作的复杂操作。然而,有一些操作是可以指定为原子操作的:
long
和double
之外的所有类型),读取和写入是原子的。
volatile
的变量,读取和写入都是原子的(包括long
和double
变量)。
原子操作不能交错,因此可以在不担心线程干扰的情况下使用它们。然而,这并不消除同步原子操作的所有需求,因为内存一致性错误仍然可能发生。使用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
死锁 描述了两个或更多线程永远被阻塞,彼此等待的情况。这里有一个例子。
阿方索和加斯顿是朋友,也是极信奉礼貌的人。一个严格的礼貌规则是,当你向朋友鞠躬时,你必须保持鞠躬的姿势,直到你的朋友有机会回礼。不幸的是,这个规则没有考虑到两个朋友可能同时向对方鞠躬的可能性。这个示例应用程序,死锁
,模拟了这种可能性:
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
之前才能继续。这样的方法理论上可以简单地循环,直到条件满足,但是该循环是浪费的,因为它在等待时持续执行。
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
不会返回,直到另一个线程发出通知,表明可能发生了某个特殊事件,尽管不一定是该线程正在等待的事件:
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
,通知所有等待该锁的线程发生了重要事件:
public synchronized notifyJoy() {
joy = true;
notifyAll();
}
第二个线程释放锁后一段时间,第一个线程重新获取锁,并通过从wait
调用返回来恢复执行。
**注意:**还有第二种通知方法,notify
,它唤醒单个线程。因为notify
不允许您指定被唤醒的线程,所以它只在大规模并行应用程序中有用,即具有大量线程的程序,所有线程都在做类似的工作。在这种应用程序中,您不关心哪个线程被唤醒。
让我们使用保护块来创建一个生产者-消费者应用程序。这种应用程序在两个线程之间共享数据:生产者创建数据,消费者对其进行处理。这两个线程使用共享对象进行通信。协调是必不可少的:消费者线程在生产者线程交付数据之前不得尝试检索数据,生产者线程在消费者尚未检索旧数据之前不得尝试交付新数据。
在这个例子中,数据是一系列文本消息,通过一个类型为Drop
的对象共享:
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"表示所有消息都已发送。为了模拟真实应用程序的不可预测性,生产者线程在消息之间暂停一段随机时间。
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"字符串为止。该线程还会暂停一段随机时间。
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
中定义,启动生产者和消费者线程。
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
,定义了代表颜色的对象。每个对象将颜色表示为三个代表主要颜色值的整数和一个给出颜色名称的字符串。
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
,以避免出现不一致的状态。例如,假设一个线程执行以下代码:
SynchronizedRGB color =
new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
int myColorInt = color.getRGB(); //Statement 1
String myColorName = color.getName(); //Statement 2
如果另一个线程在语句 1 之后但在语句 2 之前调用color.set
,myColorInt
的值将不匹配myColorName
的值。为了避免这种结果,这两个语句必须绑定在一起:
synchronized (color) {
int myColorInt = color.getRGB();
String myColorName = color.getName();
}
这种不一致性只对可变对象有效 — 对于不可变版本的SynchronizedRGB
不会有问题。
原文:
docs.oracle.com/javase/tutorial/essential/concurrency/imstrat.html
以下规则定义了创建不可变对象的简单策略。并非所有被记录为“不可变”的类都遵循这些规则。这并不一定意味着这些类的创建者粗心大意 — 他们可能有充分的理由相信他们的类的实例在构造后永远不会改变。然而,这种策略需要复杂的分析,不适合初学者。
final
和private
。
final
。更复杂的方法是将构造函数设为private
,并在工厂方法中构造实例。
将这种策略应用于SynchronizedRGB
会产生以下步骤:
set
方法任意地转换对象,并且在类的不可变版本中没有位置。第二个invert
方法可以通过创建一个新对象来适应,而不是修改现有对象。
private
;它们进一步被标记为final
。
final
。
在这些更改之后,我们有了ImmutableRGB
:
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 集合框架中还有新的并发数据结构。
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
。为了展示这种习语的多功能性,我们假设阿方索和加斯顿如此迷恋他们新发现的安全鞠躬能力,以至于他们无法停止向彼此鞠躬:
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();
}
}