JAVA并发体系-1-线程和任务

Java使用线程来执行任务。任务即我们要并发实现的事情,任务可以用Runnable、Callable来描述,任务也体现在Thread中的Run方法上,任务也可以描述为线程执行体;线程只是任务的载体,只是任务的执行单元。

任务和驱动他的线程是不一样的,体现在java上是你对Thread类实际上没有任何控制权,java的线程机制来源于c的低级的p线程方法,在物理上,创建线程可能会代价高昂,因此必须保存并管理他们,这样,从实现的角度看,将任务从线程中分离出来是很有意义的,因此这样不同的runnable就可以在不同的线程实例中执行。

明白这一点可以帮助我们更好的进行线程资源的复用和理解线程池。

任务描述

任务可以有Runnable、Callable来描述。Runnable、Callable的主要区别在于,Callable有返回值;此外Runnable任务体为run方法,Callable任务体为call方法。

具体Runnable、Callable的描述请见下面叙述

Runnable

todo: code: remove here

public class LiftOff implements Runnable{
    protected int countDown = 10;//default
    private static int taskCount = 0;
    private final int id=taskCount++;//id 可以区分多个对象实例(注意final和上句static)
    public LiftOff(){}
    public LiftOff(int countDown){
        this.countDown=countDown;
    }
    public String status(){
        return "#"+id+"("+ (countDown>0?countDown:"LiftOff!")+").";
    }

    @Override
    public void run() {
        while(countDown-- >0){
            System.out.println(status());
            Thread.yield();//yield是对线程调度器的建议
        }
    }
}
  • 注意这个id,如果单一线程创建了所有的LiftOff线程,那么id是唯一的;如果多个线程在创建LiftOff线程,那么就有可能有多个LiftOff拥有相同的id。即static变量是线程不安全的!(本例taskCount)

Callable

todo: code: remove here

class TaskWithResult implements Callable<String> {
    private int id;

    public TaskWithResult(int id) {
        this.id = id;
    }

    @Override
    public String call() {
        return "result of TaskWithResult "+ id;
    }
}
public static void main(String[] args) {
    ExecutorService exec= Executors.newCachedThreadPool();
    ArrayList<Future<String>> results =  new ArrayList<Future<String>>(); // Future
    for(int i=0;i<1;i++)
        results.add(exec.submit(new TaskWithResult(i))); // submit
    for(Future<String> fs: results){
        try {
            System.out.println(fs.get());// get
        }catch (InterruptedException e){
            System.out.println(e);
            return;
        }catch (ExecutionException e){
            System.out.println(e);
        }finally {
            exec.shutdown();
        }
    }
}

Callable

  1. callable可以在完成时返回一个值,可以接收一个泛型类型,用来指定期望的返回类型
  2. 必须使用ExecutorService.submit()来调用callable,submit()方法将产生Future对象
  3. call()方法可以有返回值,可以声明抛出异常
  4. Callable接口有泛型限制,Callable接口里的泛型形参类型与call()方法返回值类型相同

Future接口

Future接口代表Callable接口里call()方法的返回值,JAVA为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口和Runnable接口(FutureTask实现了RunnableFuture,RunnableFuture继承自Future和Runnable,没错是多继承,但是是接口多继承不是类的多继承),可以作为Thread类的执行体,即FutureTask可以传入Thread。

下面陈述接口中的方法及其使用注意事项:

  1. isDone()方法来查询 Future是否已经完成。
  2. 调用get()方法来获取该结果
    1. 如果 isDone()判断完成,调用get()方法获取到该结果
    2. 如果 isDone()判断没有完成,get()将阻塞,直至结果准备就绪
    3. 策略:在试图调用get()来获取结果之前,先调用具有超时的get(long timeout, TimeUnit unit),或者调用 isDone()来查看任务是否完成
  3. cancel(boolean maylnterruptltRunning):试图取消该Future里关联的Callable任务
  4. 调用时刻的影响
    1. 如果在任务启动之前调用,该任务永远不会执行
    2. 如果任务已经开始mayInterruptIfRunning将确定是否应中断执行该任务的线程以尝试停止该任务。
    3. 如果任务已经完成,尝试将失败,返回false
  5. 因为其他原因也会无法取消任务,返回false
  6. 方法返回后对其他方法影响
    1. 此方法返回后,对isDone()的调用始终返回true
    2. 如果此方法返回true,随后调用的isCancelled()将始终返回true
  7. isCancelled():如果在Callable任务正常完成前被取消,则返回true

线程

对于线程之间共享受限的资源的陈述,请查看之后的锁机制文章。

线程创建方式

实际上线程创建方式可以分为 常规构建线程池创建,本节着重介绍常规创建方法和技巧,线程池创建请见之后的小节。

  1. new Thread()并传入Runnable
Thread t=new Thread(new LiftOFF());
t.start();
  • start()方法会迅速的返回,并不会堵塞调用。
  • 异常不能跨线程传播,必须在本地处理所有在任务内部产生的异常
  • 另外也可以传入:FutureTask(实现了Runnable)
  1. 继承Thread重写run方法

todo: code: remove here

public class SimpleThread extends Thread {  // extends Thread
  private int countDown = 5;
  private static int threadCount = 0;
  public SimpleThread() {
    // Store the thread name:
    super(Integer.toString(++threadCount));
    start(); // start
  }
  public String toString() {
    return "#" + getName() + "(" + countDown + "), ";
  }
  public void run() { // run
    while(true) {
      System.out.print(this);
      if(--countDown == 0)
        return;
    }
  }
  public static void main(String[] args) {
    for(int i = 0; i < 5; i++)
      new SimpleThread();
  }
} /* Output:
#1(5), #1(4), #1(3), #1(2), #1(1), #2(5), #2(4), #2(3), #2(2), #2(1), #3(5), #3(4), #3(3), #3(2), #3(1), #4(5), #4(4), #4(3), #4(2), #4(1), #5(5), #5(4), #5(3), #5(2), #5(1),
*///:~
  1. 自管理的Runnable,即在Runnable内部持有Thread变量

这个写法和直接继承thread没有什么差异,但是实现接口可以是我们继承另一个不同的类,而从Thread继承将不行。

todo: code: remove here

public class SelfManaged implements Runnable {
  private int countDown = 5;
  private Thread t = new Thread(this); // new thread
  public SelfManaged() { t.start(); } // start
  public String toString() {
    return Thread.currentThread().getName() +
      "(" + countDown + "), ";
  }
  public void run() { // run
    while(true) {
      System.out.print(this);
      if(--countDown == 0)
        return;
    }
  }
  public static void main(String[] args) {
    for(int i = 0; i < 5; i++)
      new SelfManaged();
  }
} /* Output:
Thread-0(5), Thread-0(4), Thread-0(3), Thread-0(2), Thread-0(1), Thread-1(5), Thread-1(4), Thread-1(3), Thread-1(2), Thread-1(1), Thread-2(5), Thread-2(4), Thread-2(3), Thread-2(2), Thread-2(1), Thread-3(5), Thread-3(4), Thread-3(3), Thread-3(2), Thread-3(1), Thread-4(5), Thread-4(4), Thread-4(3), Thread-4(2), Thread-4(1),
*///:~

注意:2和3中start都是在构造器中调用的,在构造器启动线程可能会变得很有问题,因为另一个任务可能会在构造器结束之前开始执行,这意味着该任务能够访问处于不稳定状态的对象(todo: 这句话来自编程思想,但是现在没有体会到???),这是应该优选Executor而不是显示创建thread对象的另一个原因

  1. 使用内部类将线程代码隐藏在类中

todo: code: remove here

对于这一类写法,也应当警惕在构造器内部就执行任务体的情况。

// Using a named inner class:
class InnerThread1 {
  private int countDown = 5;
  private Inner inner;
  private class Inner extends Thread { // Inner extends Thread
    Inner(String name) {
      super(name);
      start(); // 构造器中进行start
    }
    public void run() {
      try {
        while(true) {
          print(this);
          if(--countDown == 0) return;
          sleep(10);
        }
      } catch(InterruptedException e) {
        print("interrupted");
      }
    }
    public String toString() {
      return getName() + ": " + countDown;
    }
  }
  public InnerThread1(String name) {
    inner = new Inner(name); // 构造内部类
  }
}

// Using an anonymous inner class:
class InnerThread2 {
  private int countDown = 5;
  private Thread t;
  public InnerThread2(String name) {
    t = new Thread(name) { // InnerThread2构造器中new 了一个thread
      public void run() {
        try {
          while(true) {
            print(this);
            if(--countDown == 0) return;
            sleep(10);
          }
        } catch(InterruptedException e) {
          print("sleep() interrupted");
        }
      }
      public String toString() {
        return getName() + ": " + countDown;
      }
    };
    t.start(); // 构造器中进行start
  }
}

