Java并发编程面试题,Java并发面试
本文给大家总结了Java并发基础的常见面试问题,有一定的参考价值。有需要的朋友可以参考一下,希望对你有帮助。
如何解决写爬虫IP受阻的问题?立即使用。
1. 什么是线程和进程?
1.1. 何为进程?
进程是程序的一个执行过程,是系统运行程序的基本单元,所以进程是动态的。在一个系统中运行一个程序,是一个从创建、运行到消亡的过程。
在Java中,当我们启动主函数时,实际上是启动了一个JVM进程,主函数所在的线程就是这个进程中的一个线程,也叫主线程。
如下图所示,通过查看windows中的任务管理器,我们可以清楚地看到窗口中当前运行的进程(的运行。exe文件)。
1.2. 何为线程?
类似于线程进程,但线程是比进程更小的执行单元。一个进程在执行过程中可以生成多个线程。与进程不同,同类的多个线程共享进程的堆和方法区资源,但每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。因此,当系统生成一个线程或在线程间切换时,负担要比进程小得多。因此,线程也是
Java本质上是一个多线程程序。我们可以通过JMX来看一个普通Java程序的线程。代码如下。
公共类多线程{
公共静态void main(String[] args) {
//获取Java线程管理MXBean
ThreadMXBean ThreadMXBean=management factory . getthreadmxbean();
//不需要获取同步监视器和同步器信息,只需要获取线程和线程栈信息。
ThreadInfo[]threadInfos=threadmxbean . dumpallthreads(false,false);
//遍历线程信息,只打印线程ID和线程名称信息
for(ThreadInfo ThreadInfo:threadInfos){
system . out . println([ threadinfo . getthreadid()] threadinfo . getthreadname());
}
}
}上面的程序输出如下(输出内容可能不一样,所以不用太在意下面每个线程的功能,只知道主线程执行main方法就行):
[5]附加监听器//添加事件
[4]信号分配器//将处理信号的线程分配给JVM
[3]终结器//调用对象的终结方法的线程
[2]引用处理程序//清除引用线程
[1]main//主线程,程序入口从上面的输出可以看出:一个 Java 程序的运行是 main 线程和多个其他线程同时运行。
2. 请简要描述线程与进程的关系,区别及优缺点?
从 JVM 角度说进程和线程之间的关系
2.1. 图解进程和线程的关系
下图是Java内存区。通过下图,我们可以从JVM的角度来谈谈线程和进程之间的关系。如果你对Java内存区(运行时数据区)了解不多,可以看看这篇文章:《可能是把 Java 内存区域讲的最清楚的一篇文章》。
p align=居中
src= 3359 my-blog-to-use . OSS-cn-Beijing . aliyuncs . com/2019-3/JVM runtime data area . png width= 600 px /
/p
从上图可以看出,一个进程中可以有多个线程。多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程都有自己的程序计数器、虚拟机栈和本地方法栈。
总结:线程是进程被划分成的较小的运行单元。线程和进程最大的区别是,基本上每个进程都是独立的,但每个线程不一定,因为同一个进程中的线程很可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而过程正好相反。
下面是这个知识点的延伸!
我们来考虑这个问题:为什么程序计数器、虚拟机栈和本地方法栈线程是私有的?为什么堆和方法区域是线程共享的?
2.2. 程序计数器为什么是私有的?
程序计数器主要有以下两个功能:
字节解释器通过改变程序计数器依次读取指令,从而实现代码的顺序执行、选择、循环、异常处理等流程控制。在多线程的情况下,程序计数器用来记录当前线程的执行位置,这样当线程切换回来的时候,就可以知道线程上次运行到哪里了。注意,如果执行原生方法,程序计数器记录的是未定义的地址,只有执行Java代码时,程序计数器才记录下一条指令的地址。
所以节目计数器是私人的主要为线程切换后能恢复到正确的执行位置。
2.3. 虚拟机栈和本地方法栈为什么是私有的?
虚拟机栈:每个Java方法执行时,都会创建一个堆栈框架,用来存储局部变量表、操作数堆栈、常量池引用等信息。从调用一个方法到执行完成的过程,对应的是一个堆栈帧在Java虚拟机堆栈中被堆栈和弹出的过程。本地方法栈:和虚拟机栈的作用非常相似,区别在于虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在热点虚拟机中是和Java虚拟机栈结合在一起的。因此,对于保证线程中的局部变量不被别的线程访问到,虚拟机堆栈和本地方法堆栈是线程私有的。
2.4. 一句话简单了解堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用来存储新创建的对象(这里所有对象都是分配内存的),方法区主要用来存储加载的类信息、常量、静态变量、即时编译器编译的代码等数据。
3. 说说并发与并行的区别?
并发:同时进行多个任务(单位时间内不一定同时进行);并行:单位时间,多个任务同时执行。4. 为什么要使用多线程呢?
一般来说,首先:
从计算机底层来说:线程可以比作一个轻量级的进程,是程序执行的最小单位。线程间切换和调度的开销远远小于进程的开销。另外,多核CPU时代意味着多个线程可以同时运行,减少了线程上下文切换的开销。从当代互联网发展趋势来说:现在的系统总是要求百万级甚至千万级的并发,多线程并发编程是开发高并发系统的基础。利用好多线程机制可以大大提高系统的整体并发性和性能。深入电脑底层讨论:
单核时代的单核时代:多线程主要是提高CPU和IO设备的综合利用率。比如只有一个线程时,会导致CPU计算,IO设备空闲;当IO操作正在进行时,CPU是空闲的。我们可以简单的说,目前两者的利用率都在50%左右。但是有两个线程的时候就不一样了。当一个线程进行CPU计算时,另一个线程可以进行IO操作,这样两个线程的利用率在理想情况下可以达到100%。多核时代:多核时代多线程主要是提高CPU利用率。比如我们要计算一个复杂的任务,如果只用一个线程,那么只会利用一个CPU核,而创建多个线程就可以让多个CPU核被利用,从而提高CPU的利用率。5. 使用多线程可能带来什么问题?
并发编程的目的是提高程序的执行效率和速度,但是并发编程并不能总是提高程序的运行速度,并发编程可能会遇到很多问题,比如内存泄漏、上下文切换、死锁、软硬件限制的资源闲置等。
6. 说说线程的生命周期和状态?
Java线程在其运行生命周期的指定时刻只能处于以下六种不同状态之一(Source 《Java 并发编程艺术》第4.1.4节)。
一个线程在其生命周期中并不是固定在某个状态,而是随着代码的执行在不同的状态之间切换。Java线程状态转换如下图所示(source 《Java 并发编程艺术》第4.1.4节):
从上图可以看出,线程创建后,会处于NEW(新建)的状态。调用start()方法后开始运行,此时线程处于READY(可运行)的状态。处于runnable状态的线程在获得CPU时间片后处于RUNNING(运行)的状态。
当线程执行wait()方法时,线程进入WAITING(等待)状态。进入等待状态的线程依赖于其他线程返回运行状态的通知,TIME_WAITING(超时等待)状态相当于在等待状态的基础上增加了超时限制。例如,可以通过sleep(长毫秒)方法或wait(长毫秒)方法将Java线程置于定时等待状态。当超时时间到达时,Java线程将返回到可运行状态。当线程调用同步方法时,线程将进入BLOCKED(阻塞)状态,而不获取锁。线程执行Runnable run()方法后,将进入TERMINATED(终止)状态。
7. 什么是上下文切换?
多线程编程中线程的数量一般大于CPU核的数量,一个CPU核在任何时候只能被一个线程使用。为了使这些线程得到有效执行,CPU采用了给每个线程分配时间片并轮换的策略。当一个线程的时间片用完时,它将再次准备好供其他线程使用。这个过程是一个上下文切换。
综上所述,当前任务在CPU时间片后切换到另一个任务前会保存状态,以便下次切换回该任务时可以再次加载该任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当多的处理器时间,每一次切换都以每秒几十次或几百次的速度花费纳秒级的时间。因此,上下文切换意味着消耗系统大量的CPU时间。其实可能是操作系统中最耗时的操作了。
与其他操作系统(包括其他类似Unix的系统)相比,Linux有很多优点,其中之一就是切换上下文和模式所需的时间非常少。
8. 什么是线程死锁?如何避免死锁?
8.1. 认识线程死锁
多个线程同时被阻塞,其中一个或所有线程都在等待一个资源被释放。因为线程被无限期阻塞,所以程序不可能正常终止。
如下图所示,线程A持有资源2,线程B持有资源1。它们都想同时申请对方的资源,所以这两个线程会互相等待,进入死锁。
下面是一个说明线程死锁的例子。代码模拟上图中的死锁(代码从《并发编程之美》):
公共类DeadLockDemo {
私有静态对象resource 1=new Object();//资源1
私有静态对象resource 2=new Object();//资源2
公共静态void main(String[] args) {
新线程(()- {
同步(资源1) {
system . out . println(thread . currentthread() get resource 1 );
尝试{
thread . sleep(1000);
} catch (InterruptedException e) {
e . printstacktrace();
}
system . out . println(thread . current thread()等待获取资源2 );
同步(资源2) {
system . out . println(thread . currentthread() get resource 2 );
}
}
},‘线程1’)。start();
新线程(()- {
同步(资源2) {
system . out . println(thread . currentthread() get resource 2 );
尝试{
thread . sleep(1000);
} catch (InterruptedException e) {
e . printstacktrace();
}
system . out . println(thread . current thread()等待获取资源1 );
同步(资源1) {
system . out . println(thread . currentthread() get resource 1 );
}
}
},‘线程2’)。start();
}
}输出
Thread[ thread 1,5,main]获取资源1
线程[线程2,5,main]获取资源2
线程[线程1,5,main]正在等待获取资源2
线程[ thread 2,5,2,5,main]等待get resource1线程A通过synchronized (resource1)获得resource1的监视器锁,然后通过Thread . sleep(1000);让线程A休眠1s,以便让线程B被执行,然后获得资源2的监视器锁。当线程A和线程B处于休眠状态时,都开始互相请求对方的资源,然后两个线程就会处于互相等待的状态,导致死锁。上面的例子满足了死锁的四个必要条件。
学过操作系统的朋友都知道,死锁必须满足以下四个条件:
互斥条件:这个资源在任何时候都只被一个线程占用。请求和保持条件:当一个进程被请求资源阻塞时,它保持所获得的资源。非剥夺条件:一个线程获得的资源在用完之前不能被其他线程强行剥夺,只有在自己用完之后才会释放资源。循环等待条件:几个进程之间形成循环等待资源关系。8.2. 如何避免线程死锁?
我们要做的就是打破导致死锁的四个条件中的一个。
破坏互斥条件
我们不能打破这个条件,因为我们使用锁使它们互斥(关键资源需要互斥访问)。
破坏请求与保持条件
一次性申请所有资源。
破坏不剥夺条件
当占用部分资源的线程进一步申请其他资源时,如果申请失败,可以主动释放所占用的资源。
破坏循环等待条件
通过按顺序申请资源来防止。按照一定的顺序申请资源,按照相反的顺序释放资源。打破循环等待状态。
我们将线程2的代码修改如下,这样就不会出现死锁。
新线程(()- {
同步(资源1) {
system . out . println(thread . currentthread() get resource 1 );
尝试{
thread . sleep(1000);
} catch (InterruptedException e) {
e . printstacktrace();
}
system . out . println(thread . current thread()等待获取资源2 );
同步(资源2) {
system . out . println(thread . currentthread() get resource 2 );
}
}
},‘线程2’)。start();输出
Thread[ thread 1,5,main]获取资源1
线程[线程1,5,main]正在等待获取资源2
线程[线程1,5,main]获取资源2
线程[线程2,5,main]获取资源1
线程[线程2,5,main]正在等待获取资源2
线程[线程2,5,main]获取资源2
进程以退出代码0结束让我们分析一下为什么上面的代码避免了死锁。
线程1先获得resource1的监视器锁,然后线程2无法获得。然后线程1去获取resource2的监视器锁,可以获取。然后,线程1释放resource1和resource2的监视器锁,线程2可以获取并执行它们。这打破了循环等待条件,从而避免了死锁。
9. 说说 sleep() 方法和 wait() 方法区别和共同点?
两者的主要区别是:sleep 方法没有释放锁,而 wait 方法释放了锁。两者都可以暂停线程的执行。Wait通常用于线程间的交互/通信,sleep通常用于挂起执行。调用wait()方法后,线程不会自动唤醒,其他线程需要对同一对象调用notify()或notifyAll()方法。执行sleep()方法后,线程将自动唤醒。或者可以使用wait(长超时)超时,线程会自动唤醒。10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
这是另一个经典的java多线程面试问题,在面试中也经常被问到。很简单,但是很多人答不出来!
新线程,该线程已进入新状态;调用start()方法会启动一个线程,使其处于就绪状态,分配到时间片后就可以开始运行了。Start()会执行线程相应的准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。而直接执行run()方法会把run方法当作主线程下的普通方法,不会在某个线程中执行,所以这不是多线程。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
推荐教程:java教程以上是Java并发基础常见面试问题(总结)的详细内容。请多关注我们的其他相关文章!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。