Java学习(1)——(Demo)多线程读取文件
原文地址:http://blog.youkuaiyun.com/yzh201612/article/details/78301762
目标:
- 线程1:读取txt格式文件直到结束,每读取10个字符就通知线程2执行任务;
- 线程2:打印线程1刚读取到的10个字符,将其中的小写字母转换成大写字母并打印更改后的10个字符。结束后通知线程1执行任务。
代码及分析:
MyBufferCache.java
/*
* MyBufferCache.java
*
* MyBufferCache类
* 包含读文件read()和转换字符transform()方法
*/
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
public class MyBufferCache {
private char[] tenCharacters = new char[10];
private boolean isEnd = false;//判断是否读完文件
private boolean isEndPre = false;//isEnd前一状态
public boolean isSaved = false;//判断是否已经保存了一组字符
public boolean isEnd() {
return isEnd;
}
public synchronized void read() {
try {
// Read the file
File myFile = new File("MyText.txt");
FileReader fileReader = new FileReader(myFile);
BufferedReader reader = new BufferedReader(fileReader);
while(!isEnd) {
if(isSaved) {
wait();
}
else {
for(int i = 0; i<10; i++) {
int readOneCharacter;
do{
readOneCharacter = reader.read(); //读一个字符
}while('\r' == (char)(readOneCharacter) || '\n' == (char)(readOneCharacter));
char tmp = (char)(readOneCharacter);
if(readOneCharacter == -1) { //文档中无字符后结束保存
// isEndPre = isEnd;
isEnd = true;
break;
}//end if
tenCharacters[i] = tmp; //保存在数组内
}//end for
isSaved = true;
notifyAll();
}//end else
}//end while
reader.close();
}catch(Exception ex) {
System.out.println("Here");
ex.printStackTrace();
}
}//end read method
public synchronized void transform() {
try {
while(!isEndPre) {
if(!isSaved) {
wait();
}
else {
System.out.println(tenCharacters); //打印原始数组
for(int j = 0; j<10; j++) {
if(Character.isUpperCase(tenCharacters[j])) {
System.out.print(Character.toLowerCase(tenCharacters[j]));
}
else{
System.out.print(tenCharacters[j]);
}
}
System.out.println();
tenCharacters = null; //清空数组
tenCharacters = new char[10];
isSaved = false;
notifyAll();
isEndPre = isEnd;//放在这里而不是read()中,保证最后一组转换能够仅再执行一次
}
}
}
catch(InterruptedException ex) {
ex.printStackTrace();
}
}
}
MyBufferCache类中有公用的缓存数组tenCharacters[],以及针对该数组的两个方法:read()读文件并保存在数组中,transform()转换数组中字符。(Domain Driven Design 领域驱动设计:每个类都应该是完备的领域对象,MyBufferCache提供了缓存数组,那么就应该提供操作该数组的想关方法)
因为两个方法需要对同一个结构(数组)操作,将会有两个线程都要修改buffer对象(见下面的ReadFile.java)。所以将会有资源竞争的情况,于是引入同步监视器解决资源竞争(同步监视器的目的:组织两个线程对同一个共享资源进行并发访问)。
线程同步问题,处理办法是同步方法。将read()和transform()用synchronized修饰。此时默认同步监视器为调用该方法的对象,即下面main方法中新建的buffer对象。于是,对于这一个buffer对象,任意时刻只能有一个线程获得对buffer的锁定,然后进入read()或transform()方法执行对缓存数组的操作,保证了多线程并发对数组读写的数据安全。
线程通讯问题。因为两个线程需要交替循环执行,所以借用Object类提供的wait()/ notify()/ notifyAll()方法实现线程间通讯。
- wait():线程调用后会释放对同步监视器的锁定,线程暂停。可以加入时间参数,指定时间后自动唤醒等待其他线程释放锁定。
- notify():唤醒在此同步监视器上等待的(随机指定)单个线程。只有当前线程释放锁定时(调用wait()),其他被唤醒的线程才会被执行。
- notifyAll():唤醒在此监视器上等待的所有线程。其余同notify()。
线程结束问题,当线程执行完操作后会自动结束。因为设定的两个方法需要循环执行,所以需要设定结束标识。read()检测是否读到文档结尾,transform()检测是否读到文章结尾以及是否已经保存在缓存数组中。
ReadAFile.java
/*
* ReadAFile.java
* main方法,启动两个线程
*/
public class ReadAFile {
public static void main(String[] args) {
MyBufferCache buffer = new MyBufferCache();
new MyFileReader(buffer).start();
new MyFileTranslator(buffer).start();
}
}
在main方法里新建一个MyBuffCache对象buffer,将此对象传入两个线程。
曾出现的错误:两个线程各自生成独立对象,出现无法唤醒对方线程问题。
分析:因为独立生成的两个对象各自只有一个线程在操作,相当于两个人分别用了两把锁,各自独立。这样问题不单单是无法相互唤醒,更重要的是读取和转换操作将会分别操作两个数组,无效操作。
MyFileReader.java
/*
* MyFileReader.java
* 线程1
*/
public class MyFileReader extends Thread {
private MyBufferCache buffer;//引用变量
public MyFileReader(MyBufferCache buffer) {
this.buffer = buffer;
}
public void run() {
// System.out.println(Thread.currentThread().getName());
buffer.read();
}
}
MyFileTranslator.java
/*
* MyFileTranslator.java
* 线程2
*/
public class MyFileTranslator extends Thread {
private MyBufferCache buffer;//引用变量
public MyFileTranslator(MyBufferCache buffer) {
this.buffer = buffer;
}
public void run() {
// System.out.println(Thread.currentThread().getName());
buffer.transform();
}
}
两个线程,建立方法是继承自Thread类。(在《Head First Java》中不推荐这种做法,因为违背了“继承”的理念。其推荐的是借助Runnable类建立任务,然后由Thread去执行Runnable任务)
线程中声明MyBufferCache引用变量buffer。在构造方法中引入对象buffer,并将其赋值给引用变量。
效果:
MyText.txt
Console
理论补充:
参考:
《Head First Java》. Kathy Sierra & Bert Bates.
《疯狂Java讲义(第3版)》. 李刚.