问题描述
文件描述
问题描述
现存在三个文件来存储手机号码,通过处理将对象封装成以下形式
- 通过描述可以看见上述号码有如下特点:号码很多不同,有部分重复
- 号码通过对象封装有如下要求:
电话号码、电话号码总数、电话号码指定文件出现次数- 文件读取使用字符包装类
- 使用多线程,每扫描一个文件就创建一个线程,在线程的处理时一定要注意并发问题,还要考虑线程运行完毕时的情况。
解题思路
首先此题存在两大问题:如何读取文件内容,如何使用多线程处理
问题一: 使用BufferedReader字符包装类,通过readLine()方法读取每一行数据,在读取数据的注意对数据的处理,在处理号码的时候就需要一个Map来将所有的手机号进行记录key用来存放手机号码,而value用来存储手机号码对象。
由于手机号通过对象封装起来了,要注意每一个属性的创建,比如手机号号码用String修饰,总数用Integer修饰,手机号在每一文件出现的次数用Map修饰key为文件名,value为出现次数,
在读取文件的时候,有如下几种情况
如果读取文件时发现当前手机号在总的Map中没有出现,那么就new一个手机号对象,而且添加到总的Map中,如果出现的话就将出现的总次数加1
然后想要记录每个文件中手机号出现的次数就需要在处理方法中加一个局部变量Map,Map的key用来存储手机号,value用来存储在此文件中出现的次数,每次读取文件就将手机号记录下来
当手机号遍历完成之后再来将每个文件中出现的次数通过forEach方式添加进入。
问题二:多线程问题的关键就是对共享数据的处理(多个线程同时操作共同的数据),这里的共享数据主要有两个方面,一个是手机号出现的总数,二是对总的手机号Map的put操作,
解决方式:一,就需要加锁,但是加锁的同时要注意锁的力度,如果用字节码锁,那就把所有的手机号码都限制住的,大大的减小的程序运行的效率,我们现在只是需要关心对同一手机号的操作才需要用synchronized,那么用什么来锁呢?
其实仔细一想就能发现字符串就是一个不错的锁,由于字符串的不可变性,能够使它很好的充当这个角色,手机号码就是一个字符串锁的对象,
那么现在又会出现一个问题就是如果相同的号码里面有空格怎么办,如果通过trim()方法去掉空格又会导致相同手机号所指向的地址不一样,这样就会导致同一手机号锁不住,这个时候就需要intern()方法来对这种情况进行处理。
二:HashMap的put操作会有线程安全问题,比如同时加入同一手机号码的时候会覆盖桶位,所以这边需要用ConcurrentHashMap线程安全版本。
代码实例
PhoneInfo 手机号码对象
package com.qianfeng.workone.multithreadingWork02;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
// 每个电话号码使用 PhoneInfo 对象
public class PhoneInfo {
// 电话号码
private String phoneNum;
// 出现的次数
private Integer num;
// setter getter
// 手机号在对应文件中出现的次数
private Map<String, Integer> fileToTimes = new ConcurrentHashMap<>();
public String getPhoneNum() {
return phoneNum;
}
public void setPhoneNum(String phoneNum) {
this.phoneNum = phoneNum;
}
public Integer getNum() {
return num;
}
public void setNum(Integer num) {
this.num = num;
}
public Map<String, Integer> getFileToTimes() {
return fileToTimes;
}
public void setFileToTimes(Map<String, Integer> fileToTimes) {
this.fileToTimes = fileToTimes;
}
}
PhoneInfoParser 手机号码的读取与处理
package com.qianfeng.workone.multithreadingWork02;
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
public class PhoneInfoParser {
// 文件的路径
private String path;
//计数器
private CountDownLatch cdl;
//定义一个map集合来储存电话号码。key是电话号码名称value是电话号码对象
private Map<String, PhoneInfo> phoneInfoMap = new ConcurrentHashMap<>();
//定义一个成员线程类
private class MyThread extends Thread{
//每一个线程处理一个文件、
private File file;
//构造方法初始化文件
public MyThread(File file) {
this.file = file;
}
@Override
public void run() {
//记录每个文件中手机号出现的次数
Map<String, Integer> filePhoneNum = new ConcurrentHashMap<>();
BufferedReader br = null;
try {
//通过字符包装流来读取文件
br = new BufferedReader(new InputStreamReader(new FileInputStream(this.file), "utf-8"));
//通过变量line来记录每一行的手机号码
String line = null;
//反复读取手机号的每一行
while ((line = br.readLine()) != null){
//让相同的手机号拥有相同的地址信息
line = line.trim().intern();
//从Map中读取手机号,如果没有读取到就创建手机号对象
PhoneInfo phoneInfo = phoneInfoMap.get(line);
if (phoneInfo == null){
//通过line锁住对共享数据的操作,字符串具有不可变性
synchronized(line){
//在单例模式中经常出现的双空判断,能够确保线程安全
//原因是可能两个线程都通过第一个空值判断,那么当其中一个new值成功后面的直接运行的话会在new一次
phoneInfo=phoneInfoMap.get(line);
if (phoneInfo == null){
PhoneInfo fi = new PhoneInfo();
fi.setPhoneNum(line);
fi.setNum(1);
phoneInfoMap.put(line,fi);
} else {
synchronized (line){
phoneInfo.setNum(phoneInfo.getNum() + 1);
}
}
}
} else {
synchronized (line){
phoneInfo.setNum(phoneInfo.getNum() + 1);
}
}
//在while循环中将line添加到局部变量的map中,用于记录每一文件中对应号码出现的次数
Integer integer = filePhoneNum.get(line);
if (integer == null){
filePhoneNum.put(line,1);
} else {
filePhoneNum.put(line,filePhoneNum.get(line) + 1);
}
}
//当while执行完毕之后,需要对fileToTimes成员进行添加
Set<String> phoneSet = filePhoneNum.keySet();
for (String phoneNum : phoneSet) {
phoneInfoMap.get(phoneNum).getFileToTimes().put(file.getName(),filePhoneNum.get(phoneNum));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//每一次线程执行完毕都对计数器相减一次
cdl.countDown();
}
}
}
public PhoneInfoParser(String path) {
this.path = path;
this.operationPath();
}
//对传入的path进行操作
public void operationPath(){
//首先获得path的文件对象
File file = new File(this.path);
//对文件对象进行判断
if(file == null || file.isFile()){
try {
throw new Exception(this.path + "输入路径有误");
} catch (Exception e) {
e.printStackTrace();
}
} else {
int count = 0;
File[] files = file.listFiles();
for (File f : files) {
//只对txt文件进行分析,避免读取错误
if (f.getName().endsWith(".txt")){
count++;
MyThread myThread = new MyThread(f);
myThread.start();
}
}
//只对开启的线程进行计数
this.cdl = new CountDownLatch(count);
}
}
// setter and getter
public List<PhoneInfo> getPhoneInfos() {
// TODO
try {
//只有当所有的线程执行完毕才会对List进行赋值操作
this.cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
List<PhoneInfo> list = new ArrayList<>();
//通过addAll进行List的复制操作
list.addAll(phoneInfoMap.values());
return list;
}
}
PhoneNumTest 结果的测试
package com.qianfeng.workone.multithreadingWork02;
import java.util.List;
public class PhoneNumTest {
public static void main(String[] args) {
PhoneInfoParser pip = new PhoneInfoParser("D:/千峰资料/作业/day_9_25/多线程练习题");
List<PhoneInfo> infos = pip.getPhoneInfos();
for(PhoneInfo phoneInfo : infos) {
System.out.println("手机号为:" + phoneInfo.getPhoneNum() + ",总的出现次数为" + phoneInfo.getNum() +
",出现的地方和次数");
phoneInfo.getFileToTimes().forEach((k,v)->{
System.out.println(k + "\t" + v);
});
}
}
}