【Java】多线程初探

时间:2024-01-18 16:39:08
 参考书籍:《Java核心技术 卷Ⅰ 》

Java的线程状态

从操作系统的角度看,线程有5种状态:创建, 就绪, 运行, 阻塞, 终止(结束)。如下图所示
【Java】多线程初探
 【Java】多线程初探
而Java定义的线程状态有: 创建(New), 可运行(Runnable), 阻塞(Blocked), 等待(Waiting), 计时等待(Time waiting) 被终止(Terminated)。
那么相比起操作系统的线程状态, Java定义的线程状态该如何解读呢? 如下:
1. Java的阻塞、 等待、 计时等待都属于操作系统中定义的阻塞状态,不过做了进一步的划分,阻塞(Blocked)是试图获得对象锁(不是java.util.concurrent库中的锁),而对象锁暂时被其他线程持有导致的;等待(Waiting)则是调用Object.wait,Thread.join或Lock.lock等方法导致的;计时等待(Time waiting)则是在等待的方法中引入了时间参数进入的状态,例如sleep(s)
2. Java的Runnable状态实际上包含了操作系统的就绪和运行这两种状态, 但并没有专门的标识进行区分,而是统一标识为Runnable
获取当前线程的状态和名称
currentThread()是Thread类的一个静态方法,它返回的是当前线程对象
对某个线程对象有以下方法:
  • getState方法:返回该线程的状态,可能是NEW, RUNNABLE, BLOCKED, WAITING, TIME_WAITING, TEMINATED之一
  • getName: 返回线程名称
  • getPriority: 返回线程优先级
下面的例子中,我们通过Thread.currentThread()获取到当前线程对象, 并打印它的状态,名称和优先级:
public class MyThread extends Thread{
  @Override
  public void run() {
    System.out.println("线程状态:" + Thread.currentThread().getState());
    System.out.println("线程名称:" + Thread.currentThread().getName());
    System.out.println("线程优先级:" + Thread.currentThread().getPriority());
  }
  public static void main (String args []) {
    MyThread t = new MyThread();
    t.start();
  }
}

输出:

线程状态:RUNNABLE
线程名称:Thread-0
线程优先级:5

线程的创建和启动

创建线程主要有三种方式:
1 .继承Thread类
2. 实现runnable接口
3. 使用Callable和Future
对这三种方式,创建线程的方式虽然不同,但启动线程的方式是一样的,都是对线程实例调用start方法来启动线程。创建线程的时候,线程处于New状态,只有调用了start方法后,才进入Runnable状态

一. 继承Thread类创建线程

可以让当前类继承父类Thread, 然后实例化当前类就可以创建一个线程了
public class MyThread extends Thread {
  private int i = 0;
  public void run () {
      i++;
      System.out.println(i);
  }
  public static void main (String args []){
    MyThread t = new MyThread();
    t.start();
  }
}
输出
 1

二. 实现Runnable接口创建线程

也可以让当前类继承Runnable接口, 并将当前类实例化后得到的实例作为参数传递给Thread构造函数,从而创建线程
MyRunnable.java
public class MyRunnable implements Runnable {
  private int i =0;
  @Override
  public void run() {
    i++;
    System.out.println(i);
  }
}

Test.java

public class Test {
  public static void main (String args[]) {
    Thread t = new Thread(new MyRunnable());
    t.start();
  }
}

输出

 1

三. 通过Callable接口和Future接口创建线程

Callable接口
Callable接口和Runnable接口类似, 封装一个在线程中运行的任务,区别在于Runnable接口没有返回值,具体来说就是通过Runnable创建的子线程不能给创建它的主线程提供返回值。而Callable接口可以让一个运行异步任务的子线程提供返回值给创建它的主线程。 实现Callable需要重写call方法,call方法的返回值就是你希望回传给主线程的数据。
Future接口
Future可以看作是一个保存了运行线程结果信息的容器。可以和Callable接口和Runnable接口配合使用。
Future接口中有如下方法:
public interface Future<V> {
  V get () throws ...; // 当任务完成时, 获取结果
  V get (long timeout, TimeUnit unit); // 在get方法的基础上指定了超时时间
  void cancel ( boolean mayInterupt);  // 取消任务的执行
  boolean isDone (); // 任务是否已完成
  boolean isCancel ();  // 任务是否已取消
}
Future对于Callable和Runnable对象的作用
  • 对于Callable对象来说, Future对象可帮助它保存结果信息,当调用get方法的时候将会发生阻塞, 直到结果被返回。
  • 而对于Runnable对象来说, 无需保存结果信息, 所以get方法一般为null,  这里Future的作用主要是可以调用cancel方法取消Runnable任务
