文章目录
1、并行 跟 并发 的区别
从操作系统的角度来看,线程是CPU分配的最小单位。
- 并行 就是同一时刻,两个线程都在执行。这就要求有两个CPU去分别执行两个线程。
- 并发 就是同一时刻,只有一个执行,但是一个时间段内,两个线程都执行了。并发的实现依赖于CPU切换线程,因为切换的时间特别短,所以基本对于用户是无 感知的。
就好像我们去食堂打饭,并行 就是我们在多个窗口排队,几个阿姨同时打菜;并发 就是我们挤在一个窗口,阿姨给这个打一勺,又手忙脚乱地给那个打一勺。
2、进程 和 线程 的区别
- 进程:进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
- 线程:线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
操作系统在分配资源时是把资源分配给进程的,但是 CPU 资源比较特殊,它是被分配到线程的,因为真正要占用CPU运行的是线程,所以也说线程是CPU分配的基本单位。
比如在Java中,当我们启动 main 函数其实就启动了一个JVM进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
一个进程中有多个线程,多个线程共用进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈。
3、线程 的创建方式
Java中创建线程主要有三种方式:继承Thread类、实现Runnable接口、实现 Callable接口。
详细内容可参考 创建线程的三种方式。
● 为什么调用
start()
方法时会执行run()
方法,那怎么不直接调用run()
方法?JVM执行
start()
方法,会先创建一条线程,由创建出来的新线程去执行thread的run()
方法,这才起到多线程的效果。如果直接调用Thread的run()
方 法,那么run()
方法还是运行在主线程中,相当于顺序执行,就起不到多线程的效果。
● Thread 和 Runnable 的区别
Thread类适合直接创建新线程,而Runnable接口适合定义线程要执行的任务,并可以 与其他接口组合使用。
特点 | Thread | Runnable |
---|---|---|
继承关系 | 类 | 接口 |
代码复用 | 不方便,每个线程需要创建新实例 | 方便,多个线程可以共享同一实例 |
灵活性 | 相对较低,只能继承Thread类 | 相对较高,可以与其他接口组合 |
可控性 | 相对较低,需要手动管理线程 | 相对较高,可以通过线程池管理 |
4、线程常用调度方法
详细内容可参考 线程常用方法。
5、线程六大状态
详细内容可参考 线程六大状态。
状态 | 说明 |
---|---|
NEW | 初始状态:线程被创建,但还没有调用start()方法 |
RUNNABLE | 运行状态:Java线程将操作系统中的就绪和运行两种状态笼 统的称作“运行” |
BLOCKED | 阻塞状态:表示线程阻塞于锁 |
WAITING | 等待状态:表示线程进入等待状态,进入该状态表示当前线 程需要等待其他线程做出一些特定动作(通知或中断) |
TIME_WAITING | 超时等待状态:该状态不同于 WAITIND,它是可以在指定 的时间自行返回的 |
TERMINATED | 终止状态:表示当前线程已经执行完毕 |
● BLOCKED 和 WAITING 的区别
- BLOCKED 状态表示线程被阻塞,无法继续执行,通常是因为等待获取锁。
- WAITING 状态表示线程正在等待其他线程的通知,通常是因为调用了wait()方法、 join()方法或LockSupport.park()方法。
这两种状态的区别在于阻塞状态是等待获取锁,而等待状态是等待其他线程的通知。
● WAITING 和 TERMINATED 的区别
- WAITING 状态表示线程在等待其他线程的通知或中断
- TERMINATED 状态表示线程已经执行完毕,不再执行任何代码。
WAITING 状态是暂时的,可以再次进入 RUNNABLE 状态,而 TERMINATED 状态是永久的,线程不会再进入任何状态。
6、线程上下文切换
使用多线程的目的是为了充分利用CPU,但是我们知道,并发其实是一个CPU来应付多个线程。
为了让用户感觉多个线程是在同时执行的,CPU 资源的分配采用了时间片轮转,也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是上下文切换。
7、守护线程
详细内容可参考 守护线程 deamon。
Java中的线程分为两类,分别为 **daemon 线程(守护线程)**和 user 线程(用户线 程)。
在JVM 启动时会调用 main 函数,main函数所在的钱程就是一个用户线程。其实在 JVM 内部同时还启动了很多守护线程, 比如垃圾回收线程。
● 那么守护线程和用户线程有什么区别?
区别之一是当最后一个非守护线程束时, JVM会正常退出,而不管当前是否存在守护线程,也就是说守护线程是否结束并不 影响 JVM退出。换而言之,只要有一个用户线程还没结束,正常情况下JVM就不会退出。
8、线程间的通信方式
● volatile 和 synchronized 关键字
关键字 volatile
可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
关键字 synchronized
可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
● 等待 / 通知机制
可以通过Java内置的 等待/通知机制(wait()
/notify()
)实现一个线程修改一个对象的值,而另一个线程感知到了变化,然后进行相应的操作。
● 管道输入 / 输出流
管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。
管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream
、 PipedInputStream
、 PipedReader
和 PipedWriter
,前两种面向字节,而后两种面向字符。
● 使用 Thread.join()
如果一个线程A执行了 thread.join()
语句,其含义是:当前线程A等待thread线程终止之后才从 thread.join()
返回。。线程Thread除了提供 join()
方法之外,还提供了 join(long millis)
和 join(long millis,int nanos)
两个具备超时特性的方法。
● 使用 ThreadLocal
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。
这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。 可以通过 set(T)
方法来设置一个值,在当前线程下再通过 get()
方法获取到原先设置的值。
详细内容可参考 并发(二、ThreadLocal)。
9、死锁(补充)
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现 象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
● 死锁的产生必须具备以下四个条件:
互斥条件
:线程对己经获取到的资源进行它性使用,即该资源同时只由一个线程占用。如果此时还有其它线程请求获取获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。请求并持有条件
:一个线程己经持有了至少一个资源,但又提出了新的资源请求,而新资源己被其它线程占有,所以当前线程会被阻塞,但阻塞的同时并不 释放自己已经获取的资源。不可剥夺条件
:线程获取到的资源在自己使用完之前不能被其它线程抢占,只有在自己使用完毕后才由自己释放该资源。环路等待条件
:发生死锁时,必然存在一个线程所需资源的环形链,T1等T2,T2等T3,T3等…Tn等T1。
● 如何避免死锁?
至少破坏死锁发生的一个条件!
其中,互斥条件我们没有办法破坏,因为用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的。
- 请求并持有:可以一次性请求所有的资源。
- 不可剥夺:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
- 环路等待:可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后就不存在环路了。
● 死锁排查
可以使用 JDK 自带的命令行工具排查:
- 使用 jps 查找运行的 Java 进程:
jps -l
- 使用 jstack 查看线程堆栈信息:
jstack -l 进程id
还可以利用图形化工具,比如 JConsole。