备战实习,会定期的总结常考的面试题,大家一起加油! 🎯
本文章参考:
- JavaGuide
- 《并发编程的艺术》
- 并发编程&JVM
注意:如果本文中有错误的地方,欢迎评论区指正!🍭
往期链接:
🧭【面试题】计算机网络篇-10道常见面试题p1
⚡【面试题】JVM篇-10道常见面试题p1
1.说一下并发和并行的区别?
并发:同一时间段,多个任务都在执行 (单位时间内不一定同时执行)
并行: 单位时间内,多个任务同时执行
打个比方:
- 并发(concurrent)是同一时间应对(dealing with)多件事情的能力
- 并行(parallel)是同一时间动手做(doing)多件事情的能力。
详细内容可以参考:【并发编程】(学习笔记一进程与线程)-part1
2.什么是线程?什么是进程?
- 进程:是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
- 线程:与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。在java中,线程作为最小调度单位,进程作为资源分配的最小单位。
👨💻面试官追问:线程和进程区别是什么?
- 线程是进程划分成的更小的运行单位。
- 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
- 线程更轻量,线程上下文切换成本一般比进程上下文切换低。线程也被称为轻量级进程。
- 进程间通信较为复杂。线程通信相对简单,因为它们共享进程内的内存。一个例子是多个线程可以访问同一个共享变量。
- 同一台计算机的进程通信称为IPC(Inter-process communication).
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,如HTTP。
3.说一下你对守护线程的了解?
守护线程:即Daemon
线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)
将线程设置为Daemon
线程。
👨💻面试官追问:Daemon线程的使用有什么需要注意的吗?
Daemon
属性需要在启动线程之前设置,不能在启动线程之后设置。- 在构建
Daemon
线程时,不能依靠finally
块中的内容来确保执行关闭或清理资源的逻辑。
4.使用多线程可能带来什么问题?
在进行并发编程时,如果希望通过多线程执行任务让程序运行得更快,会面临非常多的挑战,比如:
- 上下文切换的问题:频繁的上下文切换会影响多线程的执行速度。
- 死锁的问题
- 受限于硬件和软件的资源限制问题:在进行并发编程时,程序的执行速度受限于计算机的硬件或软件资源。
👨💻面试官追问:如何减少上下文切换呢?
- 无锁并发编程:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
- CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
- 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
👨💻面试官继续追问:既然你提到了锁,那么死锁产生的必要条件是什么?
一共四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
👨💻面试官继续逼问:那你说说Java多线程避免死锁有什么办法?
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用
lock.tryLock(timeout)
来替代使用内部锁机制。- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
👨💻面试官叒问:说了这么多,你用现在写一个Java死锁的案例?
可以的:
public class demo1Main1 { private static String A = "A"; private static String B = "B"; public static void main(String[] args) { new demo1Main1().deadLock(); } private void deadLock() { Thread t1 = new Thread(() -> { synchronized (A) { try { Thread.currentThread().sleep(2000);//属于线程 Thread 的方法,让线程暂缓执行,等待预计时间之后再恢复,交出CPU使用权,不会释放锁。这里是为了防止t1对A和B全部加锁了,t2还没有对B加锁。让执行顺序保证为t1->A,t2->B... } catch (InterruptedException e) { e.printStackTrace(); } synchronized (B) { System.out.println("1"); } } }); Thread t2 = new Thread(() -> { synchronized (B) { synchronized (A) { System.out.println("2"); } } }); t1.start(); t2.start(); } }
5.说说线程的生命周期和状态?
Java线程在运行的生命周期中可能处于6种状态,在给定的一个时刻,线程处于其中一个状态。
- NEW:初始状态,线程被构建,但是还没有调用
start()
方法 - RUNNABLE:运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
- BLOCKED:阻塞状态,表示线程阻塞于锁
- WAITING:等待状态,表示线程进人等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
- TIMEWAITING:超时等待状态,该状态不同于
WAITING
,它是可以在指定的时间自行返回的 - TERMINATED:终止状态,表示当前线程已经执行完毕
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
一个完整的过程可以这样来看:
- 线程创建之后它将处于 NEW(新建) 状态,调用
start()
方法后开始运行,线程这时候处于 READY(就绪) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。 - 当线程执行
wait()
方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过sleep(long millis)
方法或wait(long millis)
方法可以将 Java 线程置于TIMED_WAITING
状态。 - 当超时时间到达后 Java 线程将会返回到
RUNNABLE
状态。 - 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。
- 线程在执行 Runnable 的
run()
方法之后将会进入到 TERMINATED(终止) 状态。
6.说说创建线程的几种方式?
- 继承
Thread
类创建线程; - 实现
Runnable
接口创建线程 - 通过
Callable
和Future
创建线程 - 通过线程池创建线程
👨💻面试官又问:Runnable 和 Callable 有什么区别?
- Runnable接口中的
run()
方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已- Callable接口中的
call()
方法是有返回值的,是一个泛型,和Future
、FutureTask
配合可以用来获取异步执行的结果
例子:
public class Test { public static void main(String[] args) throws ExecutionException, InterruptedException { //创建任务对象,并传入一个Callable对象 FutureTask<Integer> task=new FutureTask<>(new Callable<Integer>() { @Override public Integer call() throws Exception { System.out.println("running..."); Thread.sleep(1000); return 100; } }); Thread t1 = new Thread(task, "t1"); t1.start(); //获取线程中方法执行后返回的结果 System.out.println(task.get());//get方法会一直等待task完成,才会得到结果 } }
7.说说sleep() 和 wait() 的区别?
sleep() :是线程类Thread
的方法,调用会暂停此线程指定的时间,不会释放对象锁,到时间自动恢复;sleep()
通常被用于暂停执行
wait() :是 Object
的方法,调用会放弃对象锁,进入等待队列。线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()
或者 notifyAll()
方法。wait()
通常被用于线程间交互/通信
8.知道线程中的 run() 和 start() 有什么区别吗?
start():启动一个新线程,在新的线程运行run
方法中的代码。start
方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的 start
方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
run():新线程启动后会调用的方法。如果在构造 Thread
对象时传递了 Runnable
参数,则线程启动后会调用 Runnable
中的 run
方法,否则默认不执行任何操作
总的来说使用start
是启动了新的线程,通过新的线程间接执行run
中的代码。直接调用run
方法是在主线程中执行了run
,没有启动新的线程。
9.说一下线程的sleep()方法和yield()方法有什么不同?
sleep():调用 sleep
会让当前线程从 Running
进入 Timed Waiting
状态。其它线程可以使用 interrupt
方法打断正在睡眠的线程,这时 sleep
方法会抛出 InterruptedException
异常
yield():调用 yield
会让当前线程从 Running
进入 Runnable
就绪状态(仍然有可能被执行),然后调度执行其它线程。不会抛出异常。
10.在 Java 程序中怎么保证多线程的运行安全?
可以从三个方面考虑:
-
原子性:在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。可以用
atomic
包下的原子类或者synchronized
来保证 -
可见性:Java提供了
volatile
关键字来保证可见性。当一个共享变量被volatile
修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值另外,通过
synchronized
和Lock
也能够保证可见性,synchronized
和Lock
能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。 -
有序性:在Java里面,可以通过
volatile
关键字来保证一定的“有序性”。另外可以通过synchronized
和Lock
来保证有序性,很显然,synchronized
和Lock
保证每个时刻是只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before
规则来保证有序性的
最后喜欢的小伙伴,记得三连哦!😏🍭😘