目录
(二)实现java.lang.Runnable接口创建线程:
一、基本概念
1.并发:一个CPU同时执行多个任务,指两个或多个事件在同一个时间段内发生。比如:秒杀、多个人做同一件事。
2.并行:多个CPU同时执行多个任务,指两个或多个事件在同一时刻发生(同时发生)。比如:多个人同时做不同的事。
3.进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程(至少有一个
),比如window系统中运行的WeChat.exe就是一个进程。
4.线程:是CPU调度和分派的基本单位
,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
package com.atguigu.practices.thread;
public class MainThread {
public static void main(String[] args) {
// 获取当前线程对象
Thread thread = Thread.currentThread();
// 获取当前线程对象的名字
String name = thread.getName();
System.out.println("当前线程的名字:"+name);// 当前线程的名字:main
thread.setName("myFirstThread");
String name1 = thread.getName();
System.out.println("更改后线程的名字:"+name1);// 更改后线程的名字:myFirstThread
}
}
二、创建线程的两种方式:
(一)继承java.lang.Thread类
继承Thread类创建线程的步骤:
1.定义MyThread类继承Thread类
2.重写run()方法,编写线程执行体
3.创建线程对象,调用start()方法启动线程
package com.atguigu.practices.thread;
// 创建线程
public class MyFirstThread extends Thread{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
package com.atguigu.practices.thread;
public class TestMyFirstThread {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
// 创建两个线程
MyFirstThread feifei = new MyFirstThread();
MyFirstThread xianmei = new MyFirstThread();
// 启动两个线程
feifei.start();
xianmei.start();
// 启动主线程
System.out.println("over");
}
}
控制台输出
(二)实现java.lang.Runnable接口创建线程:
1.定义MyRunnable类实现Runnable接口
2.实现run()方法,编写线程执行体
3.创建线程对象,调用start()方法启动线程
package com.atguigu.practices.thread;
public class MyFirstThread2 implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
(三)比较两种创建线程的方式:
继承Thread类:
- 编写简单,可直接操作线程
- 适用于单继承
实现Runnable接口
- 避免单继承局限性
- 便于共享资源
推荐使用Runnable接口方式创建线程
对比两种方法创建线程的具体例子:
继承java.lang.Thread类:
package com.atguigu.practices.thread;
public class TicketThread extends Thread{
private int ticket = 10;
@Override
public void run() {
// 销售10张票
for (int i = 1; i <= 10; i++) {
if(this.ticket>0){
System.out.println(Thread.currentThread().getName()+"买票--"+this.ticket--);
}
}
}
public class TicketThreadTest {
public static void main(String[] args) {
TicketThread window1 = new TicketThread();
window1.setName("一号窗口");
window1.start();
TicketThread window2 = new TicketThread();
window2.setName("二号窗口");
window2.start();
// 输出的票号有重复,线程不安全
}
}
实现java.lang.Runnable接口创建线程:
package com.atguigu.practices.thread;
public class TicketRunnable implements Runnable{
private int ticket = 10;
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
if(this.ticket>0){
System.out.println(Thread.currentThread().getName()+"买票--"+this.ticket--);
}
}
}
}
package com.atguigu.practices.thread;
public class TicketRunnableTest {
public static void main(String[] args) {
TicketRunnable window = new TicketRunnable();
Thread window1 = new Thread(window, "窗口一");
window1.start();
Thread window2 = new Thread(window, "窗口二");
window2.start();
// 共用一个资源,线程安全
}
}
控制台输出的结果不重复,线程安全(后面会讲到)
三、线程的生命周期
package com.atguigu.practices.thread;
public class MtThread2 implements Runnable{
public static void main(String[] args) {
MtThread2 runnable = new MtThread2();
Thread thread = new Thread(runnable);
System.out.println("线程处于创建状态");
thread.start();
System.out.println("线程处于就绪状态!");
}
@Override
public void run() {
System.out.println("线程正在运行,处于运行状态");
try {
System.out.println("线程开始休眠,处于阻塞状态!");
Thread.sleep(5000);
System.out.println("线程休眠结束,阻塞状态结束,再次进入就绪状态!");
// 线程休眠结束后又会进入到就绪状态,等待CPU给线程分配资源
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
控制台输出:
四、线程调度
线程调度指按照特定机制为多个线程分配CPU的使用权
方法 | 说明 |
void setPriority(int newPriority) | 更改线程的优先级 |
static void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠 |
void join() | 等待该线程终止 |
static void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
void interrupt() | 中断线程 |
boolean isAlive() | 测试线程是否处于活动状态 |
(一)设置线程的优先级——开发过程中较少用
线程优先级由1~10表示,1最低,默认优先级为5
优先级高的线程获得CPU资源的概率较大
package com.atguigu.practices.thread;
// 创建线程
public class MyFirstThread extends Thread{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
package com.atguigu.practices.thread;
public class TestMyFirstThread {
public static void main(String[] args) {
// 创建主线程
Thread thread = Thread.currentThread();
// 获取主线程的名字
System.out.println(thread.getName());
MyFirstThread2 feifei = new MyFirstThread2();
MyFirstThread2 xianmei = new MyFirstThread2();
Thread threadFeifei = new Thread(feifei, "threadFeifei");
Thread threadXianmei = new Thread(xianmei, "threadXianmei");
// 启动之前可以设置优先级,CPU先给谁分配资源也只是概率问题,优先级高的线程不一定先执行!
threadFeifei.setPriority(Thread.MAX_PRIORITY);
threadXianmei.setPriority(Thread.MIN_PRIORITY);
threadFeifei.start();
threadXianmei.start();
System.out.println("over");
}
}
控制台输出
public class MyThread2 implements Runnable {
public static void main(String[] args) {
MyThread2 runnable = new MyThread2();
Thread thread = new Thread(runnable);
System.out.println("线程处于创建状态");
thread.start();
System.out.println("线程处于就绪状态!");
}
@Override
public void run() {
System.out.println("线程正在运行,处理运行状态");
try {
System.out.println("线程开始休眠,处于阻塞状态!");
Thread.sleep(5000);
System.out.println("线程休眠结束,阻塞状态结束,再次进入就绪状态!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package com.atguigu.practices.thread;
public class JoinTest {
public static void main(String[] args) {
MtThread2 runnable = new MtThread2();
Thread t = new Thread(runnable, "mythread2");
t.start();
// 循环打印,main线程做的事
for (int i = 1; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
if(i==5){
// Thread.yield();// 礼让,不一定能成功,所以要少用
try {
t.join();// 等待该线程终止,一定能够抢到CPU资源
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
控制台输出:
(二)线程的礼让
暂停当前线程,允许其他具有相同优先级的线程获得运行机会
该线程处于就绪状态,不转为阻塞状态
public static void yield()
(三)练习题1——模拟多人爬山
需求说明:
1.每个线程代表一个人
2.可设置每人爬山速度
3.可爬完100米显示信息
4.爬到终点时给出相应提示
package com.atguigu.practices.thread;
public class ClimbThread extends Thread{
// 实现思路
/*
* 创建线程类ClimbThread
* * 属性:爬100米市场(time),爬多少个100米(num)
* * 构造方法完成属性初始化
* 实现run()方法
* * 线程休眠模拟爬山中的延时
* 实现测试类Test
* * 创建多个线程对象模拟多个人,设置人名、爬100米时长
* */
private int time;// 每爬100米所需要的时间(秒)
private int num;// mile / 100
/**
* name 爬山人
* time 爬山人,每爬一百米所需要的时间
* mile 山的高度
*/
public ClimbThread(String name,int time,int mile){
super(name);
this.time = time*1000;// 2000 1000
this.num = mile/100;// 10
}
@Override
public void run() {
int runNum = 0;// 每次爬的距离
while (runNum<this.num){// num>0就代表没有爬到山顶
try {
Thread.sleep(this.time);
runNum++;
// 每次休眠
System.out.println(Thread.currentThread().getName()+"爬完了"+(runNum*100)+"米");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 爬到了山顶
System.out.println(TicketThread.currentThread().getName()+"到达了山顶!");
}
public static void main(String[] args) {
ClimbThread oldman = new ClimbThread("老年人",2,1000);
ClimbThread youngman = new ClimbThread("年轻人",1,1000);
oldman.start();
youngman.start();
}
}
(四)练习题2——特需号与普通号看病
需求说明
某科室一天需看普通号20个,特需号10个特需号看病时间是普通号的2倍,开始时普通号和特需号并行叫号,叫到特需号的概率比普通号高,当普通号叫完第10号时,要求先看完全部特需号,再看普通号,使用多线程模拟这一过程。
package com.atguigu.practices.thread;
// 专家号
public class SpecialThread implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println("专家号:"+i+"号病人正在看病");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
package com.atguigu.practices.thread;
// 将普通号当做主线程
public class CommonThread {
public static void main(String[] args) {
// 专家号和普通号两个线程同时执行,普通号是主线程,专家号是子线程
SpecialThread specialThread = new SpecialThread();
Thread special = new Thread(specialThread, "special");
// 叫到特需号的概率比普通号高
special.setPriority(Thread.MAX_PRIORITY);
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
special.start();
// 20个普通号
for (int i = 1; i <= 20; i++) {
System.out.println("普通号:"+i+"号病人正在看病");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 当普通号叫完第10号时,要求先看完全部特需号,再看普通号
if(i==10){
//
try {
special.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
五、线程安全
(一)synchronized关键字
上文对于线程安全有所提及,下面我们针对这一现象进行详细讲解
线程不安全,通俗的理解就是多人买到相同的票号,资源没有进行共享。我们将上面的TicketRunnable改一下:
public class TicketRunnable implements Runnable {
private int ticket = 100;// 记录车票总数
private int num = 0;// 记录用户抢到了第几张票
@Override
public void run() {
while (true){
if(ticket<=0){
break;// 票卖光了
}
// 有多余的车票,抢票
// 第一步,修改总票数:ticket-1
// 第二步:用户抢到票,抢票数+1
ticket--;
num++;
// 每次抢完票休息0.5秒
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 阻塞状态结束后,显示出票数据反馈给控制台
System.out.println(Thread.currentThread().getName()+"抢到了第"+num+"张票,剩余"+ticket+"张票");
}
}
/*private int ticket = 10;
@Override
public void run() {
for (int i=1; i<=10; i++){
if (this.ticket>0){
System.out.println(Thread.currentThread().getName()+"买票--"+this.ticket--);
}
}
}*/
}
// 测试类
public class TicketRunnableTest {
public static void main(String[] args) {
/*TicketRunnable window = new TicketRunnable();
Thread window1 = new Thread(window, "window1");
Thread window2 = new Thread(window, "window2");
window1.start();
window2.start();*/
TicketRunnable user = new TicketRunnable();
Thread user1 = new Thread(user, "zhangsan");
Thread user2 = new Thread(user, "lisi");
Thread user3 = new Thread(user, "wangwu");
user1.start();
user2.start();
user3.start();
}
}
控制台输出:
我们可以看到还是会有用户抢到相同票号的情况,多个线程争抢CPU资源,但是多个线程之间没有关系,这个时候,我们就需要给CPU的入口添加一个锁,这样即使多个线程同时执行,但是面对资源时,只能有一个线程抢占CPU的资源。这个线程抢好离开后,下一个线程才能进入。
这个入口就是关键字:synchronized
synchronized可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象Object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍可以访问该object中的非加锁代码块。
synchronized后面的括号可以时任意对象,写this关键字调用本类对象也可以。
下面时加上synchronized关键字后的代码:
public class TicketRunnable implements Runnable {
private int ticket = 100;// 记录车票总数
private int num = 0;// 记录用户抢到了第几张票
Byte b = new Byte("1");
@Override
public void run() {
while (true){
synchronized (b){
if(ticket<=0){
break;// 票卖光了
}
// 有多余的车票,抢票
// 第一步,修改总票数:ticket-1
// 第二步:用户抢到票,抢票数+1
ticket--;
num++;
// 每次抢完票休息0.5秒
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 阻塞状态结束后,显示出票数据反馈给控制台
System.out.println(Thread.currentThread().getName()+"抢到了第"+num+"张票,剩余"+ticket+"张票");
}
}
}
}
加上synchronized关键字后结果就不会有重复了
小练习——模拟多人接力赛跑
多人参加1000米接力跑,每人跑100米,换下个选手,每跑10米显示信息。
public class RunTest implements Runnable{
private int num = 1000;
// 方法二:
boolean flag = false;
@Override
public void run() {
while (!flag){
// 如果没跑完,就调用跑步的方法
go();
return;
}
}
public synchronized void go(){
num-=100;
if(num<=0){
flag = true;
System.out.println("比赛结束!");
return;
}
System.out.println(Thread.currentThread().getName()+"拿到了接力棒!");
for (int i = 1; i <= 10; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"跑了"+(i*10)+"米");
}
}
//方法一:
/*private int num = 1000;
@Override
public void run() {
while (true){
// 未跑完
synchronized (this){
// 每个选手跑完100米,跑道减少100米
num-=100;
System.out.println(Thread.currentThread().getName()+"拿到接力棒!");
for (int i = 1; i <= 10; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"跑了"+(i*10)+"米!");
}
if(num<=0){
// 跑完了
System.out.println("比赛结束!");
break;// 停止while循环
}
return;// 停止run方法,下一个线程执行run方法又重新开启
}
}
}*/
// 1000米,每个选手跑100米,所以需要10个选手
public static void main(String[] args) {
RunTest runTest = new RunTest();
for (int i = 1; i <= 10; i++) {
Thread runner = new Thread(runTest, i + "号选手");
runner.start();
}
}
}
(二)线程安全的类型
查看ArrayList类的add()方法定义
public boolean add(E e){
ensureCapacityInternal(size+1);
elementData[size++]=e;
return true;
}
ArrayList类的add()方法为非同步方法
当多个线程向同一个ArrayList对象添加数据时,可能出现数据不一致问题。
六、线程综合练习题(I/O流、StringBuffer)
当读取的文件数据量很大时,单线程的效率要比多线程低很多。
文件示例:
单线程读取文件:
package com.atguigu.practices.thread;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Map;
import java.util.TreeMap;
public class WordCountTest {
public static void main(String[] args) {
// 单线程读取大型文件,效率低
long start = System.currentTimeMillis();
// 读取文件用IO流
FileReader fr = null;
BufferedReader bf = null;
Map<String,Integer> wordmap = new TreeMap<>();
// 使用TreeMap是为了遍历出来的单词按照首字母排序
// 因为TreeMap实现了NavigableMap接口,NavigableMap继承了SortedMap,
// SortedMap返回用于对该映射中的[键]进行排序的比较器,如果该映射使用其键的自然排序,则返回null。
try {
fr = new FileReader("./rescources/word.txt");
bf= new BufferedReader(fr);
String tmpStr = "";
int num = 0;// 读取的行数
while ((tmpStr = bf.readLine()) != null){// hello java
// 读取一行,行数+1
num++;
String[] splitStu = tmpStr.trim().split("\\s");// [hello,java,hello,html...]
// 正则表达式 \\s代表空格
// String的trim()方法可用于截去字符串开头和末尾的空白
// 以单词与单词之间的空格来划分单词,添加到String类型的数组中
// 遍历数组
for (int i = 0; i < splitStu.length; i++) {
// 使用map,其中有K,V // 期望的遍历结果:hello,1 java,1 // hello,2 java,1 html,1
// 遍历出来的数组存储到TreeMap中
// 第一步,先判断wordMap中有没有键值K
if(wordmap.containsKey(splitStu[i])){// 原map中包含key,value+1
// 取出数组中的一个值,如果wordMap中包含
// 通过键Key找值Value,找到一个Value就要+1
Integer count = wordmap.get(splitStu[i])+1;
// 找到键值对后再放入wordMap中
wordmap.put(splitStu[i],count);
}else { // 原map中不包含key(单词)
wordmap.put(splitStu[i],1);
}
}
}
// 当读取到最后一行就退出遍历
System.out.println("读取文件的总行数:"+num);
// 通过增强for遍历wordMap
for (String key :
wordmap.keySet()) {// 先拿到key值
// 再通过key拿到value
Integer wordNum = wordmap.get(key);// wordNum相当于Value
System.out.println(key+"出现的次数为:"+wordNum);
}
long totalTime = System.currentTimeMillis()-start;
System.out.println("程序运行总时长为:"+totalTime+"ms");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
bf.close();
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
控制台输出:
多线程读取文件:
实现思路:创建n个线程,每个线程读取100000行数据,因为具体线程数量未知,所以要套在while循环中,再创建线程池管理这些线程,然后再创建一个外部的线程,将n个线程看做一个整体的小集合,作为外部线程的value,外部线程的Key就是每个小线程的名字。
代码实现:
package com.atguigu.practices.thread;
import java.util.HashMap;
import java.util.Map;
public class WordCount implements Runnable{// 这是一个线程
String content = "";
Map<String,Integer> map = new HashMap<>();
public WordCount(String content) {
this.content = content;
}
@Override
public void run() {
String[] splitStr = content.trim().split("\\s");
for (int i = 0; i < splitStr.length; i++) {
if(map.containsKey(splitStr[i])){
Integer count = map.get(splitStr[i])+1;
map.put(splitStr[i],count);
}else {
map.put(splitStr[i],1);
}
}
}
}
package com.atguigu.practices.thread;
import java.io.*;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class WordCountTest2 {
static Map<String, Map<String,Integer>> threadMap = new TreeMap<>();
public static void main(String[] args) {
// 起始时间
long start = System.currentTimeMillis();
// 读取文件
FileReader fr = null;
BufferedReader br = null;
// 将读取出来的内容写入文件
FileWriter fw = null;
BufferedWriter bw = null;
int lineNum = 0;
// 线程池
ExecutorService executorService = Executors.newCachedThreadPool();
try {
fr = new FileReader("./resources/word.txt");
br = new BufferedReader(fr);
fw = new FileWriter("./resources/more.txt");
bw = new BufferedWriter(fw);
StringBuffer listStrBuf = new StringBuffer();
String lineStr = "";
while ((lineStr= br.readLine()) != null){
listStrBuf.append(lineStr+" ");
lineNum++;// 每读取一行就+1
if(lineNum % 100000 == 0){// 每读取100000行就创建一个线程
// 创建线程对象,将字符串传递到WordCount线程中
WordCount wordCount = new WordCount(listStrBuf.toString());
// 传入线程后,listStrBuf清空,重新读取下一个100000行
listStrBuf.delete(0,listStrBuf.length());// 传入起始位置
Thread thread = new Thread(wordCount);
// thread.start();
// 利用线程池管理thread
executorService.execute(thread);// 线程池启动thread
threadMap.put("thread-"+lineNum/100000,wordCount.map);// thread-1 thread-2
/**
* Map<String, Map<String,Integer>> threadMap = new TreeMap<>();
* 上面一行代码的意思是:
* threadMap的K相当于"thread-"+lineNum/100000(每一个线程的名字)
* threadMap的V相当于单独创建的子线程Map<String,Integer>,即wordCount.map
* wordCount.map就是WordCount中的线程:
* Map<String,Integer> map = new HashMap<>();
*/
}
}
// while()循环退出就相当于读取n个100000退出,还有剩余的最后一个线程
// 判断是否有最后一个线程,即%100000!=0
if(listStrBuf.toString().length()>0){
// 创建WordCount子线程对象
WordCount wordCount = new WordCount(listStrBuf.toString());
Thread thread = new Thread(wordCount);
executorService.execute(thread);
threadMap.put("thread-last",wordCount.map);
}
System.out.println("读取文件总行数"+lineNum+"行");
bw.write("读取文件总行数"+lineNum+"行");
bw.newLine();
// 关闭线程池
executorService.shutdown();
while (true){
if(executorService.isTerminated()){// 判断线程池中是否都操作完毕
// 子线程处理完毕
// 设定一个临时的总map来处理上面n个子线程(thread-1~thread-last)
HashMap<String, Integer> mapResult = new HashMap<>();
// for (Map<String, Integer> tmpMap :
// threadMap.values()) {
// System.out.println(tmpMap.keySet());
// }
// 遍历出所有thread的名字,每个thread存储100000行数据
for (String key :
threadMap.keySet()) {// thread-1 thread-2....thread-last
Map<String, Integer> perThreadWordNum = threadMap.get(key);// 得到小map
// 使用迭代器遍历小map
Set<String> perThreadWords = perThreadWordNum.keySet();// 小map中的String——每个单词的数量
Iterator<String> iterator = perThreadWords.iterator();
while (iterator.hasNext()){
String word = iterator.next();
if(mapResult.containsKey(word)){
Integer wordsNum = perThreadWordNum.get(word);// 通过小map中的key得到里面的value
Integer haveNum = mapResult.get(word);
// 临时的mapResult中的value默认是null,
// 小map中的key,在mapResult中,
// 每遍历一次小map中的value就[累加]到临时mapResult中的value
mapResult.put(word,wordsNum+haveNum);
}else {
// 小map中的key,不在mapResult中,就直接放进去
mapResult.put(word,perThreadWordNum.get(word));
}
}
/*System.out.println(key);// 拿到threadMap中所有的key
// 拿到threadMap中String中的Map<String,Integer>中的String
System.out.println(threadMap.get(key).keySet());
// 遍历拿到的所有单词
for (String word :
threadMap.get(key).keySet()) {
// 拿出小Value 即 拿到threadMap中的Map<String,Integer>中的String和Integer
System.out.print(word + ":"+threadMap.get(key).get(word)+" ");
}
System.out.println();*/
}
// static Map<String, Map<String,Integer>> threadMap = new TreeMap<>();
// 线程池操作完毕,遍历后退出while()循环
Set<String> resultWords = mapResult.keySet();
for (String word :
resultWords) {
System.out.println(word+":"+mapResult.get(word));
String content = word + ":"+mapResult.get(word);
bw.write(content);
bw.newLine();// 写入时换行
bw.flush();
}
break;
}
}
long totalTime = System.currentTimeMillis()-start;
System.out.println("文件读取时间:"+totalTime+"ms");
bw.write("文件读取时间:"+totalTime+"ms");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
bw.close();
fw.close();
br.close();
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
控制台输出:
可以将多线程遍历的结果写入到一个文件中: