Java线程和线程池 Java中的多线程你只要看这一篇就够了 Java并发编程 - Executor,Executors,ExecutorService, CompletionServie,Future,Callable

一、Java线程

  几个概念:

  进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。

  线程: 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。 

  多线程:在一个进程中创建多个线程,用多线程只有一个目的,那就是更好的利用cpu的资源,如磁盘IO,网络,计算等。

  并行与并发:

    •   并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时,如Hadoop中MapReduce的并行计算等;
    •   并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时,而是cpo对于多个线程进行cpu资源的调度;

  线程的创建方式

new Thread(new Runnable() {

    @Override
    public void run() {
        // TODO Auto-generated method stub
    }
}).start();

  线程的状态

    线程在Running的过程中可能会遇到阻塞(Blocked)情况

    1. 调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
    2. 调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)
    3. 对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。

    此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。

  线程安全:多线程在并发情况下对于共用的属性进行操作时,会出现线程不安全现象,因此,我们需要对操作公共属性的代码或者方法进行同步锁,以保证属性的线程安全。通常,我们使用synchronized关键字来实现同步锁:

  synchronized对于代码块的使用:

public class Thread1 implements Runnable {
   Object lock;
   public void run() {  
       synchronized(lock){
         ..do something
       }
   }
}

  synchronized对于方法的使用:

public class Thread1 implements Runnable {
   public synchronized void run() {  
        ..do something
   }
}

  使用wait和notify来实现:

/**
   * 生产者生产出来的产品交给店员
   */
  public synchronized void produce()
  {
      if(this.product >= MAX_PRODUCT)
      {
          try
          {
              wait();  
              System.out.println("产品已满,请稍候再生产");
          }
          catch(InterruptedException e)
          {
              e.printStackTrace();
          }
          return;
      }

      this.product++;
      System.out.println("生产者生产第" + this.product + "个产品.");
      notifyAll();   //通知等待区的消费者可以取出产品了
  }

  /**
   * 消费者从店员取产品
   */
  public synchronized void consume()
  {
      if(this.product <= MIN_PRODUCT)
      {
          try 
          {
              wait(); 
              System.out.println("缺货,稍候再取");
          } 
          catch (InterruptedException e) 
          {
              e.printStackTrace();
          }
          return;
      }

      System.out.println("消费者取走了第" + this.product + "个产品.");
      this.product--;
      notifyAll();   //通知等待去的生产者可以生产产品了
  }

  如果创建多线程应用,每次都new Thread对象的话,会有很多的弊端,如:

  a. 每次new Thread新建对象性能差。
  b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
  c. 缺乏更多功能,如定时执行、定期执行、线程中断。

  因此,java提供线程池的方式,用于创建线程任务,并对线程任务进行管理,如获取执行状态,销毁线程等等。

线程管理类

  Executor(*接口)

    并发编程的一种编程方式是把任务拆分为一系列的小任务,即Runnable,然后将这些任务提交给一个Executor执行,Executor.execute(Runnalbe) 。Executor在执行时使用其内部的线程池来完成操作。execute没有返回值。

  Executor的子接口有:

    ExecutorService、ScheduledExecutorService

  Executor的已知实现类:

    AbstractExecutorService、ScheduledThreadPoolExecutor、ThreadPoolExecutor。

  ExecutorService(接口)

    ExecutorService接口继承了Executor接口,是Executor的扩展子接口;

    ExecutorService中的submit接收的实现Runable接口的对象或者callable接口的对象;

    Executor中的execute方法没有返回值,而submit方法有future返回值;

    future的使用:

      1.如果任务已经执行完成,就可以通过 Future.get() 方法获得执行结果。需要注意的是,Future.get() 方法是一个阻塞式的方法,如果调用时任务还没有完成,会等待直到任务执行结束。

      2.可以通过Future.cancel()取消pending中的任务;

      3.当调用 shutDown 方法时,线程池会停止接受新的任务,但会完成正在 pending 中的任务;

   

  Executors(类)

    Executors 是一个工具类,类似于 Collections。提供工厂方法来创建不同类型的线程池,如:

      newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
      newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
      newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
      newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

  CompletionService(类)

    使用ExecutorService类的时候,我们常维护一个list保存submit的callable task所返回的Future对象。然后在主线程中遍历这个list并调用Future的get()方法取到Task的返回值。

        其实除了使用ExecutorService外,还可通过CompletionService包装ExecutorService,然后调用其take()方法去取Future对象。

          区别:CompletionService和ExecutorService的主要的区别在于submit的task不一定是按照加入自己维护的list顺序完成的。

      ExecutorService中从list中遍历的每个Future对象并不一定处于完成状态,这时调用get()方法就会被阻塞住,如果系统是设计成每个线程完成后就能根据其结果继续做后面的事,这样对于处于list后面的但是先完成的线程就会增加了额外的等待时间。

            而CompletionService的实现是维护一个保存Future对象的BlockingQueue。只有当这个Future对象状态是结束的时候,才会加入到这个Queue中,take()方法其实就是Producer-Consumer中的Consumer。它会从Queue中取出Future对象,如果Queue是空的,就会阻塞在那里,直到有完成的Future对象加入到Queue中。所以,先完成的必定先被取出。这样就减少了不必要的等待时间。

  总结:异步计算的线程按照职责分为3类:

    1. 异步计算的发起线程(控制线程):负责异步计算任务的分解和发起,把分解好的任务交给异步计算的work线程去执行,发起异步计算后,发起线程可以获得Futrue的集合,从而可以跟踪异步计算结果

        2. 异步计算work线程:负责具体的计算任务

        3. 异步计算结果收集线程:从发起线程那里获得Future的集合,并负责监控Future的状态,根据Future的状态来处理异步计算的结果。

参考文章:

Java并发编程 - Executor,Executors,ExecutorService, CompletionServie,Future,Callable