Java中的线程基础篇-线程基本概念

线程的概念、创建方式、生命周期、优缺点

  • 一、基础知识
    • 1. 进程、线程、协程
      • 1.1 进程
      • 1.2 线程
      • 1.3 协程
    • 2. 串行、并发、并行
      • 2.1 串行
      • 2.2 并发
      • 2.3 并行
  • 二、线程的创建
    • 1. 继承Thread类
      • 1.1 实现步骤
      • 1.2 特点
    • 2. 实现Runnable接口
      • 2.1 实现步骤
      • 2.2 与Thread相比
    • 3. 实现Callable接口(JDK5.0新增)
      • 3.1 实现步骤
      • 3.2 与Runnable接口相比
  • 三、线程的常用方法
  • 四、线程的生命周期
    • 1. 基本概念
    • 2. 六个状态
  • 五、线程的优缺点
    • 1. 优点
    • 2. 缺点

一、基础知识

1. 进程、线程、协程

Java中的线程基础篇-线程基本概念_第1张图片

1.1 进程

  • 基本概念:进程是计算机中的程序关于某数据集合上的一次运行活动,是操作系统进行资源分配与调度的基本单位。简单来说,就是操作系统中正在运行的一个程序。在操作系统中是以进程为单位分配资源,如虚拟存储空间、文件描述符等。

1.2 线程

  • 基本概念:线程是进程的基本执行单元。一个线程就是进程中一个单一顺序的控制流,是进程的一个执行分支。进程是线程的容器,一个进程中至少有一个线程。每个线程都有各自的线程栈、寄存器环境、本地存储。
  • 主线程:JVM启动时会创建一个主线程,该主线程负责执行main方法,主线程就是运行main方法的线程。
  • 父线程和子线程:Java中的线程并不孤立,线程之间存在着联系,如果在A线程中创建了B线程,则称B线程为A线程的子线程,相应的,A线程就是B线程的父线程。

1.3 协程

  • 基本概念:协程是更轻量级的线程,它不被操作系统内核所管理,而是完全由程序控制。一个线程可以拥有多个协程,每个协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

2. 串行、并发、并行

Java中的线程基础篇-线程基本概念_第2张图片

2.1 串行

  • 基本概念:所有线程依次执行。

2.2 并发

Java中的线程基础篇-线程基本概念_第3张图片

  • 基本概念:一个处理器,多个线程在逻辑上的同时运行,实际上是采用时间片轮转的技术进行的逻辑同时运行。

2.3 并行

Java中的线程基础篇-线程基本概念_第4张图片

  • 基本概念:多个处理器,多个线程在物理上的同时运行,真正的实现了多个线程的同时运行。

二、线程的创建

1. 继承Thread类

1.1 实现步骤

  1. 创建一个继承于Thread类的子类。
  2. 重写Thread的run()方法 —> 将此线程的方法声明在run()中。
  3. 创建Thread子类的对象。
  4. 通过此对象调用start()方法。

1.2 特点

  • 调用线程的start()方法来启动线程,实际上是通知JVM当前线程已经准备好了,请求JVM运行相应的线程,线程具体在什么时候运行由线程的调度器来决定,start()方法调用结束并不意味着子线程立刻开始运行。
  • 调用start()方法,实际上将会完成两个过程:① 启动当前线程 ② 调用当前线程的run()方法。
  • 如果开启了多个线程,JVM调用的顺序并不一定是线程启动的顺序,具体谁先谁后,由线程调度器决定。也就是说,多线程的运行结果与线程的调用顺序无关。
  • 我们不可以通过调用run()方法的方式启动线程。
  • 如果还需要一个线程做相同的操作我们必须重新创建一个线程的对象,让新的对象执行新的线程。

2. 实现Runnable接口

2.1 实现步骤

  1. 创建一个实现了Runnable接口的类 。
  2. 实现类去实现Runnable中的抽象方法:run() 。
  3. 创建实现类的对象。
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 。
  5. 通过Thread类的对象调用start()。

2.2 与Thread相比

1.区别

  • 只创建了一个Runnable接口的实现类,每新建一个线程,只需要新创建一个Thread类的实例,并将Runnable的接口实现类作为参数传入即可。
  • 实现的方式没有类的单继承性的局限性。
  • 实现的方式更适合来处理多个线程有共享数据的情况。

2.联系

  • 实际上Thread类也是实现了Runnable接口,public class Thread implements Runnable。
  • 两种方式都要重写run()方法。

3. 实现Callable接口(JDK5.0新增)

3.1 实现步骤

  1. 创建一个实现Callable的实现类。
  2. 实现call方法,将此线程需要执行的操作声明在call()中。
  3. 创建Callable接口实现类的对象。
  4. 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象。
  5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()。

注:可以选择使用futureTask.get()方法获取Callable中的call方法的返回值。(get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值)

3.2 与Runnable接口相比

  • call()可以有返回值的。
  • call()可以抛出异常,被外面的操作捕获,获取异常的信息。
  • Callable是支持泛型的。
  • 需要借助FutureTask类,比如获取返回结果。

三、线程的常用方法