// Using a named Runnable implementation:
class InnerRunnable1 {
  private int countDown = 5;
  private Inner inner;
  private class Inner implements Runnable {
    Thread t;
    Inner(String name) {
      t = new Thread(this, name);
      t.start();
    }
    public void run() {
      try {
        while(true) {
          print(this);
          if(--countDown == 0) return;
          TimeUnit.MILLISECONDS.sleep(10);
        }
      } catch(InterruptedException e) {
        print("sleep() interrupted");
      }
    }
    public String toString() {
      return t.getName() + ": " + countDown;
    }
  }
  public InnerRunnable1(String name) {
    inner = new Inner(name);
  }
}

// Using an anonymous Runnable implementation:
class InnerRunnable2 {
  private int countDown = 5;
  private Thread t;
  public InnerRunnable2(String name) {
    t = new Thread(new Runnable() {
      public void run() {
        try {
          while(true) {
            print(this);
            if(--countDown == 0) return;
            TimeUnit.MILLISECONDS.sleep(10);
          }
        } catch(InterruptedException e) {
          print("sleep() interrupted");
        }
      }
      public String toString() {
        return Thread.currentThread().getName() +
          ": " + countDown;
      }
    }, name);
    t.start();
  }
}

线程生命周期

(参考请见文末参考1)

线程的状态切换(采用五状态线程模型来讨论):

  1. 新建状态:当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值

  2. 就绪状态:当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行

  3. 运行状态:如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态

  4. 阻塞状态:当处于运行状态的线程失去所占用资源之后,便进入阻塞状态(执行)

todo: 应当添加一个调用new进入新建态的箭头

todo: 应当综合上下这两张图


线程新建和就绪状态注意事项

  • 希望调用线程的start()方法后立即开始执行子线程,程序可以使用Thread.sleep(1) 来让当前运行的线程(主线程)睡眠1毫秒,1毫秒就够了,因为在这1毫秒内CPU不会空闲,它会去执行另一个处于就绪状态的线程,这样就可以让子线程立即开始执行。
  • run方法和start方法:启动线程使用start()方法,而不是run()方法。永远不要调用线程对象的run()方法。调用start0方法来启动线程,系统会把该run()方法当成线程执行体来处理;但如果直按调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。也就是说,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。需要指出的是,调用了线程的run()方法之后,该线程已经不再处于新建状态,不要再次调用线程对象的start()方法。只能对处于新建状态的线程调用start()方法,否则将引发IllegaIThreadStateException异常。

线程进入堵塞状态的情况

  1. 线程调用sleep()方法主动放弃所占用的处理器资源,使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行。(解除堵塞:sleep()方法经过了指定时间)
  2. 线程调用了一个阻塞式IO方法,在等待某个输入输出完成,在该方法返回之前,该线程被阻塞(解除堵塞:阻塞式IO方法已经返回)
  3. 任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁。即线程试图获得一个同步监视器(锁),但该同步监视器正被其他线程所持有。关于同步监视器(锁)的知识、后面将进行更深入的介绍(解除堵塞:成功地获得了试图取得的同步监视器)
  4. 通过调用 wait()使线程挂起,线程在等待某个通知notify或者signal)(解除堵塞:其他线程发出了等待的某个通知)
  5. 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。(解除堵塞:调用resdme()恢复方法)

线程终止注意事项

  • 线程抛出一个未捕获的Exception或Error
  • 直接调用该线程stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用

其他生命周期注意事项

  • yield语句(即Thread.yield()):表明该线程已经完成生命周期中最重要的一部分,此刻正是切换给其他任务执行一段时间的大好时机。这完全是选择性的,可能发生也可能不发生
  • 就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度决定。
  • 为了测试某个线程是否已经死亡,可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞三种状态时,该方法将返回true;当线程处于新建、死亡状态时,该方法将返回false
  • 不要试图对一个已经死亡的线程调用start()方法使它重新启动,死亡就是死亡,该线程将不可再次作为线程执行。将引发IllegaIThreadStateException异常
  • sleep语句:

线程常见处理

优先级

可以用getPriority()setPriority()来读取和设置线程优先级,(优先级是在run()的开头部分设定的,在构造器内设置没有任何好处因为还没有开始执行任务。todo: ???尚未体会到这一点)。

为了让线程优先级具有可移植性,当调整优先级的时候,只使用MAX_PRIORITY、NORM_PRIORITY和MIN_PRIORITY三种级别。

yield让步

使用yield是在建议调度器可以让别的线程使用CPU,但并没有任何机制保证他会被采纳,yield也是在建议具有相同优先级的其他线程可以运行,对于任何重要的控制或在调整应用是,都不能依赖于yield。

