JAVA IO之管道流总结大全(转)
要在文本框中显示控制台输出,我们必须用某种方法“截取”控制台流。换句话说,我们要有一种高效地读取写入到System.out和 System.err 所有内容的方法。如果你熟悉Java的管道流PipedInputStream和PipedOutputStream,就会相信我们已经拥有最有效的工 具。
写入到PipedOutputStream输出流的数据可以从对应的PipedInputStream输入流读取。Java的管道流极大地方便了我们截取控制台输出。Listing 1显示了一种非常简单的截取控制台输出方案。
【Listing 1:用管道流截取控制台输出】
PipedInputStream pipedIS = new PipedInputStream();
PipedOutputStream pipedOS = new PipedOutputStream();
try {
pipedOS.connect(pipedIS);
}
catch(IOException e) {
System.err.println("连接失败");
System.exit(1);
}
PrintStream ps = new PrintStream(pipedOS);
System.setOut(ps);
System.setErr(ps);
可
以看到,这里的代码极其简单。我们只是建立了一个PipedInputStream,把它设置为所有写入控制台流的数据的最终目的地。所有写入到控制台流
的数据都被转到PipedOutputStream,这样,从相应的PipedInputStream读取就可以迅速地截获所有写入控制台流的数据。接下
来的事情似乎只剩下在Swing
JTextArea中显示从pipedIS流读取的数据,得到一个能够在文本框中显示控制台输出的程序。遗憾的是,在使用Java管道流时有一些重要的注
意事项。只有认真对待所有这些注意事项才能保证Listing 1的代码稳定地运行。下面我们来看第一个注意事项。
1.1 注意事项一
PipedInputStream
运用的是一个1024字节固定大小的循环缓冲区。写入PipedOutputStream的数据实际上保存到对应的
PipedInputStream的内部缓冲区。从PipedInputStream执行读操作时,读取的数据实际上来自这个内部缓冲区。如果对应的
PipedInputStream输入缓冲区已满,任何企图写入PipedOutputStream的线程都将被阻塞。而且这个写操作线程将一直阻塞,直
至出现读取PipedInputStream的操作从缓冲区删除数据。
这意味着,向PipedOutputStream写数据的线程不
应该是负责从对应PipedInputStream读取数据的唯一线程。从图二可以清楚地看出这里的问题所在:假设线程t是负责从
PipedInputStream读取数据的唯一线程;另外,假定t企图在一次对
PipedOutputStream的write()方法的调用中向对应的PipedOutputStream写入2000字节的数据。在t线程阻塞之
前,它最多能够写入1024字节的数据(PipedInputStream内部缓冲区的大小)。然而,一旦t被阻塞,读取
PipedInputStream的操作就再也不会出现,因为t是唯一读取PipedInputStream的线程。这样,t线程已经完全被阻塞,同时,
所有其他试图向PipedOutputStream写入数据的线程也将遇到同样的情形。
这并不意味着在一次write()调用中不能写入多于1024字节的数据。但应当保证,在写入数据的同时,有另一个线程从PipedInputStream读取数据。
Listing
2示范了这个问题。这个程序用一个线程交替地读取PipedInputStream和写入PipedOutputStream。每次调用write()向
PipedInputStream的缓冲区写入20字节,每次调用read()只从缓冲区读取并删除10个字节。内部缓冲区最终会被写满,导致写操作阻
塞。由于我们用同一个线程执行读、写操作,一旦写操作被阻塞,就不能再从PipedInputStream读取数据。
【Listing 2:用同一个线程执行读/写操作导致线程阻塞】
import java.io.*;
public class Listing2 {
static PipedInputStream pipedIS = new PipedInputStream();
static PipedOutputStream pipedOS =
new PipedOutputStream();
public static void main(String[] a){
try {
pipedIS.connect(pipedOS);
}
catch(IOException e) {
System.err.println("连接失败");
System.exit(1);
}
byte[] inArray = new byte[10];
byte[] outArray = new byte[20];
int bytesRead = 0;
try {
// 向pipedOS发送20字节数据
pipedOS.write(outArray, 0, 20);
System.out.println(" 已发送20字节...");
// 在每一次循环迭代中,读入10字节
// 发送20字节
bytesRead = pipedIS.read(inArray, 0, 10);
int i=0;
while(bytesRead != -1) {
pipedOS.write(outArray, 0, 20);
System.out.println(" 已发送20字节..."+i);
i++;
bytesRead = pipedIS.read(inArray, 0, 10);
}
}
catch(IOException e) {
System.err.println("读取pipedIS时出现错误: " + e);
System.exit(1);
}
} // main()
}
只
要把读/写操作分开到不同的线程,Listing 2的问题就可以轻松地解决。Listing 3是Listing
2经过修改后的版本,它在一个单独的线程中执行写入PipedOutputStream的操作(和读取线程不同的线程)。为证明一次写入的数据可以超过
1024字节,我们让写操作线程每次调用PipedOutputStream的write()方法时写入2000字节。那么,在
startWriterThread()方法中创建的线程是否会阻塞呢?按照Java运行时线程调度机制,它当然会阻塞。写操作在阻塞之前实际上最多只能
写入1024字节的有效载荷(即PipedInputStream缓冲区的大小)。但这并不会成为问题,因为主线程(main)很快就会从
PipedInputStream的循环缓冲区读取数据,空出缓冲区空间。最终,写操作线程会从上一次中止的地方重新开始,写入2000字节有效载荷中的
剩余部分。
【Listing 3:把读/写操作分开到不同的线程】
import java.io.*;
public class Listing3 {
static PipedInputStream pipedIS =
new PipedInputStream();
static PipedOutputStream pipedOS =
new PipedOutputStream();
public static void main(String[] args) {
try {
pipedIS.connect(pipedOS);
}
catch(IOException e) {
System.err.println("连接失败");
System.exit(1);
}
byte[] inArray = new byte[10];
int bytesRead = 0;
// 启动写操作线程
startWriterThread();
try {
bytesRead = pipedIS.read(inArray, 0, 10);
while(bytesRead != -1) {
System.out.println("已经读取" +
bytesRead + "字节...");
bytesRead = pipedIS.read(inArray, 0, 10);
}
}
catch(IOException e) {
System.err.println("读取输入错误.");
System.exit(1);
}
} // main()
// 创建一个独立的线程
// 执行写入PipedOutputStream的操作
private static void startWriterThread() {
new Thread(new Runnable() {
public void run() {
byte[] outArray = new byte[2000];
while(true) { // 无终止条件的循环
try {
// 在该线程阻塞之前,有最多1024字节的数据被写入
pipedOS.write(outArray, 0, 2000);
}
catch(IOException e) {
System.err.println("写操作错误");
System.exit(1);
}
System.out.println(" 已经发送2000字节...");
}
}
}).start();// 注意:内部类使用哦........
} // startWriterThread()
} // Listing3
也许我们不能说这个问题是Java管道流设计上的缺陷,但在应用管道流时,它是一个必须密切注意的问题。下面我们来看看第二个更重要(更危险的)问题。
1.2 注意事项二
从PipedInputStream读取数据时,如果符合下面三个条件,就会出现IOException异常:
1. 试图从PipedInputStream读取数据,
2. PipedInputStream的缓冲区为“空”(即不存在可读取的数据),
3. 最后一个向PipedOutputStream写数据的线程不再活动(通过Thread.isAlive()检测)。
这
是一个很微妙的时刻,同时也是一个极其重要的时刻。假定有一个线程w向PipedOutputStream写入数据;另一个线程r从对应的
PipedInputStream读取数据。下面一系列的事件将导致r线程在试图读取PipedInputStream时遇到IOException异
常:
1. w向PipedOutputStream写入数据。
2. w结束(w.isAlive()返回false)。
3. r从PipedInputStream读取w写入的数据,清空PipedInputStream的缓冲区。
4. r试图再次从PipedInputStream读取数据。这时PipedInputStream的缓冲区已经为空,而且w已经结束,从而导致在读操作执行时出现IOException异常。
构
造一个程序示范这个问题并不困难,只需从Listing
3的startWriterThread()方法中,删除while(true)条件。这个改动阻止了执行写操作的方法循环执行,使得执行写操作的方法在
一次写入操作之后就结束运行。如前所述,此时主线程试图读取PipedInputStraem时,就会遇到一个IOException异常。
这是一种比较少见的情况,而且不存在直接修正它的方法。请不要通过从管道流派生子类的方法修正该问题――在这里使用继承是完全不合适的。而且,如果Sun以后改变了管道流的实现方法,现在所作的修改将不再有效。
最后一个问题和第二个问题很相似,不同之处在于,它在读线程(而不是写线程)结束时产生IOException异常。
1.3 注意事项三
如
果一个写操作在PipedOutputStream上执行,同时最近从对应PipedInputStream读取的线程已经不再活动(通过
Thread.isAlive()检测),则写操作将抛出一个IOException异常。假定有两个线程w和r,w向
PipedOutputStream写入数据,而r则从对应的PipedInputStream读取。下面一系列的事件将导致w线程在试图写入
PipedOutputStream时遇到IOException异常:
1. 写操作线程w已经创建,但r线程还不存在。
2. w向PipedOutputStream写入数据。
3. 读线程r被创建,并从PipedInputStream读取数据。
4. r线程结束。
5. w企图向PipedOutputStream写入数据,发现r已经结束,抛出IOException异常。
实际上,这个问题不象第二个问题那样棘手。和多个读线程/单个写线程的情况相比,也许在应用中有一个读线程(作为响应请求的服务器)和多个写线程(发出请求)的情况更为常见。
1.4 解决问题
要
防止管道流前两个局限所带来的问题,方法之一是用一个ByteArrayOutputStream作为代理或替代PipedOutputStream。
Listing
4显示了一个LoopedStreams类,它用一个ByteArrayOutputStream提供和Java管道流类似的功能,但不会出现死锁和
IOException异常。这个类的内部仍旧使用管道流,但隔离了本文介绍的前两个问题。我们先来看看这个类的公用方法(参见图3)。构造函数很简单,
它连接管道流,然后调用startByteArrayReaderThread()方法(稍后再讨论该方法)。getOutputStream()方法返
回一个OutputStream(具体地说,是一个ByteArrayOutputStream)用以替代PipedOutputStream。写入该
OutputStream的数据最终将在getInputStream()方法返回的流中作为输入出现。和使用PipedOutputStream的情形
不同,向ByteArrayOutputStream写入数据的线程的激活、写数据、结束不会带来负面效果。
【Listing 4:防止管道流应用中出现的常见问题】
import java.io.*;
public class LoopedStreams {
private PipedOutputStream pipedOS =
new PipedOutputStream();
private boolean keepRunning = true;
private ByteArrayOutputStream byteArrayOS =
new ByteArrayOutputStream() {
public void close() {
keepRunning = false;
try {
super.close();
pipedOS.close();
}
catch(IOException e) {
// 记录错误或其他处理
// 为简单计,此处我们直接结束
System.exit(1);
}
}
};
private PipedInputStream pipedIS = new PipedInputStream() {
public void close() {
keepRunning = false;
try {
super.close();
}
catch(IOException e) {
// 记录错误或其他处理
// 为简单计,此处我们直接结束
System.exit(1);
}
}
};
public LoopedStreams() throws IOException {
pipedOS.connect(pipedIS);
startByteArrayReaderThread();
} // LoopedStreams()
public InputStream getInputStream() {
return pipedIS;
} // getInputStream()
public OutputStream getOutputStream() {
return byteArrayOS;
} // getOutputStream()
private void startByteArrayReaderThread() {
new Thread(new Runnable() {
public void run() {
while(keepRunning) {
// 检查流里面的字节数
if(byteArrayOS.size() > 0) {
byte[] buffer = null;
synchronized(byteArrayOS) {
buffer = byteArrayOS.toByteArray();
byteArrayOS.reset(); // 清除缓冲区
}
try {
// 把提取到的数据发送给PipedOutputStream
pipedOS.write(buffer, 0, buffer.length);
}
catch(IOException e) {
// 记录错误或其他处理
// 为简单计,此处我们直接结束
System.exit(1);
}
}
else // 没有数据可用,线程进入睡眠状态
try {
// 每隔1秒查看ByteArrayOutputStream检查新数据
Thread.sleep(1000);
}
catch(InterruptedException e) {}
}
}
}).start();
} // startByteArrayReaderThread()
} // LoopedStreams
startByteArrayReaderThread()
方法是整个类真正的关键所在。这个方法的目标很简单,就是创建一个定期地检查
ByteArrayOutputStream缓冲区的线程。缓冲区中找到的所有数据都被提取到一个byte数组,然后写入到
PipedOutputStream。由于PipedOutputStream对应的PipedInputStream由getInputStream
()返回,从该输入流读取数据的线程都将读取到原先发送给ByteArrayOutputStream的数据。前面提到,LoopedStreams类解
决了管道流存在的前二个问题,我们来看看这是如何实现的。
ByteArrayOutputStream具有根据需要扩展其内部缓冲区的
能力。由于存在“完全缓冲”,线程向getOutputStream()返回的流写入数据时不会被阻塞。因而,第一个问题不会再给我们带来麻烦。另外还要
顺便说一句,ByteArrayOutputStream的缓冲区永远不会缩减。例如,假设在能够提取数据之前,有一块500
K的数据被写入到流,缓冲区将永远保持至少500 K的容量。如果这个类有一个方法能够在数据被提取之后修正缓冲区的大小,它就会更完善。
第
二个问题得以解决的原因在于,实际上任何时候只有一个线程向PipedOutputStream写入数据,这个线程就是由
startByteArrayReaderThread()创建的线程。由于这个线程完全由LoopedStreams类控制,我们不必担心它会产生
IOException异常。
LoopedStreams类还有一些细节值得提及。首先,我们可以看到byteArrayOS和
pipedIS实际上分别是
ByteArrayOutputStream和PipedInputStream的派生类的实例,也即在它们的close()方法中加入了特殊的行为。如
果一个LoopedStreams对象的用户关闭了输入或输出流,在startByteArrayReaderThread()中创建的线程必须关闭。覆
盖后的close()方法把keepRunning标记设置成false以关闭线程。另外,请注意startByteArrayReaderThread
()中的同步块。要确保在toByteArray()调用和reset()调用之间ByteArrayOutputStream缓冲区不被写入流的线程修
改,这是必不可少的。由于ByteArrayOutputStream的write()方法的所有版本都在该流上同步,我们保证了
ByteArrayOutputStream的内部缓冲区不被意外地修改。
注意LoopedStreams类并不涉及管道流的第三个问
题。该类的getInputStream()方法返回PipedInputStream。如果一个线程从该流读取,一段时间后终止,下次数据从
ByteArrayOutputStream缓冲区传输到PipedOutputStream时就会出现 IOException异常。
二、捕获Java控制台输出
Listing 5的ConsoleTextArea类扩展Swing JTextArea捕获控制台输出。不要对这个类有这么多代码感到惊讶,必须指出的是,ConsoleTextArea类有超过50%的代码用来进行测试。
【Listing 5:截获Java控制台输出】
import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.text.*;
public class ConsoleTextArea extends JTextArea {
public ConsoleTextArea(InputStream[] inStreams) {
for(int i = 0; i < inStreams.length; ++i)
startConsoleReaderThread(inStreams[i]);
} // ConsoleTextArea()
public ConsoleTextArea() throws IOException {
final LoopedStreams ls = new LoopedStreams();
// 重定向System.out和System.err
PrintStream ps = new PrintStream(ls.getOutputStream());
System.setOut(ps);
System.setErr(ps);
startConsoleReaderThread(ls.getInputStream());
} // ConsoleTextArea()
private void startConsoleReaderThread(
InputStream inStream) {
final BufferedReader br =
new BufferedReader(new InputStreamReader(inStream));
new Thread(new Runnable() {
public void run() {
StringBuffer sb = new StringBuffer();
try {
String s;
Document doc = getDocument();
while((s = br.readLine()) != null) {
boolean caretAtEnd = false;
caretAtEnd = getCaretPosition() == doc.getLength() ?
true : false;
sb.setLength(0);
append(sb.append(s).append('\n').toString());
if(caretAtEnd)
setCaretPosition(doc.getLength());
}
}
catch(IOException e) {
JOptionPane.showMessageDialog(null,
"从BufferedReader读取错误:" + e);
System.exit(1);
}
}
}).start();
} // startConsoleReaderThread()
// 该类剩余部分的功能是进行测试
public static void main(String[] args) {
JFrame f = new JFrame("ConsoleTextArea测试");
ConsoleTextArea consoleTextArea = null;
try {
consoleTextArea = new ConsoleTextArea();
}
catch(IOException e) {
System.err.println(
"不能创建LoopedStreams:" + e);
System.exit(1);
}
consoleTextArea.setFont(java.awt.Font.decode("monospaced"));
f.getContentPane().add(new JScrollPane(consoleTextArea),
java.awt.BorderLayout.CENTER);
f.setBounds(50, 50, 300, 300);
f.setVisible(true);
f.addWindowListener(new java.awt.event.WindowAdapter() {
public void windowClosing(
java.awt.event.WindowEvent evt) {
System.exit(0);
}
});
// 启动几个写操作线程向
// System.out和System.err输出
startWriterTestThread(
"写操作线程 #1", System.err, 920, 50);
startWriterTestThread(
"写操作线程 #2", System.out, 500, 50);
startWriterTestThread(
"写操作线程 #3", System.out, 200, 50);
startWriterTestThread(
"写操作线程 #4", System.out, 1000, 50);
startWriterTestThread(
"写操作线程 #5", System.err, 850, 50);
} // main()
private static void startWriterTestThread(
final String name, final PrintStream ps,
final int delay, final int count) {
new Thread(new Runnable() {
public void run() {
for(int i = 1; i <= count; ++i) {
ps.println("***" + name + ", hello !, i=" + i);
try {
Thread.sleep(delay);
}
catch(InterruptedException e) {}
}
}
}).start();
} // startWriterTestThread()
} // ConsoleTextArea
main()
方法创建了一个JFrame,JFrame包含一个ConsoleTextArea的实例。这些代码并没有什么特别之处。Frame显示出来之
后,main()方法启动一系列的写操作线程,写操作线程向控制台流输出大量信息。ConsoleTextArea捕获并显示这些信息,如图一所示。
ConsoleTextArea
提供了两个构造函数。没有参数的构造函数用来捕获和显示所有写入到控制台流的数据,有一个InputStream[]参数的构造函数转发所有从各个数组元
素读取的数据到JTextArea。稍后将有一个例子显示这个构造函数的用处。首先我们来看看没有参数的
ConsoleTextArea构造函数。这个函数首先创建一个LoopedStreams对象;然后请求Java运行时环境把控制台输出转发到
LoopedStreams提供的OutputStream;最后,构造函数调用startConsoleReaderThread(),创建一个不断地
把文本行追加到JTextArea的线程。注意,把文本追加到JTextArea之后,程序小心地保证了插入点的正确位置。
一般来
说,Swing部件的更新不应该在AWT事件分派线程(AWT Event Dispatch
Thread,AEDT)之外进行。对于本例来说,这意味着所有把文本追加到JTextArea的操作应该在AEDT中进行,而不是在
startConsoleReaderThread()方法创建的线程中进行。然而,事实上在Swing中向JTextArea追加文本是一个线程安全的
操作。读取一行文本之后,我们只需调用JText.append()就可以把文本追加到JTextArea的末尾。
三、捕获其他程序的控制台输出
在
JTextArea中捕获Java程序自己的控制台输出是一回事,去捕获其他程序(甚至包括一些非Java程序)的控制台数据又是另一回事。
ConsoleTextArea提供了捕获其他应用的输出时需要的基础功能,Listing
6的AppOutputCapture利用ConsoleTextArea,截取其他应用的输出信息然后显示在ConsoleTextArea中。
【Listing 6:截获其他程序的控制台输出】
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;
public class AppOutputCapture {
private static Process process;
public static void main(String[] args) {
if(args.length == 0) {
System.err.println("用法:java AppOutputCapture " +
"<程序名字> {参数1 参数2 ...}");
System.exit(0);
}
try {
// 启动命令行指定程序的新进程
process = Runtime.getRuntime().exec(args);
}
catch(IOException e) {
System.err.println("创建进程时出错...\n" + e);
System.exit(1);
}
// 获得新进程所写入的流
InputStream[] inStreams =
new InputStream[] {
process.getInputStream(),process.getErrorStream()};
ConsoleTextArea cta = new
ConsoleTextArea(inStreams);
cta.setFont(java.awt.Font.decode("monospaced"));
JFrame frame = new JFrame(args[0] +
"控制台输出");
frame.getContentPane().add(new JScrollPane(cta),
BorderLayout.CENTER);
frame.setBounds(50, 50, 400, 400);
frame.setVisible(true);
frame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent evt) {
process.destroy();
try {
process.waitFor(); // 在Win98下可能被挂起
}
catch(InterruptedException e) {}
System.exit(0);
}
});
} // main()
}
JAVA IO之管道流总结大全(转)的更多相关文章
- 系统学习 Java IO (六)----管道流 PipedInputStream/PipedOutputStream
目录:系统学习 Java IO---- 目录,概览 PipedInputStream 类使得可以作为字节流读取管道的内容. 管道是同一 JVM 内的线程之间的通信通道. 使用两个已连接的管道流时,要为 ...
- java io之管道流
一.java.io中存在一中流叫管道流,类似管道的功能.PipedOutputStream和PipedInputStream分别是管道输出流和管道输入流.这个两个流必须同时使用. 二.作用:进行两个线 ...
- java多线程通过管道流实现不同线程之间的通信
java中的管道流(pipeStream)是一种特殊的流,用于在不同线程间直接传送数据.一个线程发送数据到输出管道,另外一个线程从输入管道中读取数据.通过使用管道,实现不同线程间的通信,而不必借助类似 ...
- Java IO 文件与流基础
Java IO 文件与流基础 @author ixenos 摘要:创建文件.文件过滤.流分类.流结构.常见流.文件流.字节数组流(缓冲区) 如何创建一个文件 #当我们调用File类的构造器时,仅仅是在 ...
- Java IO之处理流
一.处理流: 增强功能,提供性能,在节点流之上. 二.节点流与处理流的关系 节点流(字节流.字符流)处于IO操作的第一线,所有操作必须通过它们进行: 处理流可以对其他流进行处理(提高效率或操作灵活性) ...
- Java IO: 其他字符流(下)
作者: Jakob Jenkov 译者: 李璟(jlee381344197@gmail.com) 本小节会简要概括Java IO中的PushbackReader,LineNumberReader,St ...
- Java IO之字符流和文件
前面的博文介绍了字节流,那字符流又是什么流?从字面意思上看,字节流是面向字节的流,字符流是针对unicode编码的字符流,字符的单位一般比字节大,字节可以处理任何数据类型,通常在处理文本文件内容时,字 ...
- Java使用PipedStream管道流通信
多线程使用PipedStream 通讯 Java 提供了四个相关的管道流,我们可以使用其在多线程进行数据传递,其分别是 类名 作用 备注 PipedInputStream 字节管道输入流 字节流 Pi ...
- [Java IO]03_字符流
Java程序中,一个字符等于两个字节. Reader 和 Writer 两个就是专门用于操作字符流的类. Writer Writer是一个字符流的抽象类. 它的定义如下: public abstra ...
随机推荐
- bzoj 1200: [HNOI2005]木梳 DP
1200: [HNOI2005]木梳 Time Limit: 10 Sec Memory Limit: 162 MBSubmit: 266 Solved: 125[Submit][Status] ...
- 将excel数据导入内表的函数
call function 'TEXT_CONVERT_XLS_TO_SAP' exporting i_tab_raw_data = lt_raw "开始行 ...
- 【技术贴】jsp出现getOutputStream() has already been calle
此错误经常在websphere6.x版本里出现:原因是jsp文件中的尖括号百分号里面有空行或者其他的什么原因,Servlet1.2规范规定了OutputStream只能获得一次,在jsp中实际上已经通 ...
- 程序异常捕获库 - CrashRpt
CrashRpt.dll用来在应用程序出现异常crash时,捕获到错误. 并收集出错信息: MiniDump文件.硬件信息.系统信息.出错信息.进程信息.服务信息.驱动信息.启动信息.软件列表.端口信 ...
- Webx之表单验证
引入服务器端表单验证service,是通过在webx.xml中通过服务引入的方式完成的.例如,在user相关信息的表单验证的产生过程是这样的:webx-user.xml通过 <beans:imp ...
- 2.6.2 Notification的功能与用法
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout ...
- Oracle core03_ACID
ACID特性 oracle如何使用undo和redo来保证了关系数据库的ACID特性. ACID的特性简单描述为: Atomic:以事务为单位的原子性 Consistency:保证数据一致性 Isol ...
- The server is temporarily unable to service your request due to maintenance downtime or capacity problems. Please try again later.
The server is temporarily unable to service your request due to maintenance downtime or capacity pro ...
- Lua运算符
+ 加- 减* 乘/ 除^ 幂% 求余# 求长度= 赋值< 小于> 大于== 等于<= 不大于=> 不小于~= 不等于and 逻辑与or 逻辑或not 逻辑非.. 连接字符串
- SQL重复记录处理(查找,过滤,删除)
SQL重复记录处理(查找,过滤,删除) ID int Title nvarchar(50) AddDate datetime 数据 www.2cto.com ID ...