Java并发编程(一): 介绍线程及其生命周期

前言

操作系统有两个容易混淆的概念:进程和线程。相信有不少人对于这两个概念还是有点模糊的。本篇内容主要是介绍进程、线程的基本概念,同时介绍线程的生命周期,作为并发编程的基础。

概念

  • 进程:进程是操作系统对一个正在运行的程序的一种抽象,在一个系统上可以同时运行多个进程,而每个进程都好像在独占地使用硬件。进程是操作系统进行资源分配和调度的一个基本单位;举个例子来说,我早上起床打开网易云音乐,看看我喜欢的音乐人有没有发新歌,其实这个时候我就是启动了一个进程(只是举例,网易云音乐这种级别的应用不止一个进程);
  • 线程:线程是进程的一个实体,是CPU调度和分配的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),并且与在同一个进程中的其他线程共享进程所拥有的的全部资源。一个进程中至少会有一条线程,拿我们Android应用来说,就是主线程MainThread。再举个例子,我打开网易云音乐后,看到发了新歌,然后我点击下载到本地,这个时候其实就是开启了一条线程,下载的时候你还可以去做其他的事情,不受影响;
  • 多线程:同一时刻,多条线程同时运行,比如我同时下载多首歌曲;

Java中的线程

在Java中,java.lang包给我们提供了Thread.java线程类,在Java中,常用的创建线程的三种方式有:

1.通过继承Thread类创建线程类

图中代码就是通过继承Thread类,重写Thread类中的run()方法创建线程,其中需要注意的是:

  • 01.程序执行后,main方法中打印出来的线程是当前线程是main,这个是程序默认的线程,也就是主线程;
  • 02.启动线程是调用Thread对象的start( )方法,而不是直接调用run( )方法;我们在调用start( )方法后,线程可能并没有立即执行,这个我们后面再讲;
  • 03.通过此种方式创建线程,在run( )方法中打印线程时可以使用this获取到当前线程,线程默认的名称格式是Thread-0,Thread-1...等等;
2.通过实现Runnable接口创建线程

图中代码是通过实现Runnable接口创建线程,需要注意的是:

  • 第一种创建线程的方法中01和02注意点适用于此处;
  • run( )方法中打印线程名称不能再使用this,因为实现Runnable接口只是作为线程的执行体传给Thread构造函数,真正的线程对象还是new出来的Thread对象;
3.通过Callable和Future接口创建线程

Java5之后,Java提供了Callable接口。这个接口是一个泛型接口,声明了一个call( )方法,该接口和Runnable接口不同的地方在于:

  • call( )有返回值,返回类型就是传递进来的泛型类型;
  • 可以抛出异常;

Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果,我们看下Future接口中的方法:

对接口的方法做一个简单的介绍:

扫描二维码关注公众号,回复: 11249017 查看本文章
  • V get() :获取Callable接口中call()方法的返回值,调用该方法会导致线程阻塞,一直到子线程结束;
  • V get(Long timeout , TimeUnit unit) :获取Callable接口中call()方法的返回值,调用该方法会导致线程阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。
  • boolean isDone() :如果Callable任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。
  • boolean isCancelled() :如果任务完成前被取消,则返回true。
  • boolean cancel(boolean mayInterruptRunning) :如果任务还没开始,执行cancel(...)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务;如果停止成功,返回true;当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;当任务已经完成,执行cancel(...)方法将返回false。mayInterruptRunning参数表示是否中断执行中的线程。

因为Future类无法直接配合Callable使用,所以Java提供了FutureTask类来配合Callable完成创建线程的任务:

图中代码为使用FutureTask和Callable创建线程; Java中常用的三种创建线程的方式就介绍完毕了,下面我们看下Java线程的生命周期,了解了线程的"生老病死",使用线程就能避免很多不必要的问题

Java线程的生命周期

Java语言中线程共有六种状态:

  • NEW(初始化状态)
  • RUNNABLE(可运行 / 运行状态)
  • BLOCKED(阻塞状态),归为休眠状态
  • WAITING(无时限等待),归为休眠状态
  • TIMED_WAITING(有时限等待),归为休眠状态
  • TERMINATED(终止状态) Java线程的生命周期状态简单转换图如下:

我们具体分析下状态的转换过程:

  • NEW初始状态- > RUNNABLE可运行/运行状态:首先,初始状态很简单,当我们在Java中创建线程对象即Thread thread = new Thread()时,线程就是处于初始状态。处于初始状态的线程,不会被操作系统调度,这个时候操作系统还不知道它的存在。如果想要执行Java线程,就要转换到RUNNABLE状态,就是我们代码中用到的thread.start()调用线程对象的start()方法。这个时候线程是处于可运行状态,等待CPU分配时间片,只有当CPU分配时间片后,线程才真正处于运行状态;换句话说,如果线程的优先级非常低,而系统中有很多优先级高的线程,那么这个线程可能很久才会被执行;
  • RUNNABLE可运行/运行状态 -> TERMINATED终止状态:线程执行完run()方法后,会自动转成终止状态;当执行run()方法过程中出现异常,也会导致线程终止。线程run()方法一旦执行完成,我们认为这个线程就已经死亡了,即使这个线程对象还存在。如果在一个终止状态的线程对象上调用start()方法,会导致抛出java.lang.IllegalThreadStateException异常,即处于终止状态的对象不能再使用。
  • RUNNABLE可运行/运行状态 <-> BLOCKED阻塞状态: 这个状态,一般是线程,比如线程A,在等待获取一个锁,这个锁被其他线程占有,线程A在等待获取锁的过程中的状态就是BLOCKED阻塞状态。在Java中,当线程等待获取synchronized的隐式锁的时候,由于synchronized修饰的方法、代码块同一时刻只能允许一个线程进入,其他线程只能在外等待,这种情况就会触发这个状态转换;当持有锁的线程释放锁,等待线程获得synchronized锁,状态就会从BLOCKED转换为可运行状态;

