0%

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 中,优先级从 1 到 10,默认为 5(不是从 0 开始)
  • 相关方法是: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 数量的线程就是没有必要的,并不会加快程序的执行

20. 进程和线程的区别

  • 概念

    • 进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位
    • 线程:是进程的一个执行单元,是进程内的调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程
    • 一个程序至少一个进程,一个进程至少一个线程
  • 为什么会有线程

    • 每个进程都有自己的地址空间,即进程空间。在网络或多用户换机下,一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进
    • 线程的执行过程是线性的,尽管中间会发生中断或者暂停,但是进程所拥有的资源只为该线状执行过程服务,一旦发生线程切换,这些资源需要被保护起来
    • 进程分为单线程进程多线程进程,单线程进程宏观来看也是线性执行过程,微观上只有单一的执行过程。多线程进程宏观是线性的,微观上多个执行操作
    • 线程的改变只代表CPU的执行过程的改变,而没有发生进程所拥有的资源的变化
  • 区别

    • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间

    • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu 等,但是进程之间的资源是独立的

      • 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
      • 进程切换时,消耗的资源大、效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
    • 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制

    • 线程是处理器调度的基本单位,进程是系统资源分配的基本单位

    • 进程和线程都可以并发执行

  • 优缺点

    • 线程执行开销小,但是不利于资源的管理和保护。线程适合在SMP机器(双CPU系统)上运行
    • 进程执行开销大,但是能够很好的进行资源管理和保护。进程可以跨机器迁移
  • 进程、线程的使用场景

    • 资源的管理和保护要求高,不限制开销和效率时,使用多进程
    • 要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程
-------------------- 本文结束感谢您的阅读 --------------------