ForkJoinPool
?Java SE 7 新功能“分?l合框架”的核心c,现在可能乏h问|Q但我觉得它q早会成Z。分?l合框架是一个比较特D的U程池框Ӟ专用于需要将一个Q务不断分解成子Q务(分叉Q,再不断进行汇d到最l结果(l合Q的计算q程。比起传l的U程池类 ThreadPoolExecutor
Q?code>ForkJoinPool 实现了工作窃取算法,使得I闲U程能够d分担从别的线E分解出来的子Q务,从而让所有的U程都尽可能处于饱满的工作状态,提高执行效率?/p>
ForkJoinPool
提供了三cL法来调度子Q务:
execute
pdinvoke
?invokeAll
submit
pdFuture
对象?/dd>
子Q务由 ForkJoinTask
的实例来代表。它是一个抽象类QJDK 为我们提供了两个实现Q?code>RecursiveTask ?RecursiveAction
Q分别用于需要和不需要返回计结果的子Q务?code>ForkJoinTask 提供了三个静态的 invokeAll
Ҏ来调度子dQ注意只能在 ForkJoinPool
执行计算的过E中调用它们?/p>
ForkJoinPool
?ForkJoinTask
q提供了很多让hD~ؕ的公共方法,其实它们大多数都是其内部实现去调用的Q对于应用开发h员来说意义不大?/p>
下面以统?D 盘文件个Cؓ例。这实际上是对一个文件树的遍历,我们需要递归地统计每个目录下的文件数量,最后汇总,非常适合用分?l合框架来处理:
// 处理单个目录的Q? public class CountingTask extends RecursiveTask<Integer> { private Path dir; public CountingTask(Path dir) { this.dir = dir; } @Override protected Integer compute() { int count = 0; List<CountingTask> subTasks = new ArrayList<>(); // d目录 dir 的子路径? try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) { for (Path subPath : ds) { if (Files.isDirectory(subPath, LinkOption.NOFOLLOW_LINKS)) { // Ҏ个子目录都新Z个子d? subTasks.add(new CountingTask(subPath)); } else { // 遇到文gQ则计数器增?1? count++; } } if (!subTasks.isEmpty()) { // 在当前的 ForkJoinPool 上调度所有的子Q务? for (CountingTask subTask : invokeAll(subTasks)) { count += subTask.join(); } } } catch (IOException ex) { return 0; } return count; } } // 用一?ForkJoinPool 实例调度“MQ务”,然后敬请期待l果…? Integer count = new ForkJoinPool().invoke(new CountingTask(Paths.get("D:/")));
在我的笔记本上,l多ơ运行这D代码,耗费的时间稳定在 600 豪秒左右。普通线E池Q?code>Executors.newCachedThreadPool()Q耗时 1100 毫秒左右Q见工作窃取的优势?/p>
l束本文前,我们来围观一个最奇的结果:单线E算法(使用 Files.walkFileTree(...)
Q比q两个都快,q_耗时 550 毫秒Q这警告我们q引入多线E就能优化性能Qƈ要先经q多ơ测试才能下l论?/p>
前面已经看到Q?code>Socket cȝ getInputStream()
?getOutStream()
Ҏ分别获取套接字的输入和输出。输入流用来dq端发送过来的数据Q输出流则用来向q端发送数据?/p>
使用套接字的输入读取数据时Q当前线E会q入d状态,直到套接字收C些数据ؓ止(亦即套接字的接收~冲区有可用数据Q。该输入的 available()
Ҏ只是q回接收~冲区的可用字节数量Q不可能知道q端q要发送多字节。用输入流的时候,最好先它包装Z?BufferedInputStream
Q因取接收缓冲区导?JVM 和底层系l之间的切换Q应当尽量减切换次C提高性能?code>BufferedInputStream 的缓冲区大小最好设为套接字接收~冲区的大小?/p>
如果直接调用输入的 close()
Ҏ来关闭它Q则导致套接字被关闭。对此,Socket
cL供了一?shutdownInput()
Ҏ来禁用输入流。调用该Ҏ后,每次L作都返?EOF
Q无法再dq端发送的数据。对q个 EOF
的检,不同的输入流包装体现Z同的l果Q可能读?-1 个字节,可能d的字W串?null
Q还可能收到一?EOFException
{等。禁用输入流后,q端输出的行ؓ是^台相关的Q?/p>
用输入这U技术ƈ不常用?/p>
套接字的输出操作实际上仅仅将数据写到发送缓冲区内,当发送缓冲区填满且上ơ的发送成功后Q由底层pȝ负责发送。如果发送缓冲区的剩余空间不够,当前U程׃d。和输入类|最好将输出包装ؓ BufferedOutputStream
?/p>
如果套接字的双发都?ObjectInputStream
?ObjectOutputStream
来读?Java 对象Q则必须先创?ObjectOutputStream
Q因?ObjectInputStream
在构造的时候会试图d对象头部Q如果双发都先创?ObjectInputStream
Q则会互相等待对方的输出Q造成死锁Q?/p>
// 创徏的顺序不能颠倒! ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
cM于输入流Q关闭输出流也导致关闭套接字Q所?Socket
cdh供了一?shutdownOutput()
来禁用输出流。禁用输出流后,已写入发送缓冲区的数据会正常发送,之后的Q何写操作都会D IOException
Q且q端的输入流始终会读?EOF
。禁用输出流非常有用Q例如套接字的双发都在发送完毕数据后用输入,然后双方都会收到 EOF
Q从而知道数据已l全部交换完毕,可以安全关闭套接字。直接关闭套接字会同时关闭输入流和输出流Q且断开q接Q达不到q种效果?/p>
如果要用流q行输入和输出,只能用d模式的套接字。这里ȝ一下阻塞套接字的优~点。先看看优点Q?/p>
但在性能斚w有致命的~点Q?/p>
下一文章开始探讨用基?NIO 的套接字通道和缓冲区实现伸羃性更强的 TCP 套接字?/p>
ServerSocket
cd Socket
c都提供了多个公共构造方法。不同的构造方法不仅带的参C同,所h的意义也不一栗下面分别解析这两个cȝ实例初始化过E?/p>
ServerSocket
实例的初始化ServerSocket
cL供了四个构造器Q?/p>
public ServerSocket(int port) throws IOException
public ServerSocket(int port, int backlog) throws IOException
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
public ServerSocket() throws IOException
带参构造器用来创徏已绑定的服务器套接字Q也是说构造成功后它就已经开始侦听指定的端口Q且能够调用 accept()
Ҏ来接受客Lq接。默认构造器则会创徏未绑定的服务器套接字Q构造成功后必须手动其l定C个本地地址才能用,在绑定之前可以进行一些选项配置?/p>
ȝ来说Q带参构造器提供了三个参敎ͼ
port
backlog
accept()
ҎQ,它就会被从队列中U除?code>backlog 参数q于指定队列的最大长度,默认gؓ 50Q但q个值只是一个徏议,底层pȝ可能Ҏ需要自动调整。如果队列满了,则其行ؓ是^台相关的Q微软的 WINSOCK 会拒l新的连接,其他实现则什么都不做。严格地_微Y没有遵守规范Q破坏了游戏规则…?/dd>
bindAddr
null
Q服务器套接字会在所有的本地 IP 地址Q?code>0.0.0.0 ?::0
Q上侦听。如果希望只侦听一个地址Q则可用该参数?/dd>
如果使用默认构造器Q在l定地址前,q可以做些配|。绑定操作由两个 bind
Ҏ定义Q参数类g带参构造器。配|项包括以下斚wQ都必须在绑定前配置Q:
setReuseAddress(boolean on)
Ҏ配置Q对应底层系l的 SO_REUSEADDR
套接字选项。JDK 没有定义该选项的默认倹{如果该选项?false
Q则在关?TCP q接ӞZ保证可靠性,该连接可能在关闭后的一D|_大约两分钟)内保持超时状态(通常UCؓ TIME_WAIT
状态或 2MSL
{待状态)Q这D|间里无法新建的服务器套接字l定到同一个地址。在开发阶D,服务器可能不断重启,打开攚w项会非常有用?/dd>
setReceiveBufferSize(int size)
Ҏ配置Q对应底层系l的 SO_RCVBUF
套接字选项Q单位是字节。《RFC 1323 - TCP Extensions for High Performance》将~冲区大定义ؓ 64KB。该选项只是一个徏议|底层pȝ可能Ҏ需要自行调整?/dd>
setSoTimeout(int timeout)
Ҏ配置Q对应底层系l的 SO_TIMEOUT
套接字选项Q单位是毫秒。默认gؓ 0。该选项影响 accept
Ҏ的阻塞时间长度,如果时引?SocketTimeoutException
。如果设?0Q则表示怸时?/dd>
setPerformancePreferences(int connectionTime, int latency, int bandwidth)
Ҏ配置。这三个数值分别表C短q接旉、低延迟和高带宽的相寚w要性,数D大则重要;其各自的l对值没有意义。该Ҏ的初hZ?Java 能在用非 TCP/IP 实现的套接字环境下工作得更好Q某些需要对|络q行调优的程序也可以这三个首选项作ؓ配置参数提供l用戗?/dd>
Socket
实例的初始化Socket
cL供了六个公共构造器Q已q时的除外)Q?/p>
public Socket(String host, int port) throws UnknownHostException, IOException
public Socket(InetAddress address, int port) throws IOException
public Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
public Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
public Socket()
public Socket(Proxy proxy)
前四个构造器创徏已连接的客户端套接字Q也是说构造的时候就会去q接服务器。前两个构造器需要提供服务器的地址和端口作为参敎ͼ本地地址和端口由pȝ自动分配Q后两个允许手动指定本地地址和端口,但极用。后两个构造器创徏未连接的套接字,创徏后需要调?connect
Ҏ手动q接Q连接之前可以做一些配|。最后一个构造器接受一个代表代理服务其?Proxy
对象QJDK 支持 HTTP ?SOCKSQV4 ?V5Q两U代理类型?/p>
在连接前Q客L套接字不仅像服务器套接字那样可以讄是否重用本地地址、缓冲区大小、超时值和性能首选项Q还能够配置以下各项Q都必须在连接前配置Q:
setKeepAlive(boolean on)
Ҏ配置Q对应底层系l的 SO_KEEPALIVE
套接字选项。默认gؓ false
。如果打开该选项Q则套接字会定期自动发送保持活跃的探测性消息,cM于心x。根据《RFC 1122 - Requirements for Internet Hosts》的规定Q保持活跃机制只?TCP 的一个可选功能,如果支持的话Q默认必Mؓ false
Q而且q种机制默认在成功徏立连接后Q且q箋两小时没有数据传输的情况下才会被ȀzR从另一斚w来看Q通过套接字的 I/O 操作完全可以知道q接是否q有效,所以该选项的实用hg大?/dd>
setOOBInline(boolean on)
Ҏ配置Q对应底层系l的 SO_OOBINLINE
套接字选项。默认gؓ off
。带外数据(Out-of-band DataQ也叫做紧急数据,表示数据很重要,需要用不同于发送普通数据的一个专用通道来发送。打开该选项后,可以调?sendUrgentData(int data)
Ҏ发送一个字节的紧急数据。JDK 对带外数据只提供了有限支持,紧急数据将会和普通数据一赯收到Qƈ且无法自动区分。该选项对应用开发h员意义不大?/dl>
setSoLinger(boolean on, int linger)
Ҏ配置Q对应底层系l的 SO_LINGER
套接字选项。默认ؓ false
。该选项只会影响套接字的关闭Q其中的 linger
参数表示时旉Q单位ؓU。如果打开攚w项Q如果将 linger
设ؓ 0Q则关闭套接字的时候,未发送的数据会被丢弃Q且另一端会出现q接被同位体重置的异常;如果 linger
?0Q则关闭套接字的U程被dQ直到数据全部发送或时Q超时后的行Z底层pȝ相关QJDK 无法控制。如果关闭该选项Q则套接字正常关闭,数据也会全部发送。由于底层实现的差异性,不提倡应用开发h员打开该选项?/dd>
setTcpNoDelay(boolean on)
Ҏ配置Q对应底层系l的 TCP_NODELAY
TCP 选项。默认gؓ off
。打开该选项禁?Nagle 法QTCP 包会立即发送;关闭该选项则会启用 Nagle 法Q多个较的 TCP 包会被组合成一个大包一起发送,虽然发送gq了Q但有利于避免网l拥塞。默认ؓ false
。该选项对实时性很强的E序可能有用Q但一般的E序不需要关心?/dd>
setTrafficClass(int tc)
Ҏ配置Q对应底层系l的“流量类别”套接字属性。该选项用于向网l(例如路由器)提示从该套接字发送的包需要获取哪些服务类型,Ҏ?TCP 协议栈没有媄响。IPv4 ?IPv6 分别定义了多个不同的|例如 IPv4 ?0x08
定义为最大吞吐量Q?code>0x10 定义为最gq,{等。可以用或运将多个值合qؓ一个选项。该选项用来调整性能Q需要根据实际情况设|。由于只是徏议|可能被网l忽略?/dd>
|上很多关于单例模式写法的文章,不外乎饿汉和懒汉两种形式的讨论。很多h喜欢用懒汉式Q因得它实现了gq加载,可以让系l的性能更好。但事实果真如此吗?我对此存疑?/p>
首先我们查一下饿汉和懒汉单例模式最单的写法Q这里不讨论哪种懒汉写法更好Q:
// 饿汉 public final class HungrySingleton { private static final HungrySingleton INSTANCE = new HungrySingleton(); private HungrySingleton() { System.out.println("Initializing..."); } public static HungrySingleton getInstance() { return INSTANCE; } } // 懒汉 public final class LazySingleton { private static LazySingleton INSTANCE; private LazySingleton() { System.out.println("Initializing..."); } public static synchronized LazySingleton getInstance() { if (INSTANCE == null) { INSTANCE = new LazySingleton(); } return INSTANCE; } }
从理Z来说Q?code>HungrySingleton 的单例在该类W一ơ用的时候创建,?LazySingleton
的单例则在其 getInstance()
Ҏ被调用的时候创建。至于网上有人声U“饿汉式不管用不用都会初始化”,U属走\的时候步子迈得太大。谁的加载更q?如果你只是调用它们的 getInstance()
Ҏ来得到单例对象,则它们都是gq加载,q样懒汉式没有Q何意义,而且׃ LazySingleton
采取了同步措施,性能更低Q可以说M懒汉式的性能都低于饿汉式Q。当你用一个单例类的时候,NW一步不是调?getInstance()
么?所以在自己的代码里Q我更喜Ƣ用饿汉式?/p>
下面用一个例子来试加蝲序Q?/p>
// 饿汉 System.out.println("Before"); HungrySingleton.getInstance(); System.out.println("After"); // 懒汉 System.out.println("Before"); LazySingleton.getInstance(); System.out.println("After");
输出l果都是Q?/p>
Before Initializing... After
那么Q懒汉模式还有什么存在意义?如果pȝ使用了某些需要在启动时对c进行扫描的框架Q用饿汉式的话Q启动时间比懒汉式更长,如果使用了大量单例类Q不利于开发阶Dc在pȝ的正式运行阶D,所有的单例c迟早都要加载的Qȝ说来两者性能持^Q但是懒汉式每次都至多一个判断,所以越到后期越体现饿汉的优性?/p>
最后,推荐下《Effective Java》第二版指出的用枚Dcd实现的饿汉单例模式:
// 饿汉 public enum HungrySingleton { INSTANCE; private HungrySingleton() { } }
q种写法不但最z,q能L扩展为实例数量固定的“多例模式”?/p>
JDK 提供了对 TCPQTransmission Control ProtocolQ传输控制协议)?UDPQUser Datagram ProtocolQ用h据报协议Q这两个数据传输协议的支持。本文开始探?TCP?/p>
在“服务器-客户端”这U架构中Q服务器和客L各自l护一个端点,两个端点需要通过|络q行数据交换。TCP U需求提供了一U可靠的式q接Q流式的意思是传出和收到的数据都是q箋的字节,没有Ҏ据量q行大小限制。一个端点由 IP 地址和端口构成(专业术语为“元l?{IP 地址, 端口}
”)。这P一个连接就可以由元l?{本地地址, 本地端口, q程地址, q程端口}
来表C?/p>
?TCP ~程接口中,端点体现?TCP 套接字。共有两U?TCP 套接字:d和被动,“被动”状态也常被UCؓ“侦听”状态。服务器和客L利用套接字进行连接的q程如下Q?/p>
下面是连接过E的图解Q?/p>
TCP q接
JDK 提供?ServerSocket
cL代表 TCP 服务器的被动套接字。下面的代码演示了一个简单的 TCP 服务器(多线E阻塞模式)Q它不断侦听q接受客L的连接,然后客L发送过来的文本按行dQ全文{换ؓ大写后返回给客户端,直到客户端发送文本行 bye
Q?/p>
public class TcpServer implements Runnable { private ServerSocket serverSocket; public TcpServer(int port) throws IOException { // 创徏l定到某个端口的 TCP 服务器被动套接字? serverSocket = new ServerSocket(port); } @Override public void run() { while (true) { try { // 以阻塞的方式接受一个客Lq接Q返回代表该q接的主动套接字? Socket socket = serverSocket.accept(); // 在新U程中处理客Lq接? new Thread(new ClientHandler(socket)).start(); } catch (IOException ex) { ex.printStackTrace(); } } } } public class ClientHandler implements Runnable { private Socket socket; public ClientHandler(Socket socket) { this.socket = Objects.requireNonNull(socket); } @Override public void run() { try (Socket s = socket) { // 减少代码量的花招…? // 包装套接字的输入以d客户端发送的文本行? BufferedReader in = new BufferedReader(new InputStreamReader( s.getInputStream(), StandardCharsets.UTF_8)); // 包装套接字的输出以向客L发送{换结果? PrintWriter out = new PrintWriter(new OutputStreamWriter( s.getOutputStream(), StandardCharsets.UTF_8), true); String line = null; while ((line = in.readLine()) != null) { if (line.equals("bye")) { break; } // {换结果输出给客户端? out.println(line.toUpperCase(Locale.ENGLISH)); } } catch (IOException ex) { ex.printStackTrace(); } } }
d模式的编E方式简单,但存在性能问题Q因为服务器U程会卡d接受客户端的 accept()
Ҏ上,不能有效利用资源。套接字支持非阻塞模式,现在暂时略过?/p>
JDK 提供?Socket
cL代表 TCP 客户端的d套接字。下面的代码演示了上q服务器的客LQ?/p>
public class TcpClient implements Runnable { private Socket socket; public TcpClient(String host, int port) throws IOException { // 创徏q接到服务器的套接字? socket = new Socket(host, port); } @Override public void run() { try (Socket s = socket) { // 再次减少代码量…? // 包装套接字的输出以向服务器发送文本行? PrintWriter out = new PrintWriter(new OutputStreamWriter( s.getOutputStream(), StandardCharsets.UTF_8), true); // 包装套接字的输入以d服务器返回的文本行? BufferedReader in = new BufferedReader(new InputStreamReader( s.getInputStream(), StandardCharsets.UTF_8)); Console console = System.console(); String line = null; while ((line = console.readLine()) != null) { if (line.equals("bye")) { break; } // 文本行发送给服务器? out.println(line); // 打印服务器返回的文本行? console.writer().println(in.readLine()); } // 通知服务器关闭连接? out.println("bye"); } catch (IOException ex) { ex.printStackTrace(); } } }
?JDK 文档可以看到Q?code>ServerSocket ?Socket
在初始化的时候,可以讑֮一些参敎ͼq支持gq绑定。这些东西对性能和行为都有所影响。下一文章将详解q两个类的初始化?/p>
我竟然到现在才发现《Fundamental Networking in Java》这本神作,真有Ҏ地自容的感觉。最q几q做的都是所谓的企业U开发,免不了和|络打交道,但在实际工作中,往往会采用框架将底层l节和上层应用隔dQ感觉就像是在一?Word 模板表单里面填写内容Q做出来也没什么成感。虽然没有不使用框架的理由,但我q真是有Ҏ念当初直接用套接字做|络~程的日子,既能掌控更多东西Q还可以学到更多知识Qؓ研究框架的实现原理打基础。闲话完毕,转入今天的正题:IPQInternet ProtocolQ互联网协议Q?/p>
说到 IPQ大多数人的W一反应估计都是 IP 地址。其?IP 是一U协议,IP 地址只是协议的一部分。《RFC 791 - INTERNET PROTOCOL》说Q“互联网协议是ؓ在包交换计算机通信|络的互联系l中使用而设计的。”IP 包含三方面的功能Q?/p>
?Java 的角度来看上面说到的三个功能Q只有第一个是开发h员需要关心的。另外两个都依赖底层pȝ的实玎ͼJDK 也没有提供相关的cd操作。下面一一介绍 JDK 提供的用于处?IP 地址的类?/p>
此类用来表示 IP 地址Q它有两个子c:Inet4Address
?Inet6Address
Q分别用于处?IPv4 ?IPv6 两个版本。在实际应用中,InetAddress
以应付l大多数情况。它提供了一些静态方法来构造实例,能根据参数格式自动识?IP 版本Q?/p>
public static InetAddress[] getAllByName(String host) throws UnknownHostException
public static InetAddress getByAddress(byte[] addr) throws UnknownHostException
public static InetAddress getByAddress(String host, byte[] addr) throws UnknownHostException
public static InetAddress getByName(String host) throws UnknownHostException
getAllByName(host)[0]
?/dd>
public static InetAddress getLocalHost() throws UnknownHostException
public static InetAddress getLoopbackAddress()
127.0.0.1
Q不抛出异常Q等同于 getByName("localhost")
Q不要和 getLocalHost()
搞Q。环回地址使主够自p接自己,常被用来对在同一台机器上试|络应用E序。在 IPv4 中,环回地址的网Dؓ 127.0.0.0/8
Q通常?127.0.0.1
QIPv6 中只有一?::1
?/dd>
接下来看?InetAddress
中定义的部分实例ҎQ?/p>
public byte[] getAddress()
public String getCanonicalHostName()
InetAddress.getByName("www.google.com").getCanonicalHostName()
q回 we-in-f99.1e100.net?/dd>
public String getHostAddress()
public String getHostName()
getCanonicalHostName()
Q否则直接返回构造时传入的主机地址?/dd>
public boolean isAnyLocalAddress()
0.0.0.0
QIPv4Q或 ::0
QIPv6Q,代表所有的本地 IP 地址。例如,假设电脑有两块网卡,各有一个地址Q如果想让一个程序同时监听这两个地址Q就需要用通配W地址?/dd>
public boolean isLinkLocalAddress()
169.254.0.0/16
QIpv6 里是?fe80::/64
为前~的地址。在电脑没联|的时候查看本?IPQ就能看到这U地址?/dd>
public boolean isLoopbackAddress()
public boolean isSiteLocalAddress()
fc00::/7
。这些地址用于U有|络Q例如企业内部的局域网?/dd>
此外q有一些有兛_播地址的方法,暂时略过?/p>
JDK 默认同时支持 IPv4 ?IPv6。如果只想用一U,可以Ҏ情况?java.net.preferIPv4Stack
?java.net.preferIPv6Addresses
q两个系l属性之一设ؓ true
。两个属性的默认值都?false
。一般来说不需要去惊动它们?/p>
该类是一个空壻I事实上应用程序用的是它的唯一子类 InetSocketAddress
Q目前还看不栯计有什么意义。该cd不过?InetAddress
的基上增加了一个端口属性?/p>
该类代表|络接口Q例如一块网卡。一个网l接口可以绑定一?IP 地址。具有多个网l接口的L被称为多宿主L。下面的代码可打印出所有本机网l接口的信息Q?/p>
for (NetworkInterface ni : Collections.list(NetworkInterface.getNetworkInterfaces())) { System.out.println(ni); for (InterfaceAddress ia : ni.getInterfaceAddresses()) { System.out.println("\t" + ia); } System.out.println(); }
在我的笔记本上运行结果ؓQ?/p>
name:lo (Software Loopback Interface 1) /127.0.0.1/8 [/127.255.255.255] /0:0:0:0:0:0:0:1/128 [null] name:net0 (WAN Miniport (SSTP)) name:net1 (WAN Miniport (L2TP)) name:net2 (WAN Miniport (PPTP)) name:ppp0 (WAN Miniport (PPPOE)) name:eth0 (WAN Miniport (IPv6)) name:eth1 (WAN Miniport (Network Monitor)) name:eth2 (WAN Miniport (IP)) name:ppp1 (RAS Async Adapter) name:net3 (WAN Miniport (IKEv2)) name:net4 (Intel(R) Wireless WiFi Link 4965AGN) /fe80:0:0:0:288a:2daf:3549:1811%11/64 [null] name:eth3 (Broadcom NetXtreme 57xx Gigabit Controller) /10.140.1.133/24 [/10.140.1.255] /fe80:0:0:0:78c7:e420:1739:f947%12/64 [null] name:net5 (Teredo Tunneling Pseudo-Interface) /fe80:0:0:0:e0:0:0:0%13/64 [null] name:net6 (Bluetooth Device (RFCOMM Protocol TDI)) name:eth4 (Bluetooth Device (Personal Area Network)) name:eth5 (Cisco AnyConnect VPN Virtual Miniport Adapter for Windows x64) name:net7 (Microsoft ISATAP Adapter) /fe80:0:0:0:0:5efe:a8c:185%17/128 [null] name:net8 (Microsoft ISATAP Adapter #2) name:net9 (Intel(R) Wireless WiFi Link 4965AGN-QoS Packet Scheduler-0000) name:eth6 (Broadcom NetXtreme 57xx Gigabit Controller-TM NDIS Sample LightWeight Filter-0000) name:eth7 (Broadcom NetXtreme 57xx Gigabit Controller-QoS Packet Scheduler-0000) name:eth8 (Broadcom NetXtreme 57xx Gigabit Controller-WFP LightWeight Filter-0000) name:eth9 (WAN Miniport (Network Monitor)-QoS Packet Scheduler-0000) name:eth10 (WAN Miniport (IP)-QoS Packet Scheduler-0000) name:eth11 (WAN Miniport (IPv6)-QoS Packet Scheduler-0000) name:net10 (Intel(R) Wireless WiFi Link 4965AGN-Native WiFi Filter Driver-0000) name:net11 (Intel(R) Wireless WiFi Link 4965AGN-TM NDIS Sample LightWeight Filter-0000) name:net12 (Intel(R) Wireless WiFi Link 4965AGN-WFP LightWeight Filter-0000)
Exchanger
用来让两个线E互相等待ƈ交换计算l果。这个类的用法很单,因ؓ它就定义了两个重载的 exchange
ҎQ参数多的那个无非增加了对超时的支持。当一个线E调?exchange
的时候(以计结果作为参敎ͼQ它开始等待另一个线E调?exchange
Q然后两个线E分别收到对方调?exchange
时传入的参数Q从而完成了计算l果的交换?/p>
不用太多的解释,q行下面q个例子׃清二楚:
final Exchanger<String> e = new Exchanger<>(); new Thread() { @Override public void run() { long id = Thread.currentThread().getId(); String s = "abc"; System.out.println("U程 [" + id + "] 出 " + s); try { TimeUnit.SECONDS.sleep(new Random().nextInt(5)); System.out.println("U程 [" + id + "] 收到 " + e.exchange(s)); } catch (InterruptedException ex) { ex.printStackTrace(); } } }.start(); new Thread() { @Override public void run() { long id = Thread.currentThread().getId(); String s = "xyz"; System.out.println("U程 [" + id + "] 出 " + s); try { TimeUnit.SECONDS.sleep(new Random().nextInt(5)); System.out.println("U程 [" + id + "] 收到 " + e.exchange(s)); } catch (InterruptedException ex) { ex.printStackTrace(); } } }.start();
q行l果Q可能ؓQ:
U程 [9] 出 abc U程 [10] 出 xyz U程 [10] 收到 abc U程 [9] 收到 xyz
最后强调下Q该cd适用于两个线E,妄图用它来处理多个生产者和消费者之间的数据交换是注定要p|的…?/p>
以前我曾用两个类Q?a >ZipItem
?ZipSystem
Q实C一个简单的 ZIP 文gpȝQ以下简U?ZFSQ。其实这两个类挺好用的Q而且支持嵌套?ZIP 文gQ但是,但是……JDK 7 丢下来一枚叫?NIO2 的笑气炸弹,引入了一套标准的文gpȝ APIQ我承认我中弹了Q手痒了Q又Ҏq套 API 重新实现?ZIP 文gpȝQ终于在今天初步完工Q哈?/p>
话说QJDK 7 其实捆绑销售了一?ZFSQdemo 目录下还有源代码。可……它达不到我的奢求,而且 BUG 不少。随侉K两个:
// com.sun.nio.zipfs.ZipFileSystemProvider cM的方? @Override public Path getPath(URI uri) { String spec = uri.getSchemeSpecificPart(); int sep = spec.indexOf("!/"); if (sep == -1) throw new IllegalArgumentException("URI: " + uri + " does not contain path info ex. jar:file:/c:/foo.zip!/BAR"); // 难怪该Ҏ始终?IllegalArgumentException 异常Q原来你子把文件的 URI // 当成 ZFS ?URI 在用…? return getFileSystem(uri).getPath(spec.substring(sep + 1)); } // com.sun.nio.zipfs.ZipFileSystem cM的方? @Override public PathMatcher getPathMatcher(String syntaxAndInput) { int pos = syntaxAndInput.indexOf(':'); // 丫的Qpos == syntaxAndInput.length()Q!谁写的?抓出来鞭? if (pos <= 0 || pos == syntaxAndInput.length()) { throw new IllegalArgumentException();
很明显,官方 ZFS 没有l过代码审阅、没有经q测试、没有经q……然后,@author Xueming Shen
Q真是丢咱华夏民族的脸…?/p>
下面列个表格详细比较官方 ZFS 和山?ZFSQ?/p>
比较内容 | 官方 ZFS | 山寨 ZFS |
实现方式 | 另v炉灶Q用U?Java 重新实现了对 ZIP 文g格式的处理代码?/td> | Z ZipFile ?ZipInputStream q两个已l稳定多q的c,但涉及了大量本地代码调用Q也怼影响性能?/td>
|
L?/td> | 支持Q且通过解压C时文件支持随问?/td> | 支持Q但不支持随问?/td> |
写操?/td> | 通过解压C时文件进行支持,但无法检到其他q程对同一?ZIP 文g的写操作Q不适用于ƈ发环境?/td> | 不支持。ZIP 文g事实上是一个整体,对内部条目的M修改都可能导致重构整个文Ӟ因此所谓的写操作必通过临时文g来处理,效率低下Q意义不大,而且难以处理嵌套 ZIP 文g。这也符合我的原则:不解压?/td> |
嵌套 ZIP 文g | 不支持?/td> | 支持Q当然读取嵌?ZIP 文g会慢一些?/td> |
反斜U分隔符 | 不支持,直接瓜掉?/td> | 支持Q且和标准的斜线分隔W区别对待。例如,/abc/ ?/abc\ 不同的文gQ实际上q两个能够ƈ存于 ZIP 文g中?/td>
|
I目录名 | 不支持,直接瓜掉?/td> | 支持。例?/a/b ?/a//b 是两个可以ƈ存且不同的文件?/td>
|
山寨 ZFS 的用法示例:
Map<String, Object> env = new HashMap<>(); // 用于解码 ZIP 条目名。默认ؓ Charset.defaultCharset()? env.put("charset", StandardCharsets.UTF_8); // 指示是否自动探测嵌套?ZIP 文g。默认ؓ false? env.put("autoDetect", true); // 默认目录Q用于创建和解析相对路径。默认ؓ?”? env.put("defaultDirectory", "/dir/"); // 从文件创Z?ZFS? try (FileSystem zfs = FileSystems.newFileSystem( URI.create("zip:" + Paths.get("docs.zip").toUri()), env)) { Path path = zfs.getPath("app.jar"); if ((Boolean) Files.getAttribute(path, "isZip")) { // 创徏一个嵌套的 ZFS? try (FileSystem nestedZfs = zfs.provider().newFileSystem(path, env)) { // 此处省略若干行? } } }
最后双手奉上源代码Q?a >LL处!
先看出错的代码:
public class Holder<T> { private T value; public Holder() { } public Holder(T value) { this.value = value; } public void setValue(T value) { this.value = value; } // 此处省略若干行? } Holder<Object> holder = new Holder<>("xxx");
看v来还好,但编译的时候却报错Q?/p>
Uncompilable source code - incompatible types required: zhyi.test.Holder<java.lang.Object> found: zhyi.test.Holder<java.lang.String>
老老实实把cd写出来就没问题:
Holder<Object> holder = new Holder<Object>("xxx");
如果非要用钻矌符的话Q可以采取下列两U方式之一Q?/p>
// 使用默认构造器Q再调用 setValue Ҏ? Holder<Object> holder = new Holder<>(); holder.setValue("xxx"); // 使用泛型通配W,但之后就不能调用 setValue 了,否则~译出错? Holder<? extends Object> holder = new Holder<>("xxx");
CyclicBarrier
的功能类g前面说到?CountDownLatch
Q用于让多个U程Q子dQ互相等待,直到共同到达公共屏障点(common barrier pointQ,在这个点上,所有的子Q务都已完成,从而主d完成?/p>
该类构造的时候除了必要指定U程数量Q还可以传入一?Runnable
对象Q它?run
Ҏ在到达公共屏障点后执行一ơ。子U程完成计算后,分别调用 CyclicBarrier#await
Ҏq入d状态,直到其他所有子U程都调用了 await
?/p>
下面仍然以运动员准备赛跑Z来说?CyclicBarrier
的用法:
final int count = 8; System.out.println("q动员开始就位?); final CyclicBarrier cb = new CyclicBarrier(count, new Runnable() { @Override public void run() { System.out.println("比赛开?.."); } }); for (int i = 1; i <= count; i++) { final int number = i; new Thread() { @Override public void run() { System.out.println(number + " 可动员到场q开始准?.."); try { // 准备 2~5 U钟? TimeUnit.SECONDS.sleep(new Random().nextInt(4) + 2); } catch (InterruptedException ex) { } System.out.println(number + " 可动员׃?); try { cb.await(); } catch (InterruptedException | BrokenBarrierException ex) { } } }.start(); } System.out.println("{待所有运动员׃...");
q行输出Q可能)为:
q动员开始就位? 1 可动员到场q开始准?.. 2 可动员到场q开始准?.. {待所有运动员׃... 3 可动员到场q开始准?.. 4 可动员到场q开始准?.. 6 可动员到场q开始准?.. 8 可动员到场q开始准?.. 5 可动员到场q开始准?.. 7 可动员到场q开始准?.. 1 可动员׃? 3 可动员׃? 8 可动员׃? 6 可动员׃? 2 可动员׃? 7 可动员׃? 5 可动员׃? 4 可动员׃? 比赛开?..
最后看?CyclicBarrier
?CountDownLatch
的主要异同:
Runnable
对象Q在d完成后自动调用,执行者ؓ某个子线E;后者可?await
Ҏ后手动执行一D代码实现相同的功能Q但执行者ؓȝE?/li>
思义Q?code>CountDownLatch 是一个用来倒计数的咚咚。如果某Q务可以拆分成若干个子d同时q行Q然后等待所有的子Q务完成,可以考虑使用它?/p>
该类的用法非常简单。首先构造一?CountDownLatch
Q唯一的参数是d数量Q一旦构造完毕就不能修改。接着启动所有的子Q务(U程Q,且每个子d在完成自q计算后,调用 CountDownLatch#countDown
Ҏ倒计数减一。最后在ȝE中调用 CountDownLatch#await
Ҏ{待计数器归零?/p>
例如赛跑的准备阶D,八名q动员先后到达v点做好准备,然后裁判打响发o枪,准备工作q束了Q比赛开始。如果把从运动员׃到发令枪响看做赛跑准备Q务,那么每个q动员的准备q程是其子dQ可以用 CountDownLatch
模拟如下Q?/p>
final int count = 8; System.out.println("q动员开始就位?); // 构?CountDownLatch? final CountDownLatch cdl = new CountDownLatch(count); for (int i = 1; i <= count; i++) { final int number = i; new Thread() { @Override public void run() { System.out.println(number + " 可动员到场q开始准?.."); try { // 让运动员随机准备 2~5 U钟? TimeUnit.SECONDS.sleep(new Random().nextInt(4) + 2); } catch (InterruptedException ex) { } System.out.println(number + " 可动员׃?); // 倒计数减一? cdl.countDown(); } }.start(); } System.out.println("{待所有运动员׃..."); try { // {待倒计数变?0? cdl.await(); System.out.println("比赛开始?); } catch (InterruptedException ex) { }
q行输出Q可能)为:
q动员开始就位? 1 可动员到场q开始准?.. 2 可动员到场q开始准?.. 4 可动员到场q开始准?.. {待所有运动员׃... 8 可动员到场q开始准?.. 6 可动员到场q开始准?.. 3 可动员到场q开始准?.. 7 可动员到场q开始准?.. 5 可动员到场q开始准?.. 6 可动员׃? 1 可动员׃? 5 可动员׃? 4 可动员׃? 7 可动员׃? 8 可动员׃? 2 可动员׃? 3 可动员׃? 比赛开始?/pre>从上面的例子q可以看?
]]>CountDownLatch
的局限性和CompletionService
cMQ在于无法处理子d数量不确定的情况Q例如统计某个文件夹中的文g数量。另外,如果某个子Q务在调用countDown
之前挂掉了Q倒计数就永远不会归零。对于这U情况,要么?finally
之类的手D保?countDown
一定会被调用,要么用带参数?await
Ҏ指定时旉?/p>
CompletionService
接口的实例可以充当生产者和消费者的中间处理引擎Q从而达到将提交d和处理结果的代码q行解耦的目的。生产者调?submit
Ҏ提交dQ而消费者调?poll
Q非dQ或 take
Q阻塞)Ҏ获取下一个结果:q一特征看v来和d队列Q?code>BlockingQueueQ类|两者的区别在于 CompletionService
要负责Q务的处理Q而阻塞队列则不会?/p>
?JDK 中,该接口只有一个实现类 ExecutorCompletionService
Q该cM用创建时提供?Executor
对象Q通常是线E池Q来执行dQ然后将l果攑օ一个阻塞队列中Q果然本是一家亲啊!ExecutorCompletionService
线E池和阻塞队列糅合在一P仅仅通过三个ҎQ就实现了Q务的异步处理Q可谓ƈ发编E初学者的兵利器Q?/p>
接下来看一个例子。楼L一大堆 *.java 文gQ需要计它们的代码总行数。利?ExecutorCompletionService
可以写出很简单的多线E处理代码:
public int countLines(List<Path> javaFiles) throws Exception { // Ҏ处理器数量创建线E池。虽然多U程q不保证能够提升性能Q但适量? // 开U程一般可以从pȝ骗取更多资源? ExecutorService es = Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors() * 2); // 使用 ExecutorCompletionService 内徏的阻塞队列? CompletionService cs = new ExecutorCompletionService(es); // 按文件向 CompletionService 提交d? for (final Path javaFile : javaFiles) { cs.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { // 略去计算单个文g行数的代码? return countLines(javaFile); } }); } try { int loc = 0; int size = javaFiles.size(); for (int i = 0; i < size; i++) { // take Ҏ{待下一个结果ƈq回 Future 对象。不直接q回计算l果是ؓ? // 捕获计算时可能抛出的异常? // poll 不等待,有结果就q回一?Future 对象Q否则返?null? loc += cs.take().get(); } return loc; } finally { // 关闭U程池。也可以线E池提升为字D以侉K用? // 如果dU程QCallable#callQ能响应中断Q用 shutdownNow 更好? es.shutdown(); } }
最后,CompletionService
也不是到处都能用Q它不适合处理d数量有限但个C可知的场景。例如,要统计某个文件夹中的文g个数Q在遍历子文件夹的时候也会“递归地”提交新的Q务,但最后到底提交了多少Q以及在什么时候提交完了所有Q务,都是未知敎ͼ无论 CompletionService
q是U程池都无法q行判断。这U情况只能直接用U程池来处理?/p>