线程与进程的区别

  • 进程是操作系统 分配资源 的最小单元,线程是操作系统 调度 的最小单元,也可以说是进程里的一个执行单元
  • 一个程序至少有一个进程,一个进程至少有一个线程

线程的生命周期

  • 新建【NEW】:当程序使用 new 关键字创建了一个 Thread 对象后,该线程就处于新建状态,此时的 JVM 为其分配内存,并且初始化其成员变量的值;例 Thread t1 = new Thread()
  • 就绪状态/可运行状态【RUNNABLE】:当线程对象调用 start() 方法后,该线程就处于可运行状态,此时的 JVM 会为其创建方法调用栈和程序计数器,等获取到 CPU 后执行
  • 运行状态【RUNNING】: Thread 类里面没有这个状态 在就绪状态的线程获取到了 CPU 的使用权时候开始执行 run() 方法,该线程就处于运行状态
  • 阻塞状态【BLOCKED】:线程因为某种原因放弃了 CPU 的使用权,也让出了 cpu 时间片,暂时的停止运行直到线程再次获取到 CPU 的使用权,才有机会再次获得 CPU 的时间片转到运行状态。在同步阻塞的情况下,在获取到对象的同步锁时,如果该同步锁被其他的线程所占用,JVM 会把该线程放入到锁池中,当线程获取到锁的时,线程重新转为可运行状态
  • 等待状态【WAITING】:运行状态的线程执行 wait() 方法后, JVM 会把该线程放入等待队列
  • 限时等待【TIMED_WAITING】:运行状态的线程执行 sleep()/join() 方法后进入到限时等待状态,当 sleep() 超时 / join() 等待线程终止后,线程重新转为可运行状态
  • 终止【TERMINATED】:

Java 面试题总结之并发编程篇

Java 面试题总结之并发编程篇

线程的创建方式

  • 继承 Thread 类
  • 实现 Runnable 接口
  • 实现 Callable 接口,需要实现的是 call() 方法
  • 使用线程池

如何停止正在运行的线程

  • 正常地运行结束

  • 使用退出标志退出线程,使用 boolean 类型的标志,并通过这个标志的为 false/true 来控制 while 循环是否退出,同时需要使用 volatile 关键字进行修饰,这个关键的目的是使标志同步,并且修改标志位的值时让其他线程可见

  • interrupt() 结束线程

    • 如果一个线程由于等待某些事件的发生而阻塞【例:Thread.sleep()/Thread.join()/IO 等待时】,当调用 Thread.interrupt() 方法时,会抛出 InterruptException 异常,通过代码捕获该异常,然后 break 跳出循环。

    • 如果一个线程未处于阻塞状态:使用 isInterrupted() 方法判断线程的中断标志来退出循环,当使用 interrupt() 方法的时候中断标志会设置为 true,类似自定义标志退出循环

  • stop() 方法停止线程【不推荐】: Thread 类里面也提供了 stop() 方法来停止线程,但是该方法是很危险的,就像是突然关闭计算机,可能会产生不可预料的后果。在调用 Thread#stop() 方法之后,创建子线程的线程就会抛出 ThreadDeathError 的错误,并且会释放子线程的所有锁,一般情况下对加锁的代码都是为了保证数据的一致性,突然释放所有的锁可能被保护的数据出现不一致性。

volatile 关键字的作用

  • 变量可见性: 保证了该变量对所有的线程是可见的。【例:线程 one 修改了变量的值,那么其他线程都可以获取到最新值】

  • 禁止指令重排序

  • Java 语言提供的一种稍弱的同步机制, volatile 关键字 用于确保将变量的更新操作通知给其他的线程;volatile 具备两种特性,它不会被缓存在寄存器【CPU 缓存】,所以在读取 volatile 关键字修饰的变量时总是会从主内存中返回最新值。

  • 对非 volatile 关键字修饰的变量进行读写操作的时候,每个线程先从主内存拷贝变量的值到 CPU cache 中,假如有多个 CPU,每个线程可能在不同的 CPU 上处理,这意味着每个线程可以拷贝到不同的 CPU 缓存。

  • 使用 volatile 关键字修饰的变量, JVM 跳过了 CPU 缓存,保证了每次读写都是对主内存进行操作。

  • volatile 关键字修饰的变量的单次 read/write 操作是可以保证原子性的,比如 long/double/boolean 类型的变量,因为 long/double/boolean 都是 64 位,对于这两种数据类型的读是分为两个部分,第一次读取的是 32 位,然后再读取剩下的 32 位,在这个过程中不是原子操作。但 JVM 中的 volatile 关键字修饰的 long/double/boolean 后, 其读写就是原子操作。但是对于 自增/自减 并不能保证原子性,因为它们本质上是读和写两次操作。

wait() 方法与 sleep() 方法的区别

  • sleep() 方法是线程类 Thread 的静态方法,调用此方法设置休眠时间,会让出 CPU 的使用权给其他线程,但还是持有对象锁,当休眠时间结束时,该线程会回到就绪状态【RUNNABLE
  • wait() 方法是 Object 类中的方法,调用 Object#wait() 方法后会导致当前线程放弃对象锁(线程暂停执行),也会让出 CPU 的使用权给其他线程,进入到等待池,只有针对此对象调用 Object#notify() 方法后该线程才准备获取对象锁进入运行状态【RUNNING

为什么需要把 wait()/notify() 放在 synchronized 代码块里?

  • JAVA 的 API 强制要求放置在同步块中,如果不这样做会抛出 IllegalMonitorStateException 异常,还有一个原因是为了避免 Object#wait()、Object#notify() 之间产生竞态条件

start() 与 run() 方法的区别

  • start() 方法是真正启动一个线程,实现了多线程运行,调用了 start() 方法之后,此线程就进入到了就绪状态【RUNNABLE
  • run() 方法里称之为线程体,包含了要执行这个线程的内容,线程就进入到了运行状态【RUNNING】,开始运行 run() 方法中的代码

Java 锁

相关文章: