业务需求:将数据库表中的大数据以文本方式保存到本地磁盘,即通过线程写入文件。
业务实现:
- 主线程开启创建文件缓冲流,启动多条子线程,并将文件缓冲流提供给每个子线程
- 每个子线程调用DAO分页查询接口获取到的数据,组装拼接写入到文件缓冲流中
在这个简单的业务里面最需要注意的应该是每个子线程分页查询时的页码数,需要通过同步的方式来控制。
一、同步锁(synchronized)的方式
同步页码类:
/** * 同步对象,提供页码 */ public class SyncObj { private int pageNo = 0; public synchronized int getPageNo() { pageNo ++; return pageNo; } }
子线程类:
public class WriteFileThread implements Runnable { protected final Log log = LogFactory.getLog(this.getClass()); private String name; private ItemMapper itemMapper; private SyncObj obj; private BufferedWriter bufferwriter = null; public WriteFileThread(String name, ItemMapper itemMapper, SyncObj obj, BufferedWriter bufferwriter){ this.name = name; this.itemMapper = itemMapper; this.obj = obj; this.bufferwriter = bufferwriter; } @Override public void run() { int pageNoCopy = 0; List<Item> itemList = null; StringBuilder sb = new StringBuilder(); try { Map<String, Object> param = new HashMap<String, Object>(); while(true){ pageNoCopy = obj.getPageNo(); log.info("线程["+name+"]获取到的当前页码为:"+pageNoCopy); param.put("index", (pageNoCopy-1)*10000); param.put("pageSize", 10000);//一万条读一次 itemList = itemMapper.queryPagination(param); if(itemList == null || itemList.size() == 0){ log.info("线程["+name+"]在第"+pageNoCopy+"页退出了"); break; } for(Item item : itemList){ sb.append(item.getItemNum()).append(item.getItemName()).append("\n"); bufferwriter.write(sb.toString()); sb.delete(0, sb.length()); } bufferwriter.flush();//刷新流 } } catch (IOException e) { e.printStackTrace(); } } }
主线程(这里采用Http Request)代码:
@RequestMapping(value="/generate_file") public void generateFile(){ File file = new File("D://"+System.currentTimeMillis()+".txt"); if(!file.exists()){ try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } SyncObj obj = new SyncObj(); ExecutorService pool = Executors.newFixedThreadPool(20); FileWriter filewriter = null; BufferedWriter bufferwriter = null; try { filewriter = new FileWriter(file, true); bufferwriter = new BufferedWriter(filewriter); for(int i=0; i<20; i++) pool.execute(new WriteFileThread("线程"+(i+1), itemMapper, obj, bufferwriter)); Thread.sleep(1000*60); } catch (Exception e) { e.printStackTrace(); }finally{ try { bufferwriter.close(); filewriter.close(); } catch (Exception e) { e.printStackTrace(); } } }
二、原子类(AtomicXXX)及同步量计数器(CountDownLatch)的应用
同步页码类:无
这里使用了JDK自带的原子类,能够更高效的提供数据同步,所以同步页码类就不需要了。
子线程类:
public class WriteFileThread2 implements Runnable { protected final Log log = LogFactory.getLog(this.getClass()); private String name; private ItemMapper itemMapper; private AtomicInteger pageNo; private CountDownLatch countDown; private BufferedWriter bufferwriter = null; public WriteFileThread2(String name, ItemMapper itemMapper, AtomicInteger pageNo, CountDownLatch countDown, BufferedWriter bufferwriter){ this.name = name; this.itemMapper = itemMapper; this.pageNo = pageNo; this.bufferwriter = bufferwriter; this.countDown = countDown; } @Override public void run() { int pageNoCopy = 0; List<Item> itemList = null; StringBuilder sb = new StringBuilder(); try { Map<String, Object> param = new HashMap<String, Object>(); while(true){ pageNoCopy = pageNo.getAndIncrement();//原子量中获取页码,并且自增1 log.error("线程["+name+"]获取到的当前页码为:"+pageNoCopy); param.put("index", (pageNoCopy-1)*10000); param.put("pageSize", 10000);//每页获取一万条记录 itemList = itemMapper.queryPagination(param); if(itemList == null || itemList.size() == 0){ log.info("线程["+name+"]在第"+pageNoCopy+"页退出了"); break; } for(Item item : itemList){ sb.append(item.getItemNum()).append(item.getItemName()).append("\n"); bufferwriter.write(sb.toString()); sb.delete(0, sb.length()); } bufferwriter.flush();//刷新流 } countDown.countDown();//计数器自减1 } catch (IOException e) { countDown.countDown();//计数器自减1 e.printStackTrace(); } } }
主线程代码:
@RequestMapping(value="/generate_file2") public void generateFile2(){ final int threadNum = 20;//子线程数 File file = new File("D://"+System.currentTimeMillis()+".txt"); if(!file.exists()){ try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } AtomicInteger pageNo = new AtomicInteger(1);//页码 CountDownLatch countDown = new CountDownLatch(threadNum);//计数器 ExecutorService pool = Executors.newFixedThreadPool(threadNum);//固定大小线程池 FileWriter filewriter = null; BufferedWriter bufferwriter = null; try { filewriter = new FileWriter(file, true); bufferwriter = new BufferedWriter(filewriter); for(int i=0; i<threadNum; i++) pool.execute(new WriteFileThread2("线程"+(i+1), itemMapper, pageNo, countDown, bufferwriter)); countDown.await();//阻塞主线程 } catch (Exception e) { e.printStackTrace(); }finally{ try { bufferwriter.close(); filewriter.close(); } catch (Exception e) { e.printStackTrace(); } } }
注意:
- 每个子线程必须公用主线程中的文件缓冲流;若子线程各自使用自己的文件缓冲流,在线程刷出缓冲流数据时出现了碰撞,会导致写入的数据内容窜了。
- 在我的程序中用了C3P0的数据源,需要注意设置最大可用连接数(maxPoolSize)及获取连接时等待超时时间(checkoutTimeout)以确保请求时不会出现超时异常。
- 因主线程感知不到子线程出现异常的情况,所以子线程出现异常时也需要减计数器,否则主线程会一直被阻塞。
按照正常的业务,当子线程中出现异常时,首先需要进行自我修复(例如:出现数据库连接异常时,可重新获取连接,重新进行查询操作);
若修复不成功(例如:数据本身存在问题),需要立即通知主线程,并且终止掉其他子线程。在这里可以简单实现成:在主线程中增加原子布尔值(AtomicBoolean)作为是否异常的状态标志位,每个子线程在循环时进行检查;若出现异常,计数器减一并跳出当前线程即可。