yield语句(即Thread.yield()):表明该线程已经完成生命周期中最重要的一部分,此刻正是切换给其他任务执行一段时间的大好时机。这完全是选择性的,可能发生也可能不发生

后台线程

  • main是一个非后台线程。

  • 必须在线程启动之前调用setDaemon(boolean on)方法才能把它设置成为后台进程。示例如下:

    Thread daemon = new Thread(new Runnable() {...});
    daemon.setDaemon(true);
    daemon.start();
    
  • 后台线程创建的任何线程都自动设置为后台线程。

  • 后台线程在所有其他非后台线程结束后就会终止,可能不会执行finally语句。

针对最后一点不会执行finally语句的说明:

todo: code: remove here

public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10; i++) {
        Thread daemon = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true){
                        TimeUnit.MILLISECONDS.sleep(100);
                        System.out.println(Thread.currentThread()+" "+this);
                    }
                }catch (InterruptedException e){
                    System.out.println("sleep() interruted");
                }finally {
                    System.out.println("all down");
                }
            }
        });
        daemon.setDaemon(true);
        daemon.start();
    }

    System.out.println("All started");
    TimeUnit.MILLISECONDS.sleep(175);
}
// output
All started
Thread[Thread-7,5,main] A$1@42337127
Thread[Thread-9,5,main] A$1@fb49a4f
Thread[Thread-8,5,main] A$1@47c48106
Thread[Thread-6,5,main] A$1@6fada00f
Thread[Thread-0,5,main] A$1@3cbeafaf
Thread[Thread-2,5,main] A$1@22e90474
Thread[Thread-1,5,main] A$1@3d142f7d
Thread[Thread-3,5,main] A$1@56a590e
Thread[Thread-4,5,main] A$1@1eec3130
Thread[Thread-5,5,main] A$1@2e24d89c

观察output可以发现,后台线程并没有循环打印,并且也没有执行finally中的all down打印,因此后台线程在不执行finally自居的情况下就会终止其run方法

这种行为是正确的,即便你基于前面对finally给出的承诺,并不希望出现这种行为,但情况仍将如此。当最后一个非后台线程终止时,后台线程会“突然”终止。因此一旦 main退出, JVM就会立即关闭所有的后台进程,而不会有任何你希望出现的确认形式。因为你不能以优雅的方式来关闭后台线程,所以它们几乎不是一种好的思想。非后台的 Executor通常是一种更好的方式,因为 Executor控制的所有任务可以同时被关闭。正如你将要在本章稍后看到的,在这种情况下,关闭将以有序的方式执行。

join一个线程

如果某个a线程在另一个线程t上调用t.join(),此调用线程a将被挂起,直到目标线程t结束才恢复。也可以在t.join()调用时加上一个超时参数,这样如果目标线程在这段时间到期时还没有结束的话join方法总能返回。

对join方法的调用可以被中断,做法就是在调用线程上调用interrupt方法。

在线程中捕获异常

由于线程的本质特性,使得我们不能捕获从线程中逃逸的异常。但是可以通过Thread.UncaughtExceptionHandler来进行异常处理,它允许在每个thread对象上附着一个异常处理器。

策略: 如果你知道将要在代码中处处使用相同的异常处理器,那么更简单的方式是在 Thread类中设置一个静态域,并将这个处理器设置为默认的未捕获异常处理器。这个处理器只有在不存在线程专有的未捕获异常处理器的情况下才会被调用。系统会检查线程专有版本,如果没有发现,则检查线程组是否有其专有的 uncaughtException()方法,如果也没有,再调用 defaultUncaughtExceptionHandler。示例如:

public class SettingDefaultHandler {
  public static void main(String[] args) {
    Thread.setDefaultUncaughtExceptionHandler(
      new MyUncaughtExceptionHandler());
    ExecutorService exec = Executors.newCachedThreadPool();
    exec.execute(new ExceptionThread());
  }
}

其他重要处理

以下处理叙述篇幅过长,请看本节分小节

  1. 本地存储ThreadLocal
  2. 终结任务/线程
  3. 线程之间的协作

线程注意事项

是否是线程安全的

  • static变量线程不安全,见上述的Runnable示例下的说明
  • 打印语句线程不安全

参考

  1. Java多线程学习(三)---线程的生命周期
  2. Java编程思想 第四版 中文版 倒数第二章
posted @ 2020-03-23 00:36  cheaptalk肥皂  阅读(373)  评论(0编辑  收藏  举报