什么是进程?什么是线程?
进程
是程序的一次执行过程,是系统运行程序的基本单位,进程是一个系统对一个程序从创建,运行到消亡的过程。
在Java中,我们启动main函数就是启动了一个JVM的进程,main函数所在线程就是这个进程中的主线程。
线程
是程序的一个更小的执行单位,一个进程在执行过程中可以产生多个线程。不同在与,同类的多个线程可以共享进程的堆
和方法区
资源,但每个线程有自己的程序计数器
、虚拟机栈
和本地方法栈
,系统在线程之间切换的代价要比进程小得多。
请简要描述线程与进程的关系,区别及优缺点?
从JVM的角度说明
一个进程中包括多个线程,线程可以共享进程的堆和方法区,但是每个线程都有自己的虚拟机栈,本地方法栈,程序计数器
线程是进程划分成的更小的运行单位,区别在与进程基本上是各自独立的,而同一进程的不同线程则可能会相互影响。线程开销更小,但是不利于资源的管理和保护;进程则相反。
程序计数器为什么是私有的?
程序计数器主要作用:
- 字节码解释器通过改变程序计数器的位置来读取指令,从而实现代码的流程控制。如:顺序执行、选择、循环、异常处理。
- 多线程环境下,程序计数器用于记录当前程序的运行到的位置,当从别的线程切换回来的时候才能从上次运行到的位置继续运行。
所以程序计数器私有主要是为了线程切换回来后,能从原来停止的位置正确恢复运行。
虚拟机栈和本地方法栈为什么是私有的?
虚拟机栈
:每个Java方法在执行的同时会创建一个栈帧用于存储局部变量表、函数返回地址和参数、划定栈帧范围的ebp和esp指针等信息。一个方法从被调用到执行完成,就对应着一个栈帧在Java虚拟机中入栈和出栈的过程。本地方法栈
:和虚拟机栈作用差不多,区别在与虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为Native方法用的。在HotSpot虚拟机中两者合二为一。
所以为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈就是线程私有的。
简单介绍下堆和方法区
堆和方法区都是线程共享的资源。
堆
是进程中最大的一块内存,主要用于存放新创建的对象,几乎所有的对象都在这里分配内存。
方法区
主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
说说并发和并行的区别?
- 并行:在单位时间内多个任务同时进程,一般在是多核CPU上出现,
- 并发:在同一段时间内,多个任务由CPU切换着处理,在某一瞬间是不一定同时处理多个任务的。例外就是当我们CPU使用了因特尔的超线程技术,一个内核被虚拟成2个逻辑内核,当两个任务分别用到CPU的不同运算资源时,比如一个任务计算整数另一个计算浮点数,这个时候就又可能是真的同时进行的。
为什么要使用多线程?
从总体上说:
- 从计算机底层的角度,线程是轻量级的进程,是程序执行的最小单位,线程切换的开销要远小于进程的切换,另外多核CPU时代意味着多个线程可以同时运行,再次减少了开销。
- 从当代互联网的发展趋势角度:现在的系统基本上就动辄百万千万级的并发,多线程技术就是高并发系统的基础,可以大大提高系统整体的性能和并发能力。
从计算机底层来说:
- 单核时代:如果我们只有一个线程,那请求进程IO就会阻塞我们整个进程,而CPU就会被闲置了,如果由多个线程,那我们可以在一个线程被IO阻塞的时候,用另一个线程继续使用CPU的运算能力,提高系统整体的资源利用效率。
- 多核时代:如果有多个核心,那多个线程可以映射不同核心上并行执行,在没发生资源争抢的情况下执行效率就会显著提高。
使用多线程可能会带来什么问题?
- 内存泄漏:ThreadLocal就可能会导致内存泄漏
- 死锁:两个线程互相占用对方需要的资源并互相等待其释放
- 线程不安全:多线程访问并修改临界资源
说说线程的生命周期和状态?
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行 |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其它线程作出一些特定动作(通知或中断) |
TIME_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
线程随着代码的执行在不同状态之间切换,Java线程状态变迁如下所示:
(文字描述省略,要用自己的话能描述出下面这副图)
什么是上下文切换?
线程在执行过程中有自己的运行条件和状态(也成上下文),比如程序计数器,栈帧的一些信息,出现如下情况的时候,线程会从占用CPU状态中退出:
- 调用了
sleep()
,wait()
,主动让出CPU - 时间片用完 ,操作系统要防止一个线程或者进程长时间占用CPU导致其它线程饿死
- 调用了阻塞类的系统中断,比如请求IO,线程被阻塞
- 被终止或结束运行
前三种都会发生线程切换,这意味这要保存线程的上下文,留着线程下次占用CPU的时候恢复现场,并加载下一个要占用CPU的线程上下文。
什么是线程死锁?如何避免死锁?
认识线程死锁
死锁就是多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放,而资源却又同时被对方占用锁住,此时线程就会无限期等待,程序就无法正常终止。
死锁的代码例子(代码来源于《并发编程之美》):
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
代码中先让线程A获取资源1的锁,如何sleep1秒让系统切换运行线程B,线程B获取资源2的锁,然后线程A和线程B都在尝试获取被对方占用着的资源,陷入互相等待的状态,因此也造成了死锁。
造成死锁的四个必要条件
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
如何预防死锁?
破坏死锁产生的必要条件即可
- 一次性申请所有资源
- 破环不剥夺条件:占用部分资源的线程进一步申请其它资源时,如果申请不到,可以主动释放资源。
- 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件条件。
如果避免死锁?
避免死锁就是在资源分配时,借助算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态指的是系统能够按照某种进程推进顺序(P1、P2、P3…..Pn)来为每个进程分配所需资源,直到满足每个进程对资源的最大需求,使每个进程都可顺利完成。称<P1、P2、P3…..Pn>序列为安全序列。 上述代码线程B的获取资源顺序改为资源1→资源2就可以解决死锁,因为线程A首先获取了资源1,线程B再去获取就得不到了,就阻塞在这里,然后资源2也不会被它获取,而是被线程A顺利获取。线程A使用完后释放资源1和2线程B也可以正常获取,这就破坏了循环等待的条件,避免了死锁。
说说sleep()方法和wait()方法区别和共同点?
共同点:两者都可以用于暂停线程的执行。
两者的主要区别在于:
sleep()
方法不会释放锁,而wait()
方法释放了锁。wait()
方法通常用于线程间交互/通信,sleep()
主要用于暂停线程的执行wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()
或者notifyAll()
方法。sleep()方法执行完成之后线程会自动苏醒。或者可以用wait(long timeout)
超时后线程会自动苏醒.
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
new一个Thread进入新建状态,然后调用start()
方法就会启动这个线程进入就绪状态,当分配到时间片就可以开始执行了。start()会执行线程的一些准备工作,然后就会自动执行run()方法中的内容,这就是真正的多线程工作。
但是如果直接执行run()方法,会把run()当成main线程下的一个普通方法进行执行,而不会在一个新的线程中去执行它,所以这并不是多线程工作。