Java 并发基础知识(一):线程的基本概念

  1. 线程的概念?
    答:线程表示一条单独的执行流,它有自己的程序执行计数器,有自己的栈

  2. Java 中创建线程的方式?
    答:

    • 继承 Thread:继承 Thread 并重写其 run() 方法来实现一个子线程,再调用这个子线程对象的 start() 方法启动该子线程。
    • 实现 Runnable:实现 Runnable 接口并实现 run() 方法,创建一个 Thread 子线程对象,传递一个 Runnable 对象,调用 Thread 对象的 start() 方法启动该子线程。
  3. 为什么调用的是 start() 方法,执行的却是 run() 方法?
    答:

    • start() 表示启动该线程,使其成为一条单独的执行流,操作系统会分配线程相关的资源,每个线程会有单独的程序执行计数器和栈,操作系统会把这个线程作为一个独立的个体进行调度,分配时间片让它执行。
    • 执行的起点就是 run() 方法
  4. 如果不调用 start() 而直接调用 run() 方法呢?怎样确认代码是在哪个线程中执行的呢?
    答:

    • 这时,并不会启动一条单独的执行流,run() 方法的代码依然是在 main() 线程中执行,run() 方法只是 main() 方法调用的一个普通方法。
    • Thread 类有一个静态方法 currentThread(),返回当前执行的线程对象:public static native Thread currentThread()
  5. 线程与 CPU 之间的关系是?
    答:

    • 调用 start() 方法后就有了多条执行流,多条执行流并发执行,操作系统负责调度。
    • 在单 CPU 的机器上,同一时刻只能有一个线程在执行;在多 CPU 的机器上,同一时刻可以有多个线程同时执行
    • 但操作系统给我们屏蔽了这种差异,给程序员的感觉就是多个线程并发执行,但哪条语句先执行哪条语句后执行是不一定的。当所有线程都执行完毕的时候,程序退出
  6. 一个线程都有哪些基本属性和方法?
    答:线程有一些基本属性和方法,包括:idname优先级状态是否 daemon 线程sleep() 方法yield() 方法join() 方法过时方法等。

  7. 怎样理解线程的 idname
    答:

    • 每个线程都有一个 idname
    • id 是一个递增的整数,每创建一个线程就加 1
    • name 的默认值是 Thread- 后跟一个编号
    • name 可以在 Thread 的构造方法中进行指定,也可以在 setName() 方法中进行设置。
    • Thread 设置一个友好的名字,可以方便调试。
  8. 怎样理解线程的优先级?
    答:

    • 线程有一个优先级的概念,Java 中,优先级从 110,默认为 5
    • 相关方法是:public final void setPriority(int newPriority)/public fianl int getPriority()
    • 这个优先级会被映射到操作系统中线程的优先级。不过,因为操作系统各不相同,不一定都是 10 个优先级,Java 中不同的优先级可能会被映射到操作系统中相同的优先级
    • 优先级对操作系统而言主要是一种建议和提示,而非强制。简单说,在编程中,不要过于依赖优先级
  9. 怎样理解线程的状态?一个线程有哪些状态?
    答:

    • 线程有一个状态的概念,Thread 有一个方法用于获取线程的状态:public State getState()
    • 返回值类型为:Thread.State,它是一个枚举类型,有如下值:NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED

      • NEW:没有调用 start() 方法的线程的状态是 NEW
      • RUNNABLE:调用 start() 方法后线程在执行 run() 且没有阻塞时的状态是 RUNNABLE。不过,RUNNABLE 不代表 CPU 一定在执行该线程的代码,可能正在执行也可能在等待操作系统分配时间片,只是它没有在等待其他条件。
      • BLOCKEDWAITINGTIMED_WAITING:都表示线程被阻塞了,在等待一些条件。
      • TERMINATED:线程运行结束后状态是 TERMINATED
      • Thread 还有一个方法,返回线程是否活着:public final native boolean isAlive()。线程被启动后,run() 方法运行结束前,返回值都是 true
  10. 怎样理解一个线程是否是 daemon 线程?
    答:

    • Thread 有一个是否 daemon 线程的属性,相关方法是:public final void setDaemon(boolean on)/public final boolean isDaemon()
    • 启动线程会启动一条单独的执行流,整个程序只有在所有线程都结束的时候才退出。daemon 线程是个例外,当整个线程剩下的都是 daemon 线程的时候,程序就会退出
    • daemon 线程的作用是:它一般是其他线程的辅助线程,在它辅助的主线程退出的时候,它就没有存在的意义了。
    • 即使运行一个最简单的程序,实际上,Java 也会创建多个线程。除了 main 线程外,至少还有一个负责垃圾回收的线程,这个线程就是 daemon 线程,在 main 线程结束的时候,垃圾回收线程也会退出。
  11. 谈谈你对线程的 sleep() 方法的理解?
    答:

    • 作用:sleep()Thread 的一个静态方法,调用该方法会让当前线程睡眠指定的时间,单位是毫秒:public static native void sleep(long millis) throws InterruptedException
    • 原理:睡眠期间,该线程会让出 CPU,但睡眠时间不一定是确切的给定毫秒数,可能有一定偏差,偏差与系统定时器和操作系统调度器的准确度和精度有关
    • 中断:睡眠期间,线程可以被中断,如果被中断,sleep() 会抛出 InterruptedException 异常。
  12. 谈谈你对线程的 yield() 方法的理解?
    答:

    • yield() 也是 Thread 的一个静态方法:public static native void yield()
    • 调用该方法,是告诉操作系统的调度器,我现在不着急占用 CPU,你可以让其他线程先运行
    • 不过,这对调度器也仅仅是建议,调度器如何处理是不一定的,它可能完全忽略该调用
  13. 谈谈你对线程的 join() 方法的理解?
    答:

    • 作用:join() 方法可以让调用 join() 的线程等待该线程结束(相当于把子线程放到主线程中串行执行,更符合 join 单词的本意)join() 方法的声明为:public final void join() throws InterruptedException
    • 中断:在等待线程结束的过程中,这个等待可能被中断,如果被中断,会抛出 InterruptedException 异常。
    • 扩展:join() 方法还有一个变体,可以限定等待的最长时间,单位是毫秒,如果是 0,表示无限期等待public final synchronized void join() throws InterruptedException
  14. Thread 类中的过时方法有哪些?
    答:

    • Thread 类中还有一些看上去可以控制线程生命周期的方法,如:public final void stop()/public final void suspend()/public final void resume()
    • 这些方法因为各种原因已被标记为了过时,不应该在程序中使用它们
  15. 线程之间共享内存导致可能存在的问题有哪些?
    答:每个线程表示一条单独的执行流,有自己的程序计数器,有自己的栈。但是,线程之间可以共享内存,它们可以访问和操作相同的对象。这就导致可能出现一些意料之外的结果,包括:

    • 竞态条件(race condition
    • 内存可见性问题
  16. 什么是竞态条件(race condition)?出现竞态条件的原因是?怎样解决?
    答:

    • 概念:当多个线程访问和操作同一个对象时,最终执行结果与时序有关,可能正确也可能不正确
    • 原因:很多操作并不是原子操作。比如,counter++,它分为三个步骤:

      • counter 的当前值。
      • 在当前值基础上加 1
      • 将新值重新赋值给 counter
    • 解决:

      • 使用 synchronized 关键字
      • 使用显示锁
      • 使用原子变量
  17. 什么是内存可见性问题?出现内存可见性问题的原因是?
    答:

    • 概念:多个线程可以共享访问和操作相同的变量,但一个线程对一个共享变量的修改,另一个线程不一定马上就能看到,甚至永远也看不到
    • 原因:在计算机系统中,除了内存,数据还会被缓存在 CPU寄存器以及各级缓存中,当访问一个变量时,可能直接从寄存器或 CPU 缓存中获取,而不一定到内存中去取;当修改一个变量时,也可能是先写到缓存中,稍后才会同步更新到内存中。在单线程的程序中,这一般不是问题,但在多线程的程序中,尤其是在有多 CPU 的情况下,这就是严重的问题。一个线程对内存的修改,另一个线程看不到,一是修改没有及时同步到内存,二是另一个线程根本就没从内存读
    • 解决:

      • 使用 volatile 关键字
      • 使用 synchronized 关键字
      • 使用显示锁同步
  18. 多线程的优点?
    答:

    • 充分利用多 CPU 的计算能力。单线程只能利用一个 CPU,使用多线程可以利用多 CPU 的计算能力。
    • 充分利用硬件资源CPU 和硬盘、网络是可以同时工作的,一个线程在等待网络 IO 的同时,另一个线程完全可以利用 CPU,对于多个独立的网络请求,完全可以使用多个线程同时请求。
    • 在用户界面(GUI)应用程序中,保持程序的响应性。界面和后台任务通常是不同的线程,否则,如果所有事情都是一个线程来执行,当执行一个很慢的任务时,整个界面将停止响应,也无法取消该任务。
    • 简化建模及 IO 处理。比如,在服务器应用程序中,对每个用户请求使用一个单独的线程进行处理,相比使用一个线程,处理来自各种用户的各种请求,以及各种网络和文件 IO 事件,建模和编写线程要容易得多。
  19. 线程的成本?
    答:

    • 线程是有成本的,创建线程需要消耗操作系统的资源,操作系统会为每个线程创建必要的数据结构、栈、程序计数器等,创建也需要一定的时间
    • 线程调度和切换也是有成本的,当有大量可运行线程的时候,操作系统会忙于调度,为一个线程分配一段时间,执行完后,再让另一个线程执行;一个线程被切换出去后,操作系统需要保存它的当前上下文状态到内存,上下文状态包括当前 CPU 寄存器的值、程序计数器的值等,而一个线程被切换回来后,操作系统需要恢复它原来的上下文状态,整个过程称为上下文切换这个切换不仅耗时,而且使 CPU 中的很多缓存失效
    • 成本是相对而言的,如果线程中实际执行的事情比较多,这些成本是可以接受的。
    • 另外,如果执行的任务都是 CPU 密集型的,即主要消耗的都是 CPU,那创建超过 CPU 数量的线程就是没有必要的,并不会加快程序的执行。