需要注意的一点:阻塞在java.concurrent包中Lock接口的线程状态是等待状态WAITING,因为Lock接口对阻塞的实现均使用了LockSupport类中的相关方法;

  • RUNNABLE可运行/运行状态 <-> WAITING无时限等待状态: 能够触发RUNNABLE状态转换为 WAITING状态的方法有三种:
    • 01.在获取synchronized锁的线程中,调用了Object.wait()无参方法;
    • 02.调用LockSupport.park()方法;调用此方法时,线程会阻塞,线程状态从RUNNABLE转换到WAITING状态,调用LockSupport.unpark(Thread thread)可唤醒目标线程,线程状态又切换回RUNNABLE;
    • 03.调用Thread.join()无参方法;此方法是一个同步方法,比如下面代码:

代码执行到thread2.join()方法时,会阻塞线程,等thread2这个线程执行完成后,才会打印下面的输出内容;你可以将这句代码去掉,再看下输出结果是不是这样。 输出结果如下:
针对wait(),调用Object.notify()和Object.notifyAll()方法可以从WAITING状态转换到RUNNABLE状态;其中这两个的区别是第一个方法是随机通知等待线程中的一个线程,而后面一个方法是通知所有等待线程;通知的结果是等待线程从条件变量的等待队列进入到锁的同步队列。 notify()和notifyAll()对比: 推荐使用notifyAll()方法,因为notify()方法有可能导致死锁。举个例子,生产者和消费者数量都为2,缓冲区为1,首先消费者1获得锁,一看缓冲区为0,等待并释放锁;然后消费者2获得锁,情况和1一样;这个时候生产者1获得了锁,发现缓冲区是0,可以生产,生产之后缓存区为1了,然后notify,这个时候我们理想的状态是通知了一个消费者来消耗缓冲区内容,但是这个时候消费者虽然被通知了可以争夺锁,但是被生产者2抢到了这个锁;生产者2一看,缓冲区为1,无法生产,这个时候调用wait()方法等待并释放了锁;然后消费者1获得了锁,消耗了缓冲区的内容,并调用了notify()通知等待队列中的某个线程,缓冲区现在为0;结果这个时候被唤醒的是消费者2,然后消费者2一看缓冲区为0,好吧,继续等待;这样就成了死锁。

  • RUNNABLE可运行/运行状态 <->TIMED_WAITING有时限等待状态: 能够触发RUNNABLE状态转换到TIMED_WAITING状态的操作有以下五种:
    • 调用Thread.sleep(long millis)静态方法,线程睡眠millis时间,这段时间是阻塞状态,超过设定的时长即会转换为RUNNABLE状态;
    • 在获取synchronized锁的线程中,调用Object.wait(long millis)方法;
    • 调用Thread.join(long millis)方法
    • 调用LockSupport.parkNanos(Object blocker, long nanos) 方法
    • 调用LockSupport.parkUntil(Object blocker, long deadline)方法 上述五种方法中都包含了一个超时的参数,超过这个设定的时长即可转换回RUNNABLE状态;

如何终止线程的运行

线程的终止需要由自己决定,所以原来的stop()方法被废弃掉了,同样废弃的还有resume()和suspend()方法,为什么被废弃呢?因为stop()调用的时候有可能导致该线程获取到的锁没有来得及释放就被终止,这样就导致其他线程都无法再获取锁;另一个原因就是stop()方法立即停止线程有可能导致状态的不同步; 我们使用thread.interrupt() 来停止线程,其实这个方法的作用并不是中断线程,而是通知线程应该中断了,api如下:

  • interrupt(): 将线程中断的标志位置为true,线程是否要中断由自己说了算,也可能被忽略掉;
  • interrupted():静态方法,获取线程是否有中断标记,如果是中断状态返回true,不是则返回false,同时将清除线程的中断状态,也就是说如果一开始是返回true,当第二次调用的时候返回则为false;
  • isInterrupted():获取线程是否被中断;

当线程处于阻塞状态时,如果调用线程的 interrupt() 方法,线程会返回RUNNABLE状态,同时抛出InterruptedException异常,我们在调用阻塞方法时,也需要处理此异常; 当线程处于正常活动状态时,如果调用线程的 interrupt() 方法,则会将线程中的中断标志置为true,并继续运行,不受影响。如何才能中断线程呢?其实我们可以在执行run()方法时,不断检测中断标志位,如果是true,就自己停止线程;我们一般的做法是用while循环,通过调用就isInterrupted()方法,如果是false,就执行正常任务,否则就执行中断处理。至于如何处理这个根据你任务情况自行处理就好了。 Thread.interrupted()会清除标志位,并不是代表线程又恢复了,可以理解为仅仅是代表它已经响应完了这个中断信号然后又重新置为可以再次接收信号的状态。

写在最后

要想搞懂并发,首先要熟悉线程,而熟悉线程,最重要的就是了解线程执行的生命周期,这样才能更好的写好并发代码。

欢迎扫码或搜索关注公众号:Android进阶之旅

猜你喜欢

转载自juejin.im/post/5e478b96518825497558121c