题目要求:一个文件中有10000个数,用Java实现一个多线程程序将这个10000个数输出到5个不用文件中(不要求输出到每个文件中的数量相同)。要求启动10个线程,两两一组,分为5组。每组两个线程分别将文件中的奇数和偶数输出到该组对应的一个文件中,需要偶数线程每打印10个偶数以后,就将奇数线程打印10个奇数,如此交替进行。同时需要记录输出进度,每完成1000个数就在控制台中打印当前完成数量,并在所有线程结束后,在控制台打”Done”。
思路
- 因为是多线程,要提升效率的话,首先要把全部数读到内存里,并且分为两个队列,一个奇数队列,一个偶数队列,这两个队列使用
BlockingQueue
,保证线程安全。 - 打印到五个不同文件,说明是五个共享资源,而10个线程,两两一组,就很好实现了,一组线程使用一个同步控制机制(我选择了使用锁和通知机制)。
- 一组线程见实现轮流打印十个数字,问题就简化成了两个线程间的同步问题,这种模型比较常见,可以看作一个生产者消费者模型的变种。
- 其他一些细节的实现,比如计数还有所有线程结束以后打印,很容易想到使用JUC包中的
Atomic类
和CountDownLatch
这些工具类,保证线程安全的情况下方便的实现功能。 - 考虑到奇数和偶数数量可能不相等的情况,还需要进行特殊控制。
一组奇偶打印线程的实现
public class Worker {
private Lock lock; // 并发控制
private Condition condition;
private BlockingQueue<Integer> oddQueue; // 奇数队列
private BlockingQueue<Integer> evenQueue; // 偶数队列
private AtomicBoolean flag; // 标志位,当其中一个队列为空以后,变为true
private CountDownLatch latch; // 计次锁
private File file; // 要写入的目的文件
private AtomicInteger totalCount; // 总输出数
public Worker(BlockingQueue<Integer> oddQueue, BlockingQueue<Integer> evenQueue, AtomicBoolean flag,
CountDownLatch latch, File file, AtomicInteger totalCount) {
// 这里的锁,一组线程有一个,为了控制这两个写线程间的同步
this.lock = new ReentrantLock();
this.condition = lock.newCondition();
this.oddQueue = oddQueue;
this.evenQueue = evenQueue;
this.flag = flag;
this.latch = latch;
this.file = file;
this.totalCount = totalCount;
}
/**
* 入口函数
*/
public void start() {
new Thread(new Handler(lock, condition, oddQueue, flag, latch, new PrintUtil(file), totalCount), "odd")
.start();
new Thread(new Handler(lock, condition, evenQueue, flag, latch, new PrintUtil(file), totalCount), "even")
.start();
}
/**
* 写入文件类,使用的是阻塞式IO
* 如果考虑性能优化的话,可以使用一个缓冲区,最后一次性全部写入文件
* 也可以直接使用NIO中的buffer和Channel实现
* 但是如果程序被中断,就不会有写入文件的操作
* 所以为了满足实际情况,采用了对每个数写入都进行一次IO的做法
*
* @author CringKong
*
*/
static class PrintUtil {
File file;
public PrintUtil(File file) {
this.file = file;
}
public void printToFile(int i) throws FileNotFoundException {
DataOutputStream dataOutputStream = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(file, true)));
PrintWriter printWriter = new PrintWriter(dataOutputStream);
try {
printWriter.print(i);
printWriter.println();
dataOutputStream.flush();
printWriter.close();
dataOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 实际工作类
*
* @author CringKong
*
*/
static class Handler implements Runnable {
public Handler(Lock lock, Condition condition, BlockingQueue<Integer> queue, AtomicBoolean flag,
CountDownLatch latch, PrintUtil tools, AtomicInteger totalCount) {
super();
this.lock = lock;
this.condition = condition;
this.queue = queue;
this.flag = flag;
this.latch = latch;
this.tools = tools;
this.totalCount = totalCount;
}
private Lock lock;
private Condition condition;
private BlockingQueue<Integer> queue;
private AtomicBoolean flag;
private CountDownLatch latch;
PrintUtil tools;
AtomicInteger totalCount;
@Override
public void run() {
lock.lock();
try {
int count = 0;
while (true) { // 因为是循环打印直到队列空,所以while循环
if (flag.get()) { // 如果奇数或者偶数队列有一个已经为空,就直接一直从另一个队列打印
Integer integer = queue.poll();
if (integer == null) {
condition.signal(); // 这里需要唤醒一下另一个线程,因为如果另一个队列提前为空,就会进入等待状态
break;
}
int tem;
if ((tem = totalCount.incrementAndGet()) % 1000 == 0) {
System.out.println("总完成数" + tem); // 每1000个打印一次
}
tools.printToFile(integer);
} else {
if (count == 10) { // 打印十个以后,就换另一个组员线程打印
count = 0; // 计数器置0
condition.signal(); // 唤醒另一个组员写线程
condition.await(); // 将自己等待
}
Integer integer = queue.poll(); // 从队列中取值
if (integer == null) {
condition.signal(); // 直接唤醒另一个组员线程
flag.set(true); // 说明奇数或者偶数队列有一个已经空了
break;
}
count++;
int tem;
if ((tem = totalCount.incrementAndGet()) % 1000 == 0) {
System.out.println("总完成数" + tem);
}
tools.printToFile(integer);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 一个线程已经结束了
lock.unlock();
}
}
}
}
全部线程的控制
public class Boss {
public static void main(String[] args) throws IOException {
AtomicInteger totalCount = new AtomicInteger(0); // 初始化打印总数是0
AtomicBoolean flag = new AtomicBoolean(false); // 初始为false,认为两个队列初始都不为空
CountDownLatch latch = new CountDownLatch(10); // 10个线程完成以后,打印DONE
BlockingQueue<Integer> oddQueue = new LinkedBlockingQueue<>(); // 奇数的阻塞队列
BlockingQueue<Integer> evenQueue = new LinkedBlockingQueue<>(); // 偶数的阻塞队列
File srcFile = new File("test.txt");
DataInputStream dataInputStream = new DataInputStream(new BufferedInputStream(new FileInputStream(srcFile)));
try {
while(dataInputStream.available() != 0) { // 这个过程是读文件中的数字,然后分别添加进奇偶数组
int x = dataInputStream.readInt();
if ((x&1) == 1) {
oddQueue.add(x);
}else {
evenQueue.add(x);
}
}
} catch (IOException e) {
e.printStackTrace();
}
Worker[] workers = new Worker[5];
Queue<File>fileList = new ArrayDeque<>();
for(int i=0;i<5;i++) { // 输出到5个文件
File tem = new File("file"+i+".txt");
fileList.add(tem);
}
for (Worker worker : workers) { // 启动5组线程
worker = new Worker(oddQueue, evenQueue, flag, latch, fileList.poll(), totalCount);
worker.start();
}
try {
latch.await(); // 主线程进入阻塞,直到latch计数到0被唤醒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("DONE");
}
}
测试数据构建
DataOutputStream dataOutputStream = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
for(int i=0;i<10000;i++) {
int tem = (int) (Math.random()*10000); // 全部随机生成了
try {
dataOutputStream.writeInt(tem);
dataOutputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
运行测试
可以看到,控制台打印是没有问题的。
再来看文件:
我们随便打开一个文件:
这里为了方便观察,向文件写的时候,加了换行符。
是奇数偶数交替打印的,而我们生产的数据,奇数偶数不一样多,再来看一下文件末尾。
可以看到,奇数打印了9个,剩下的全部是偶数,说明这次生成的数据中,偶数比奇数多,打印奇数的线程,因为阻塞队列为空,提前进入了等待状态,唤醒了偶数打印线程。
总结:
刚刚看到要求的时候,还是很容易懵逼的,但是只要细心分析,结合多线程的知识,灵活使用JUC提供的强大的辅助类,解决多线程编程方面的问题,Java还是比其他语言简单很多的。
我这个思路就是从Java线程池中提吸取了一些想法,结合消费者生产者模型的运用,所以总结还是,多看源码多学基础,不要闭门造车也不要不思进取。