Java
Java多线程

Java多线程 01 - 多线程基础

简介:线程共包括5种状态:新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)、死亡状态(Dead)。

1. 多线程状态

版权声明:本文大部分内容属原创,有部分内容参考自https://www.cnblogs.com/skywang12345/p/java_threads_category.html

多线程是Java中不可避免的一个重要主体。从本文开始,我们将展开对多线程的学习。接下来的内容,是对“JDK中新增JUC包”之前的Java多线程内容的讲解,涉及到的内容包括,Object类中的wait()notify()等接口;Thread类中的接口;synchronized关键字。

注:JUC包是指java.util.concurrent包,它是由Java大师Doug Lea完成并在JDK 1.5版本添加到Java中的。

在进入后面的学习之前,先对了解一些多线程的相关概念。首先需要了解的是线程状态图,它描述了线程的五种状态:

1.线程状态图.png

线程共包括以下5种状态:

  1. 初始状态(NEW):线程对象被创建后,就进入了新建状态。例如Thread thread = new Thread(),此时还未调用线程对象的start()方法。
  2. 运行状态(RUNNABLE):也被称为“可执行状态”,Java中将操作系统层面上线程的就绪和运行状态统称为RUNNABLE状态。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程,线程被启动后先会进入READY就绪状态,处于就绪状态的线程,随时可能被CPU调度执行,转而进入RUNNING正在运行状态。该状态分为以下两种情况:
    • 就绪状态(READY):此时线程被启动了,随时都可以开始执行,但是还未得到CPU调度。
    • 运行状态(RUNNING):线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
  3. 阻塞状态(BLOCKED):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。当线程在获取synchronized同步锁失败(因为锁被其它线程所占用)时就会进入BLOCKED状态。直到线程成功获取到synchronized同步锁,才会进入就绪状态,然后有机会转到运行状态。
  4. 等待状态(WAITING):线程进入等待状态则表示该线程需要等待其它线程做出一些特定的动作,比如通知或中断等。一般线程执行了wait()join(),或着被其他线程使用LockSupport.park()方法挂起后,就会进入WAITING状态。直到线程被notify()notifyAll()LockSupport.unpark()等方法唤醒后,才会进入就绪状态,然后有机会转到运行状态。
  5. 超时等待状态(TIMED_WAITING):该状态与WAITING状态不同的是,它可以在指定时间内自行结束等待。当线程执行了Thread.sleep(sleep_time)wait(timeout)join(timeout),或者被其他线程使用LockSupport,parkNanos(timeout)LockSupport,parkUntil(until)等方法挂起时,就会进入TIMED_WAITING状态。直到线程被notify()notifyAll()LockSupport.unpark()等方法唤醒,或者超时,才会进入就绪状态,然后有机会转到运行状态。
  6. 死亡状态(TERMMINATED):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

注:还有一种分类的方法,会将WAITING和TIME_WAITING统称为一种特殊的BLOCKED状态,即等待阻塞状态,而BLOCKED则被称为是同步阻塞状态。这两种方式都大同小异,WAITING、TIME_WAITING状态的线程在表现形式上相当于阻塞。

这6种状态涉及到的内容包括Object类、Thread类、LockSupport类和synchronized关键字。这些内容我们会在后面的章节中逐个进行学习。

  • Object类,定义了wait()notify()notifyAll()等休眠 / 唤醒函数。
  • Thread类,定义了一些列的线程操作函数。例如,sleep()休眠函数,interrupt()中断函数,getName()获取线程名称等。
  • synchronized是关键字;它区分为synchronized代码块和synchronized方法。synchronized的作用是让线程获取对象的同步锁。
  • LockSupport类是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport中的park()unpark()的作用分别是阻塞线程和解除阻塞线程。

在后面详细介绍wait()notify()等方法时,我们会分析为什么wait()notify()等方法要定义在Object类,而不是Thread类中。

2. Thread和Runnable

Runnable是一个接口,该接口中只包含了一个run()方法。它的定义如下:

  • public interface Runnable {
  • public abstract void run();
  • }

Runnable可以用来实现多线程。我们可以定义一个类A实现Runnable接口;然后,通过new Thread(new A())等方式新建线程。

Thread是一个类,Thread本身就实现了Runnable接口,它也是用来实现多线程。它的声明如下:

  • public class Thread implements Runnable { ... }

Thread和Runnable的相同点:都可以用于实现多线程。
Thread和Runnable的不同点:Thread是类,而Runnable是接口;Thread本身是实现了Runnable接口的类。由于一个类只能有一个父类,但是却能实现多个接口,使用Runnable实现的多线程具有更好的扩展性。此外,Runnable还可以用于资源的共享,如果多个线程都是基于某一个Runnable对象建立的,它们会共享Runnable对象上的资源。通常,建议通过Runnable实现多线程。

3. 多线程的实现方式

多线程可以有多种实现方式,但究其根本,还是通过Thread和Runnable来实现的。下面将分别介绍这些方式。

3.1. 使用Thread实现多线程

Thread是实现多线程的最基本的方法,用它来实现多线程是非常简单的。下面是一个通用的示例,模拟售票员卖票:

  • class SoldThread extends Thread {
  • // 总票数
  • private int tickets = 10;
  • @Override
  • public void run() {
  • // 循环30次,卖票
  • for (int i = 0; i < 30; i++) {
  • if (tickets > 0) {
  • System.out.println(Thread.currentThread().getName() + " sold ticket number :" + tickets--);
  • }
  • }
  • }
  • }
  • public class ThreadTest {
  • public static void main(String[] args) {
  • // 创建三个Thread线程
  • SoldThread soldThread1 = new SoldThread();
  • SoldThread soldThread2 = new SoldThread();
  • SoldThread soldThread3 = new SoldThread();
  • // 启动线程
  • soldThread1.start();
  • soldThread2.start();
  • soldThread3.start();
  • }
  • }

运行结果如下:

  • Thread-0 sold ticket number 10
  • Thread-0 sold ticket number 9
  • Thread-0 sold ticket number 8
  • Thread-0 sold ticket number 7
  • Thread-0 sold ticket number 6
  • Thread-0 sold ticket number 5
  • Thread-0 sold ticket number 4
  • Thread-0 sold ticket number 3
  • Thread-0 sold ticket number 2
  • Thread-0 sold ticket number 1
  • Thread-1 sold ticket number 10
  • Thread-1 sold ticket number 9
  • Thread-1 sold ticket number 8
  • Thread-1 sold ticket number 7
  • Thread-1 sold ticket number 6
  • Thread-1 sold ticket number 5
  • Thread-1 sold ticket number 4
  • Thread-1 sold ticket number 3
  • Thread-1 sold ticket number 2
  • Thread-1 sold ticket number 1
  • Thread-2 sold ticket number 10
  • Thread-2 sold ticket number 9
  • Thread-2 sold ticket number 8
  • Thread-2 sold ticket number 7
  • Thread-2 sold ticket number 6
  • Thread-2 sold ticket number 5
  • Thread-2 sold ticket number 4
  • Thread-2 sold ticket number 3
  • Thread-2 sold ticket number 2
  • Thread-2 sold ticket number 1

从结果可以得知:

  1. SoldThread继承于Thread,它是自定义个线程。每个SoldThread都会卖出10张票。
  2. 主线程main创建并启动3个SoldThread子线程。每个子线程都各自卖出了10张票。

3.1.1. Thread类中重要属性和方法

在Thread类中提供了与线程相关的大量属性和方法,用于记录线程信息和操作线程状态。下面将介绍几个相对比较重要的属性和方法。

  • public final native boolean isAlive():判断线程是否存活,是个本地方法。
  • public final ThreadGroup getThreadGroup():用于获取线程所属的组。
  • public final boolean isDaemon():是否是守护线程。可以使用setDaemon(boolean)方法来设置。需要注意的是,即使使用setDaemon(boolean)方法将某个线程设置为守护线程时,这个线程也会随主线程的死亡而死亡。
  • public State getState():获取线程状态。

3.2. 使用Runnable实现多线程

接下来是Runnable的多线程示例,代码如下:

  • class SoldThread2 implements Runnable {
  • // 总票数
  • private int tickets = 10;
  • @Override
  • public void run() {
  • // 循环30次,卖票
  • for (int i = 0; i < 30; i++) {
  • if (tickets > 0) {
  • System.out.println(Thread.currentThread().getName() + " sold ticket number :" + tickets--);
  • }
  • }
  • }
  • }
  • public class ThreadTest {
  • public static void main(String[] args) {
  • SoldThread2 soldThread2 = new SoldThread2();
  • new Thread(soldThread2).start();
  • new Thread(soldThread2).start();
  • new Thread(soldThread2).start();
  • }
  • }

运行结果如下:

  • Thread-0 sold ticket number :10
  • Thread-0 sold ticket number :7
  • Thread-0 sold ticket number :6
  • Thread-2 sold ticket number :8
  • Thread-1 sold ticket number :9
  • Thread-2 sold ticket number :4
  • Thread-0 sold ticket number :5
  • Thread-2 sold ticket number :2
  • Thread-1 sold ticket number :3
  • Thread-0 sold ticket number :1

从结果可以得知:

  1. 和上面SoldThread继承于Thread的方式不同;这里的SoldThread2实现了Thread接口。
  2. 主线程main创建并启动3个子线程,而且这3个子线程都是基于soldThread2这个Runnable对象而创建的,运行结果是这3个子线程一共卖出了10张票。这说明它们是共享了soldThread2的属性tickets的。