FutureTask
FutureTask包装器是衔接Callable和Future的一座桥梁, 它可以将Callable转化为一个Runnable和一个Future, 同时实现两者的接口。 即通过
FutureTask task = new FutureTask(new Callable);

得到的task既是一个Runnable也是一个Future。这样一来,可以先把得到的task传入Thread构造函数中创建线程并运行(作为Runnable使用), 接着通过task.get以阻塞的方式获得返回值(作为Future使用)

下面是一个示范例子:
MyCallable.java
import java.util.concurrent.Callable;
 
public class MyCallable implements Callable {
  @Override
  public Object call() throws Exception {
    Thread.sleep(1000);
    return "返回值";
  }
}

Test.java

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class Test {
  public static void main (String args []) throws ExecutionException, InterruptedException {
    // task同时实现了Runnable接口和Future接口
    FutureTask task = new FutureTask(new MyCallable());
    // 作为 Runnable 使用
    Thread t = new Thread(task);
    t.start();
    // 作为Future使用, 调用get方法时将阻塞直到获得返回值
    System.out.println(task.get());
  }
}
大约1秒后输出:
返回值
继承Thread和实现Runnable接口的区别
总体来说实现Runnable接口比继承Thread创建线程的方式更强大和灵活,体现在以下几个方面:
1. 可以让多个线程处理同一个资源:实现Runnable的类的实例可以被传入不同的Thread构造函数创建线程, 这些线程处理的是该实例里的同一个资源
2. 更强的扩展性:接口允许多继承,而类只能单继承, 所以实现Runnable接口扩展性更强
3. 适用于线程池:线程池中只能放入实现了Runnable或者Callable类的线程,不能放入继承Thread的类
Runnable接口和Callable接口的区别
1. 实现Runnable接口要重写run方法, 实现Callable接口要重写call方法
2. Callable的call方法有返回值, Runnable的run方法没有
3. call方法可以抛出异常, run方法不可以

四.通过线程池创建和管理线程

在实际的应用中, 通过上面三种方式直接创建线程可能会带来一系列的问题,列举如下:
<1>. 启动撤销线程性能开销大:线程的启动,撤销会带来大量开销,大量创建/撤销单个的生命周期很短的线程将是这一点更加严重
<2>. 响应速度慢:从创建线程到线程被CPU调度去执行任务需要一定时间
<3>. 线程难以统一管理
而使用线程池能够解决单独创建线程所带来的这些问题:
对<1>: 线程池通过重用线程能减少启动/撤销线程带来的性能开销
对<2>: 相比起临时创建线程,线程池提高了任务执行的响应速度
对<3> :  线程池能够统一地管理线程。例如1. 控制最大并发数 2.灵活地控制活跃线程的数量
3. 实现延迟/周期执行
和线程池相关的接口和类
对线程池的操作, 要通过执行器(Executor)来实现。通过执行器,可以将Runnable或Callable提交(submit)到线程池中执行任务,并在线程池用完的时候关闭(shutdown)线程池。
(注意:线程池和执行器在一定程度上是等效的)
Executor接口
它是执行器的顶层接口, 定义了execute方法
public interface Executor {
    void execute(Runnable command);
}
ExecutorService接口
它是Executor接口的子接口,并对Executor接口进行了扩展,下面是部分代码
public interface ExecutorService extends Executor {
    void shutdown();
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    // 其他方法
}
对于实现了ExecutorService接口的类的实例:
  • 调用submit方法可以将Runnable或Callable实例提交给线程池里的空闲线程执行,同时返回一个Future对象, 保存了和执行结果有关的信息
  • 当线程池用完时, 需要调用 shutdown方法关闭线程
Executors类
(注意Executor是接口,Executors是类)
Executor是一个保存着许多静态的工厂方法的类,这些静态方法都返回ExecutorService类型的实例
public class Executors {
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
        }
        
     public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
        }
}
Executors的工厂方法汇总
【Java】多线程初探
【Java】多线程初探
线程池的一般使用流程
1. 调用Executors类中的工厂方法,如newFixedThreadPool获得线程池(执行器)实例
2. 调用submit方法提交Runnable对象或Callable对象
3. 如果提交的是Callable对象, 或者提交的是Runnable对象但想要取消,则应该保存submit方法返回的Future对象
4. 用完线程池,调用shutdown方法关闭它
线程池使用的例子
MyRunnable.java:
public class MyRunnable implements Runnable{
  @Override
  public void run() {
    for (int i=0;i<3;i++) {
      System.out.println("MyRunnable正在运行");
    }
  }
}
MyCallable.java:
import java.util.concurrent.Callable;
 
