1:什么是线程和进程?
进程介绍:
进程是系统运行程序的基本单位,系统运行一个程序即是一个进程从创建,运行到消亡的过程。(一个进程一个端口号)
当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
线程介绍:
一个进程在其执行的过程中可以产生多个线程。
多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,
由于资源共享:线程之间作切换工作时, 代价较小;
2:在JVM的角度分析线程与进程的关系,区别及优缺点?
一个进程中可以有多个线程;
每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源
进程:相互独立,开销大,但资源有利于管理和保护
线程:相互影响,开销小,但资源不利于管理和保护
JVM下的线程.png
3:线程的组成部分介绍
1:私有的程序计数器:
1:字节码解释器通过改变程序计数器来依次读取指令,
从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
2:在多线程的情况下,程序计数器用于记录当前线程执行的位置,
从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
2:私有的虚拟机栈
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。
从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中 入栈和出栈的过程。
3:私有的本地方法栈
与虚拟机栈所发挥的作用非常相似,
共同点:
保证线程中的局部变量不被别的线程访问到;
区别:
虚拟机栈 为虚拟机执行 Java 方法 (也就是字节码)服务;
本地方法栈 为虚拟机使用到的 Native 方法服务。
4:堆与方法区:
1:堆和方法区是所有线程共享的资源,
2:堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存)
3:方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
4:并发与并行的区别?
并发: 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
并行:单位时间内,多个任务同时执行
5:为什么要使用多线程?
1:从计算机底层来说
1:线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。
2:多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
2:从当代互联网发展趋势看:
应对于现在百万到千万级的并发量,多线程并发编程正是开发高并发系统的基础;
6:多线程可能带来的问题?
上下文切换、 死锁、内存泄漏、 还有受限于硬件和软件的资源闲置问题。
7:什么是上下文切换?
一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用;
当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换会这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间;
上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
8:线程的生命周期和状态
1:线程创建后,将处于 NEW(新建) 状态,
2:调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。
3:可运行状态的线程等待获取获得了 CPU 时间片(timeslice)。
4:获取时间片后处于 RUNNING(运行) 状态。
操作系统隐藏 Java 虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态;
runable(ready 与 running).png
WAITING(等待):
进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态;
TIME_WAITING(超时等待) :
在waiting的状态上加上超时限制,如sleep(long millis) wait(long millis),在到达超时时间后,线程会回到运行状态;
BLOCKED(阻塞):
程调用同步方法时,在没有获取到锁的情况下进入;
TERMINATED(终止) :
run()方法之后进入
9:sleep()方法和wait()方法区别和共同点?
相同点:两者都可以暂停线程的执行。
区别:
1:sleep方法没有释放锁,而wait方法释放了锁 ;
2:sleep()作用于Thread wait()作用于Obejct;
3:sleep()方法执行完成后,线程会自动苏醒。(作用于线程)
wait()方法被调用后,不会自动苏醒,
需要别的线程调用同一个对象上的notify()或者notifyAll()方法。
4:sleep通常被用于暂停执行,Wait通常被用于线程间交互/通信。
10:什么是线程死锁?如何避免死锁?
线程 A 持有资源 2,线程 B 持有资源 1;
他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
死锁概念.png
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();
1:线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,
2:通过Thread.sleep(1000);让线程 A 休眠 1s ,等待线程B开始执行;
3:线程 B 通过 synchronized (resource2) 获得 resource2 的监视器锁,
4:线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,
5:最后这两个线程就会陷入互相等待的状态,这也就产生了死锁。
死锁的4个必要条件:
1:互斥条件:该资源任意一个时刻只由一个线程占用。(锁的核心 仅被独占)
2:请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
3:不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
4:循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
接触死锁:
请求与保持条件:一次性申请所有的资源
不剥夺条件:
占用部分资源的线程进一步申请其他资源时,
如果申请不到,可以主动释放它占有的资源。
循环等待条件:
靠按序申请资源来预防。
按某一顺序申请资源,释放资源则反序释放。
针对于上述问题如何破坏死锁:线程B修改为与线程A一样的代码(破坏等待条件)
11:为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
调用start方法方可新增并启动线程并使线程进入就绪状态,等待分配的时间片,就可以自动执行run方法了;
而直接调用run方法只是在主线程里执行 来调用 thread的一个普通方法;