疑问:这种方式是否可行:

  • class SoldThread extends Thread {
  • // 总票数
  • private int tickets = 10;
  • @Override
  • public void run() {
  • // 卖票
  • for (int i = 0; i < 30; i++) {
  • if (tickets > 0) {
  • System.out.println(Thread.currentThread().getName() + " sold ticket number :" + tickets--);
  • }
  • }
  • }
  • }
  • public class ThreadTest {
  • public static void main(String[] args) {
  • // 创建一个soldThread线程
  • SoldThread soldThread = new SoldThread();
  • // 交给三个线程处理
  • new Thread(soldThread).start();
  • new Thread(soldThread).start();
  • new Thread(soldThread).start();
  • }
  • }

随机打印结果如下:

  • Thread-1 sold ticket number :10
  • Thread-2 sold ticket number :8
  • Thread-3 sold ticket number :9
  • Thread-2 sold ticket number :6
  • Thread-2 sold ticket number :4
  • Thread-1 sold ticket number :7
  • Thread-2 sold ticket number :3
  • Thread-3 sold ticket number :5
  • Thread-2 sold ticket number :1
  • Thread-1 sold ticket number :2

3.3. 带有返回值的多线程

使用FutureTask和Callable可以实现带有返回值的多线程任务,代码如下:

  • class CallableTest implements Callable<Integer> {
  • @Override
  • public Integer call() throws Exception {
  • System.out.println(Thread.currentThread().getName() + " Thread Begin invoke CallableTest call method");
  • Thread.sleep(1000);
  • return new Random().nextInt(Integer.MAX_VALUE);
  • }
  • }
  • public class FutureTest {
  • public static void main(String[] args) throws Exception {
  • CallableTest callableTest = new CallableTest();
  • FutureTask<Integer> futureTask = new FutureTask<>(callableTest);
  • Thread thread = new Thread(futureTask, "Call");
  • thread.start();
  • System.out.println(Thread.currentThread().getName() + " Thread invoked");
  • // 通过get()方法获取多线程任务的返回值
  • Integer result = futureTask.get();
  • System.out.println("Task Result: " + result);
  • }
  • }

FutureTask类是继承自Runnable接口的,它也可以配合Callable接口,使用Thread来创建多线程任务。上述代码运行结果如下:

  • main Thread invoked
  • Call Thread Begin invoke CallableTest call method
  • Task Result: 59516840

FutureTask和Callable会在后面的章节详细介绍。

3.4. 使用定时器实现多线程

使用java.util包下面提供的Timer类可以实现定时任务,这里的定时任务其实就是一个多线程任务,示例代码如下:

  • public class TimerTest {
  • public static void main(String [] args) {
  • Timer timer = new Timer();
  • // 使用schedule来调度任务,第一个参数是一个TimerTask,第二个参数时延时,第三个参数是执行间隔周期
  • timer.schedule(new TimerTask() {
  • @Override
  • public void run() {
  • System.out.println("Schedule executed " + new Date());
  • }
  • }, 0, 1000);
  • }
  • }

在实现定时任务中的TimerTask主要用于书写任务主题,TimerTask是继承自Runnable接口的。

注:企业开发中定时任务一般使用Quartz。

3.5. 使用线程池创建多线程

Java中提供了Executors用于创建线程池服务,使用线程池也可以创建多线程任务,示例代码如下:

  • public class ExecutorPoolTest {
  • public static void main(String[] args) {
  • // 创建线程池,线程的数量是10
  • ExecutorService threadPool = Executors.newFixedThreadPool(2);
  • // 循环创建10个多线程任务
  • for (int i = 0; i < 10; i++) {
  • // 使用线程池来创建多线程任务
  • final int number = i;
  • threadPool.execute(new Runnable() {
  • @Override
  • public void run() {
  • System.out.println(Thread.currentThread().getName() + " " + number);
  • }
  • });
  • }
  • // 关闭线程池
  • threadPool.shutdown();
  • }
  • }

从上述代码可以发现,我们创建了含有2个线程的线程池,然后循环提交了10个任务,线程池只会使用创建的2个线程分别执行这10个任务, 运行结果如下:

  • pool-1-thread-1 0
  • pool-1-thread-2 1
  • pool-1-thread-1 2
  • pool-1-thread-2 3
  • pool-1-thread-1 4
  • pool-1-thread-2 5
  • pool-1-thread-1 6
  • pool-1-thread-2 7
  • pool-1-thread-1 8
  • pool-1-thread-2 9

3.6. Lambda中的多线程