public class MyCallable implements Callable{
  @Override
  public Object call() throws Exception {
    for (int i=0;i<3;i++) {
      System.out.println("MyCallable正在运行");
    }
    return "回调参数";
  }
}
Test.java:
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
public class Test {
  public static void main (String args []) throws ExecutionException, InterruptedException {
    // 创建一个固定数量为2的线程池
    ExecutorService service = Executors.newFixedThreadPool(2);
    // 向线程池提交Callable任务,并将结果信息保存到Future中
    Future callableFuture = service.submit(new MyCallable());
    // 向线程池提交Runnable任务,并将结果信息保存到Future中
    Future runnableFuture = service.submit(new MyRunnable());
    // 输出结果信息
    System.out.printf("MyCallable, 完成:%b取消:%b返回值:%s%n", callableFuture.isDone(),
            callableFuture.isCancelled(), callableFuture.get());
    System.out.printf("MyRunnable, 完成:%b取消:%b返回值:%s%n", runnableFuture.isDone(),
            runnableFuture.isCancelled(), runnableFuture.get());
    // 关闭线程池
    service.shutdown();
  }
}
输出:
MyCallable正在运行
MyCallable正在运行
MyCallable正在运行
MyCallable, 完成:true取消:false返回值:回调参数
MyRunnable正在运行
MyRunnable正在运行
MyRunnable正在运行
MyRunnable, 完成:false取消:false返回值:null

线程的运行

我们是不能通过调用线程实例的一个方法去使线程处在运行状态的, 因为调度线程运行的工作是由CPU去完成的。通过调用线程的start方法只是使线程处在Runnable即可运行状态, 处在Runnable状态的线程可能在运行也可能没有运行(就绪状态)。
【注意】Java规范中并没有规定Running状态, 正在运行的线程也是处在Runnable状态。

线程的阻塞(广义)

开头介绍线程状态时我们说到, 线程的阻塞状态(广义)可进一步细分为:阻塞(Blocked), 等待(Waiting), 计时等待(Time waiting) 这三种状态, 这里再说明一下:
  • 阻塞(Blocked)是试图获得对象锁(不是java.util.concurrent库中的锁),而对象锁暂时被其他线程持有导致
  • 等待(Waiting)则是调用Object.wait,Thread.join或Lock.lock等方法导致的
  • 计时等待(Time waiting)则是在等待的方法中引入了时间参数进入的状态,例如sleep(s)

线程的终止

线程终止有两个原因:
1.run方法正常运行结束, 自然终止
2.发生异常但未捕获, 意外终止
这里暂不考虑第二种情况, 仅仅考虑第一种情况,则:
正如我们没有直接的方法调用可以让线程处在运行状态, 我们同样也没有直接的方法调用可以终止一个线程(注:这里排除了已经废弃的stop方法),所以,我们要想终止一个线程,只能是让其“自然结束”, 即run方法体内的最后一条语句执行结束, 在这个思路的基础上,我们有两种方式可以结束线程:
1. 共享变量结束线程
2. 使用中断机制结束线程

1. 共享变量结束线程

我们可以设置一个共享变量,在run方法体中,判断该变量为true时则执行有效工作的代码,判断为false时候则退出run方法体。共享变量初始为true, 当想要结束线程的时候将共享变量置为false就可以了
优点: 简单易懂,在非阻塞的情况下能正常工作
缺点:  当线程阻塞的时候, 将不会检测共享变量,线程可能不能及时地退出。
public class InteruptSimulation implements Runnable{
  private volatile static boolean stop = false;
  @Override
  public void run() {
    try {
      while (!stop) {
        System.out.println("线程正在运行");
        // 休眠5秒
        Thread.sleep(5000);
      }
      System.out.println("线程终止");
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
 
  public static void main (String args []) throws InterruptedException {
    Thread t = new Thread(new InteruptSimulation());
    t.start();
    // 休眠1秒
    Thread.sleep(1000);
    // 将共享变量stop置为true
System.out.println("发出终止线程的信号");
    stop = true;
  }
}
线程正在运行
发出终止线程的信号
// 约5s后输出
线程终止
如上所示, 我们试图在线程启动1秒后就结束线程,但实际上在大约5秒后线程才结束。这是因为线程启动后因为休眠(sleep)而陷入了阻塞状态(等待),这时候自然是不会检测stop变量了。 所以在阻塞状态下,共享变量结束线程的方式可能并不具备良好的时效性

2. 利用中断机制结束线程

因为直接使用共享变量的方式不能很好应对线程阻塞的情况,所以我们一般采用中断机制结束线程,单从形式上看,采用中断机制结束线程和共享变量的管理方式
并没有太大区别,假设t是当前线程,则调用t.interrupt()会将线程中的中断状态位置为true, 然后通过t.isInterrupted()可以返回中断状态位的值。
区别在于:当刚好遇到线程阻塞的时候, 中断会唤醒阻塞线程,这样的话我们就可以及时的结束线程了。
public class InteruptReal implements Runnable{
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程正在运行");
Thread.sleep(5000);
}
} catch (InterruptedException e) {
// 发生中断异常后,中断状态位会被置为false,这里不做任何操作
}
System.out.println("线程已中断");
} public static void main (String args []) throws InterruptedException {
Thread t = new Thread(new InteruptReal());
t.start();
// 休眠1s
Thread.sleep(1000);
System.out.println("发出终止线程的信号");
t.interrupt();
}
}

输出:

线程正在运行
发出终止线程的信号
// 立即输出
线程已中断

线程现在已经能够及时退出啦

中断线程的时候, 如果线程处在阻塞状态,则会1. 唤醒阻塞线程,使其重新重新处于RUNNABLE状态 2. 将中断状态位 置为false
注意! 在唤醒阻塞线程的同时会将中断状态位置为false, 这也许让人感觉有些奇怪,但这说明了JAVA给了你更多的处理线程的*度。在被阻塞的线程唤醒后,你可以选择再次发起中断,也可以选择不中断。
例子如下: 唤醒阻塞线程后, 中断状态位会被置为false
public class InteruptReal implements Runnable{
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程正在运行");
Thread.sleep(5000);
}
} catch (InterruptedException e) {
System.out.println("中断状态位:"+Thread.currentThread().isInterrupted());
} } public static void main (String args []) throws InterruptedException {
Thread t = new Thread(new InteruptReal());
t.start();
// 休眠1s
Thread.sleep(1000);
System.out.println("发出中断");
t.interrupt();
}
}

输出:

线程正在运行
发出中断
中断状态位:false
【注意】 Java已经废弃了线程的stop方法, 因为在多线程中,它极有可能破坏预期的原子操作, 并因此使得线程共享变量取得错误的值。

线程的常用方法调用

Thread.sleep

让线程休眠一段时间,调用它时,线程将进入计时等待状态(Time waiting)

Thread.yeild

使正在运行的线程让出CPU的执行权,进入就绪状态(注意是就绪),将占用CPU的机会让给优先级相同或者更高的线程

Thread.join

join方法的作用是等待某个线程终止后才继续执行,效果类似于Future的get方法。 例如我们可能存在这样一个需求: 在主线程中启动了一个子线程,但希望子线程运行完后才执行主线程中的代码,在子线程运行完毕前主线程处于阻塞的状,这时就可以使用join方法
举个例子,在下面我们想要在子线程t执行完毕后,在主线程中输出“子线程执行完毕”, 下面的使用显然不能达到效果, 因为主线程的输出并不会因为子线程的运行而阻塞。
public class JoinRunnable implements Runnable{
  @Override
  public void run() {
    for(int i=0;i<3;i++) {
      System.out.println(Thread.currentThread().getName()+ "正在执行");
    }
  }
 
  public static void main (String args[]) throws InterruptedException {
    Thread t = new Thread(new JoinRunnable());
    t.start();
    System.out.println("子线程执行完毕");
  }
}
输出:
子线程执行完毕
Thread-0正在执行
Thread-0正在执行
Thread-0正在执行
而使用join方法后就可以达到我们想要的效果
public class JoinRunnable implements Runnable{
  @Override
  public void run() {
    for(int i=0;i<3;i++) {
      System.out.println(Thread.currentThread().getName()+ "正在执行");
    }
  }
 
  public static void main (String args[]) throws InterruptedException {
    Thread t = new Thread(new JoinRunnable());
    t.start();
    t.join();
    System.out.println("子线程执行完毕");
  }
}

输出:

Thread-0正在执行
Thread-0正在执行
Thread-0正在执行
子线程执行完毕
【注意】 join方法可以用Future的实现代替