注:以下解释中如果方法被某个线程调用,则“当前线程”代表调用该方法的线程。如果方法无需调用,则当前方法在哪个线程中使用,“当前线程”就代表哪个线程。

  1. start():启动当前线程,调用当前线程的run()。
  2. run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中。
  3. currentThread():静态方法,返回执行当前代码的线程。
  4. getName():获取当前线程的名字。
  5. setName(String name):设置当前线程的名字。
  6. yield():释放当前cpu的执行权(只是有更大可能分配给其他线程,也有可能再分配回来)(会抛异常)。
  7. join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
  8. join(long n):在线程a中调用线程b的join(long n),此时线程a就进入阻塞状态,直到线程b完全执行完以后或者等待了指定时间n之后,线程a才结束阻塞状态。
  9. stop():已过时。当执行此方法时,强制结束当前线程。
  10. sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒,在指定的millitime毫秒时间内,当前的线程是阻塞状态,注意,睡眠结束后该线程未必会立刻执行,需要CPU进行分配。
  11. isAlive():判断当前线程是否存活。
  12. getPriority():获取线程的优先级
  13. setPriority(int p):设置线程的优先级
  14. interrupt():打断当前线程,有三种情况:① 打断sleep、wait、join的线程:当前线程处于阻塞状态,则该方法会让当前线程抛出异常,然后被捕获。当前线程中存在一个打断标记,如果当前线程曾经被interrput()打断过,则为true,否则为false。但是处于阻塞状态的线程被打断抛出异常后,会将该标志清空。② 打断正常运行的线程:打断后将当前线程的打断标记置为true,表示当前线程被打断过,但是当前线程是否进入阻塞,需要由当前线程决定。 ③ 打断park线程:当前线程处于阻塞状态,则该方法会将打断标记置为true,并直接唤醒该方法,且不会清空打断标志。当打断一次park的阻塞状态后,如果再运行park()方法,当前线程也不会阻塞了,此时如果设置打断标记为false,则再次调用park()方法会进入阻塞。
  15. isInterrupted():返回打断标记的值。
  16. interrupted():返回打断标记,并在之后设置打断标记为false
  17. setDaemon(true):设置当前线程为守护线程。Java中的线程还可以分为用户线程和守护线程,守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个典型的守护线程。守护线程不能单独运行,当JVM中没有其他用户线程,只有守护线程时,守护线程会自动销毁,JVM会退出。注意,应该在线程A启动前,设置线程A为守护线程,否则设置不成功。
  18. getState():获取当前线程的运行状态。

四、线程的生命周期

1. 基本概念

  • 线程的生命周期,就是线程生老病死的过程。

2. 六个状态

Java中的线程基础篇-线程基本概念_第5张图片
我们可以通过getState()方法获得线程的当前状态,线程状态是枚举类型,有以下几种状态:

  1. NEW(新建):创建了线程对象,但是还未调用start()方法。
  2. RUNNABLE(可运行):是一个复合状态,包含READY和RUNNING状态。
    • READY(准备):该线程准备执行,即调用了start()方法但是还未调用run()方法。可以通过调用run()方法来使其进入RUNNING状态。
    • RUNNING(运行):RUNNING表示该线程正在执行,即正在执行run()方法。可以通过调用yield()方法来使其进入READY状态。
  3. BLOCKED(阻塞):当前线程申请由其他线程占用的独占资源,则当前线程就会转入BLOCKED状态。此状态的线程不会占用CPU资源。当前线程获得了申请的资源,则当前线程可以结束BLOCKED状态,转换为RUNNABLE状态。
  4. WAITING(等待):① 当前线程A执行了A.wait(),则会将当前线程转换为WAITING状态,当其他线程(比如线程B)执行B.notify()方法或B.notifyAll()方法后,则会将当前线程转换为RUNNABLE状态;② 当前线程A执行了其他线程(比如线程B)的B.join()方法,则当前线程会进入WAINTING状态,等到执行了join()方法的线程(比如线程B)执行结束,当前线程会进入RUNNABLE状态;③ 在当前线程(比如线程A)中,执行了LockSupport工具类的park()方法,则当前线程A会进入WATING状态,等到其他线程(比如线程B)执行了LockSupport工具类的B.unpark(A)方法,则当前线程A会重新进入RUNNABLE状态。
  5. TIME_WAITING(超时等待):类似于WAINTING,但处于TIME_WAITING状态的线程不会无限的等待,如果线程没有在指定的时间范围内完成期望的操作,则该线程自动转换为RUNNABLE。① 当前线程A执行了A.sleep()方法,则线程A进入TIME_WAITING状态,等到达指定时间后,线程A重回RUNNABLE状态。② 当前线程A执行了带参数的,有时间限制的A.wait(long),或者其他线程(比如线程B)执行了B.join(long)方法,则进入TIMIE_WAITING状态,如果到达指定时间,或者其他线程(比如线程C)执行了C.notify()、C.notifyAll()、其他线程(比如线程B)执行完毕,则当前线程进入RUNNABLE状态;③ 在当前线程(比如线程A)中,执行了LockSupport工具类的parkNanos(long time)方法或者parkUntil(long time)方法,则当前线程A会进入TIME_WATING状态,等到其他线程(比如线程B)执行了LockSupport工具类的B.unpark(A)方法,或者到达指定时间,则当前线程A会重新进入RUNNABLE状态。
  6. TERMINATED(终止):线程结束,处于终止状态。

五、线程的优缺点

1. 优点

  • 提高系统的吞吐率:多线程编程可以使一个进程有多个并发的操作。
  • 提高响应性:web服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间。
  • 充分利用多核资源:通过多线程可以充分利用CPU资源。

2. 缺点

  • 线程安全问题:多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据的一致性问题,如脏数据(过期数据)、丢失数据更新。
  • 线程活性问题:由于程序自身缺陷或者资源稀缺,导致线程长期处于非RUNNABLE状态,致使资源的浪费。常见的活性故障包括死锁(互相争夺且皆不放手)、锁死(永不开锁)、活锁、饥饿(一个线程多次抢夺则其他线程抢夺不到)。
  • 上下文切换:处理器从执行一个线程切换到执行另外一个线程,需要性能消耗。
  • 可靠性:可能会有某一个线程问题导致JVM意外终止,此时其他线程也无法执行。

你可能感兴趣的:(JUC,java,面试)