Java 8中的Lambda也提供了相应的多线程执行方法,例如下面的parallelStream()方法:

  • public class LambdaTest {
  • public static void main(String [] args) {
  • List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5, 6);
  • // 对数字的打印是多线程执行的
  • integers.parallelStream().map(i -> "parallelStream: " + i).forEach(System.out :: println);
  • // 单线程方式
  • integers.stream().map(i -> "stream: " + i).forEach(System.out :: println);
  • }
  • }

parallelStream()方法其实就是一个并行执行的流,它通过默认的ForkJoinPool,可能提高多线程任务的速度.。

4. 关于start和run方法

start()run()的有下面的区别:

start():它的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。
run()run()就和普通的成员方法一样,可以被重复调用,但单独调用run()的话,会在当前线程中执行run(),并不会启动新线程。

如上面的例子中:

  • SoldThread soldThread = new SoldThread();
  • soldThread.start();

soldThread.start();会启动一个新线程,并在新线程中运行run()方法。而soldThread.run()则会直接在当前线程中运行run()方法,并不会启动一个新线程来运行run()方法。

使用一个实际的例子进行演示:

  • class SoldThread extends Thread {
  • private int tickets = 10;
  • @Override
  • public void run() {
  • for (int i = 0; i < 30; i++) {
  • if (tickets > 0) {
  • System.out.println(Thread.currentThread().getName() + " sold ticket number :" + tickets--);
  • }
  • }
  • }
  • }
  • public class ThreadTest {
  • public static void main(String[] args) {
  • // 创建线程
  • SoldThread soldThread = new SoldThread();
  • // 使用start启动线程
  • soldThread.start();
  • // 直接运行run方法
  • soldThread.run();
  • }
  • }

运行结果如下:

  • main sold ticket number 10
  • main sold ticket number 8
  • main sold ticket number 7
  • main sold ticket number 6
  • main sold ticket number 5
  • Thread-0 sold ticket number 9
  • Thread-0 sold ticket number 3
  • Thread-0 sold ticket number 2
  • Thread-0 sold ticket number 1
  • main sold ticket number 4

其中main开头的打印是调用run()方法发生的,也就是说直接调用run()方法会直接在当前主线程执行相关的代码。不过从这个例子中可以看出,同一个Thread对象的属性是被主线程和子线程共享的。

在上面的例子中,由于我们调用start()方法在run()方法之前,因此在直接执行run()方法时是已经启动了子线程进行卖票操作,如果我们将run()方法的调用放在start()之前,调整如下:

  • public class ThreadTest {
  • public static void main(String[] args) {
  • // 创建线程
  • SoldThread soldThread = new SoldThread();
  • // 直接运行run方法
  • soldThread.run();
  • // 使用start启动线程
  • soldThread.start();
  • }
  • }

这种情况下运行将直接得到下面的打印:

  • main sold ticket number :10
  • main sold ticket number :9
  • main sold ticket number :8
  • main sold ticket number :7
  • main sold ticket number :6
  • main sold ticket number :5
  • main sold ticket number :4
  • main sold ticket number :3
  • main sold ticket number :2
  • main sold ticket number :1

不管尝试运行多少次,打印结果与上面的一样,表明所有的卖票操作都在main主线程中完成的。这是因为run()方法在直接执行的时候并不是异步的,所以必须等run()方法执行完成,才会调用start()方法,而此时在run()方法里已经将票全部卖完了,所以start()启动子线程后并不会打印任何结果。

java.lang.Thread类中可以得到start()方法的源码如下:

  • public synchronized void start() {
  • // 如果线程状态不是就绪状态就抛出异常
  • if (threadStatus != 0)
  • throw new IllegalThreadStateException();
  • // 将线程添加到线程组中
  • group.add(this);
  • // 标识是否启动成功的变量
  • boolean started = false;
  • try {
  • // 使用本地方法start0()启动变量
  • start0();
  • // 更新标识变量,标识线程启动成功
  • started = true;
  • } finally {
  • try {
  • // 启动失败则需要告诉线程组
  • if (!started) {
  • group.threadStartFailed(this);
  • }
  • } catch (Throwable ignore) {
  • /* do nothing. If start0 threw a Throwable then
  • it will be passed up the call stack */
  • }
  • }
  • }
  • // 本地方法
  • private native void start0();

从源码可以看出,在调用start()方法时实际上是调用start0()本地方法去创建新的线程。另外,源码可以解释一个很常见的面试问题:对一个线程连续调用两次start()会发生什么?源码中给出了解释,连续调用两次start()会抛出java.lang.IllegalThreadStateException,即非法线程状态异常。

java.lang.Thread类中run()的代码如下:

  • public void run() {
  • if (target != null) {
  • target.run();
  • }
  • }

target是一个Runnable对象,run()就是直接调用Thread线程中Runnable成员的run()方法,并不会新建一个线程。