12 March 2016

jdk1.5之后自带了线程池功能,妈妈再也不用担心线程使用中遇到的各种坑了。

先来一个简单的例子来体验下线程池:

  • 比如有个需求,我需要读取一个目录下的文件,将所有的文件进行解析。那么如果依次读取文件肯定很慢,所以用多线程的方式来处理:

  • 代码:

      public class ThreadTest {
          private Logger logger = LoggerFactory.getLogger(ThreadTest.class);
          public static  void main(String[] args){
              new ThreadTest().test();
          }
          public void test() {
              String dirName = "/home/public/test/";
              File dir = new File(dirName);
              File[] files = dir.listFiles(new FileFilter() {
                  @Override
                  public boolean accept(File file) {
                      if(file.getName().endsWith(".txt")){
                          return true;
                      }
        
                      return false;
                  }
              });
        
              ExecutorService executor = Executors.newFixedThreadPool(5);
              for(final File file:files){
                  executor.execute(new Runnable() {
                      @Override
                      public void run() {
                          try {
                              String fileName= file.getName();
                              List<String> lines = Files.readLines(file, Charset.defaultCharset());
                              for(int i=0;i<lines.size();i++) {
                                  logger.info("读取文件:  {}  的第  {}  行的内容为:{}" , fileName, i, lines.get(i));
                              }
                          } catch (IOException e) {
                              e.printStackTrace();
                          }
                      }
                  });
              }
              executor.shutdown();
          }
      }
    

java多线程组成

  • jdk1.5以后提供了很方面的原生的线程池用法。顶级的线程池接口是Executor。
  • 先来看下线程池中几个类的继承关系: threadpool
  • 然后jdk还提供了一个Executors类,该类可以很方面的创建线程池的方法。常见的创建线程池方法有以下几种:
    • newCachedThreadPool
      • 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
      • 对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。
      • 调用 execute 将重用以前构造的线程(如果线程可用)。
      • 如果现有线程没有可用的,则创建一个新线程并添加到池中。
      • 线程默认有60秒的缓存期,如果60秒内未被使用,就会移除这个线程。
      • 长时间保持空闲的线程池不会使用任何资源。因为都被移除了嘛
    • newFixedThreadPool
      • newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程
      • 其独特之处:任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子
      • 和cacheThreadPool不同,FixedThreadPool没有IDLE机制(可能也有,但既然文档没提,肯定非常长,类似依赖上层的TCP或UDP IDLE机制之类的),所以FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器
      • 从方法的源代码看,cache池和fixed 池调用的是同一个底层池,只不过参数不同:fixed池线程数固定,并且是0秒IDLE(无IDLE);cache池线程数支持0至Integer.MAX_VALUE(显然完全没考虑主机的资源承受能力),60秒IDLE
    • newScheduledThreadPool
      • 调度型线程池
      • 这个池子里的线程可以按schedule依次delay执行,或周期执行
    • newSingleThreadExecutor -单例线程,任意时间池中只能有一个线程。 -用的是和cache池和fixed池相同的底层池,但线程数目是1,0秒IDLE(无IDLE)

使用注意事项:

  • 线程池本身用法很简单,但是如果想用好,需要考虑的东西很多。比如每个任务执行结果如何。线程池有没有正常关闭等。比如仍然是上面的例子,还有完善的余地:

      public class ThreadTest {
          private Logger logger = LoggerFactory.getLogger(ThreadTest.class);
          public static  void main(String[] args){
              new ThreadTest().test();
          }
          public void test() {
              String dirName = "/home/public/test/";
              File dir = new File(dirName);
              File[] files = dir.listFiles(new FileFilter() {
                  @Override
                  public boolean accept(File file) {
                      if(file.getName().endsWith(".txt")){
                          return true;
                      }
        
                      return false;
                  }
              });
        
              ExecutorService executor = Executors.newFixedThreadPool(5);
              final List<String> badFiles = new ArrayList<String>();
              for(final File file:files){
                  executor.execute(new Runnable() {
                      @Override
                      public void run() {
                          try {
                              String fileName= file.getName();
                              List<String> lines = Files.readLines(file, Charset.defaultCharset());
                              for(int i=0;i<lines.size();i++) {
                                  logger.info("读取文件:  {}  的第  {}  行的内容为:{}" , fileName, i, lines.get(i));
                              }
                          } catch (IOException e) {
                             logger.error("处理文件:{}失败,",file.getName(),e);
                              badFiles.add(file.getName());
                          }
                      }
                  });
              }
              executor.shutdown();
              try {
                  executor.awaitTermination(1, TimeUnit.DAYS);
              } catch (InterruptedException e) {
                  logger.error("等待线程池结束时被中断", e);
              }
              if (!badFiles.isEmpty()) {
                  logger.info("处理数据时有错误出现,详情请检查日志. 出错的文件如下: {}",
                          Joiner.on("\n").join(badFiles));
              }
          }
      }
    
  • 例子中代码使用了google的guava库。如果你不知到这个库。那么…刚快试试吧,做java开发不用这个库,简直是浪费了。
  • 下次分享一些日常开发常用的库。




Fork me on GitHub