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. 一个线程都有哪些基本属性和方法
- 线程有一些基本属性和方法
- 属性:
id
、name
、优先级、状态 - 方法:是否 daemon 线程、
sleep()
方法、yield()
方法、join()
方法、过时方法等
7. 怎样理解线程的 id
和 name
- 每个线程都有一个
id
和name
id
是一个递增的整数,每创建一个线程就加 1name
的默认值是 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
,它是一个枚举类型,有如下值:NEW
、RUNNABLE
、BLOCKED
、WAITING
、TIMED_WAITING
、TERMINATED
NEW
:没有调用start()
方法的线程的状态是NEW
RUNNABLE
:调用start()
方法后线程在执行run()
且没有阻塞时的状态是RUNNABLE
。不过,RUNNABLE
不代表 CPU 一定在执行该线程的代码,可能正在执行也可能在等待操作系统分配时间片,只是它没有在等待其他条件BLOCKED
、WAITING
、TIMED_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系统)上运行
- 进程执行开销大,但是能够很好的进行资源管理和保护。进程可以跨机器迁移
进程、线程的使用场景
- 对资源的管理和保护要求高,不限制开销和效率时,使用多进程
- 要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程