面试题003-Java-Java多线程(上)
目录
- 面试题003-Java-Java多线程(上)
- 题目自测
- 题目答案
- 1. 什么是线程和进程? 线程与进程的关系和区别?
- 2. 为什么要使用多线程?
- 3. 什么是线程上下文切换?
- 4. 什么是线程死锁? 如何避免死锁?
- 5. 乐观锁和悲观锁了解么?如何实现乐观锁?
- 6. 说说 sleep() 方法和 wait() 方法区别和共同点?
- 7. 讲一下 JMM(Java 内存模型) ?
- 8. Java 内存区域和 JMM 有何区别?
- 9. Java中如何创建线程?
- 10. 实现Runnable接口和实现Callable接口的区别?
- 参考资料
题目自测
- 1. 什么是线程和进程? 线程与进程的关系和区别?
- 2. 为什么要使用多线程?
- 3. 什么是线程上下文切换?
- 4. 什么是线程死锁?如何避免死锁?
- 5. 乐观锁和悲观锁了解么?如何实现乐观锁?
- 6. 说说 sleep() 方法和 wait() 方法区别和共同点?
- 7. 讲一下 JMM(Java 内存模型) ?
- 8. Java 内存区域和 JMM 有何区别?
- 9. Java中如何创建线程?
- 10. 实现Runnable接口和实现Callable接口的区别?
题目答案
1. 什么是线程和进程? 线程与进程的关系和区别?
答:进程是程序一次执行的过程,系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程是程序执行的最小单位,一个进程可以产生多个线程,线程可以共享其进程的资源。
线程与进程的关系:一个进程可以包含多个线程,线程是进程的一部分。线程共享进程的资源,进程之间相互独立。
线程和进程的区别:进程有独立的内存空间,线程共享进程的内存空间。进程间通信复杂,线程可以通过共享内存进行通信速度快。进程独立性强安全性高,线程独立性相对较弱,一个线程崩溃可能导致整个程序崩溃。
2. 为什么要使用多线程?
答:提高性能和响应速度,多线程可以使程序同时执行多个任务,从而充分利用多核处理器的能力,提高程序的执行效率。如一个多线程的Web服务器可以同时处理多个客户端请求。
更好的利用资源,在多核处理器环境下,多线程可以让多个CPU核心并行工作。在单核处理器上,多线程可以让某些任务等待I/O操作时执行其他任务,从而更高效的利用CPU时间。
3. 什么是线程上下文切换?
答:线程上下文切换是指操作系统在多线程任务处理中,将CPU从一个线程切换到另外一个线程的过程。在这个过程中,操作系统需要保存当前线程的状态也称上下文(程序计数器、堆栈信息等),然后加载另外一个线程的状态,以便该线程可以继续执行。
出现线程切换的情况:
- 时间片用完:操作系统将CPU时间划分为多个时间片,每个线程会被分配一个固定的时间片,当时间片用完,操作系统会进行上下文切换,让其他线程活动CPU时间。
- 阻塞操作:当线程执行阻塞操作(如等待I/O操作完成、执行slepp()、wait()、等待同步锁等)时,它会主动放弃CPU的使用权,进入阻塞状态。
- 中断处理:当硬件发生中断时(如键盘输入、定时器触发),操作系统需要处理当前线程的执行,以处理中断。
- 线程优先级变化:当某个线程的优先级发生变化时(如通过调用线程的setPriority()方法),作系统可能会进行上下文切换,将CPU分配给这个高优先级的线程。这种调度策略称为抢占式调度。
4. 什么是线程死锁? 如何避免死锁?
答:线程死锁是指两个或多个线程在执行的过程中,因相互持有对方所需要的资源而处于等待状态,导致所有线程都无法执行继续的情况。具体表现为,每个线程都在等待其他线程释放资源,而每个线程又都持有其他线程所需的资源,从而形成一个等待环路,造成永久阻塞。
死锁的四个必要条件:
- 互斥:至少有一个资源必须是非共享的,即一次只能由一个线程使用。
- 占有且等待:个线程已经持有至少一个资源,同时等待获取其他被其他线程持有的资源。
- 不可剥夺:资源不能被强行剥夺,必须由持有线程显示释放。
- 循环等待:发生死锁时,涉及的所有线程必定存在一个循环等待资源的链。
避免死锁的方法: - 资源有序分配:为资源分配一个全局顺序,所有线程按照这个顺序请求资源,可以避免发生循环等待。
- 避免嵌套锁:尽量避免一个线程同时持有多个锁,减少嵌套锁的使用。
- 限时等待:如无法在指定时间内获取锁,就放弃请求并释放已经持有的锁。
- 死锁检测和恢复:使用JVM提供的工具或第三方库来监控和检测死锁。
- 资源预先分配:确保线程在开始执行前就已经获取到所需的所有资源。
5. 乐观锁和悲观锁了解么?如何实现乐观锁?
答:乐观锁和悲观锁这两种锁策略用于解决并发控制问题。
悲观锁是一种假设最坏情况的锁策略,每次在去访问数据时都会认为别人会修改,所以在每次访问数据时都会上锁,这样别人在拿这个数据就会阻塞直到它拿到锁。Java中悲观锁是通过synchronized关键字或Lock接口来实现。
乐观锁是一种假设最好情况的锁策略,每次在去访问数据时都认为别人不会修改,所以不会上锁,但是在修改的时候会去判断有没有人修改这个数据。Java中通常通过版本号机制或者CAS算法来实现。
实现乐观锁的方式:
- 版本号:在数据库表中添加一个版本号字段,每次更新数据时版本号加1。在更新数据时,将当前版本号作为条件之一进行更新,如果版本号不匹配,则说明数据已经被其他线程修改。
- CAS:使用一个预期值和要更新的变量值进行比较,两值相等才会进行更新
6. 说说 sleep() 方法和 wait() 方法区别和共同点?
答:slepp()和wait()都可以暂停线程的执行。
两者的区别:
- sleep() 是 Thread 类的静态方法,wait() 是 Object类的实例方法。
- sleep() 不会释放锁,wait() 会释放锁进入等待状态、直到被唤醒。
- sleep() 在指定的时间到期后会自动唤醒,wait() 需要 notify()/notifyAll()方法来唤醒。
- sleep() 可以在任何地方使用,wait() 必须在同步块或者同步方法中调用。
7. 讲一下 JMM(Java 内存模型) ?
答:Java内存模型是Java虚拟机规范的一部分,它定义了在多线程环境下,Java程序中变量的访问方式。JMM是一种抽象概念,它屏蔽了不同硬件和操作系统内存访问的差异,为Java程序提供了一致的内存访问效果。
JMM分为主内存和工作内存,主内存是所有线程共享的区域,对应于JVM内存区域中的堆。工作内存是JMM抽象的概念,并非真实存在,它对应于JVM中的栈,线程对变量的所有操作都必须在工作内存中进行,不能直接操作主内存中的数据。
8. Java 内存区域和 JMM 有何区别?
答:Java内存区域侧重于JVM如何管理和划分运行时内存区域。Java内存区域有堆、方法区、虚拟机栈、程序计数器、本地方法栈。
JMM侧重于多线程环境下线程与主存之间的交互关系以及变量的访问规则。
9. Java中如何创建线程?
答:Java中创建线程的方式有很多,如继承Thread类、实现Runnable接口、实现Callable接口、使用ExecutorService线程池等等。
- 继承Thread类:创建一个类来继承Thread类,重写它的run()方法。然后通过创建这个类的实例来创建新的线程,通过调用类实例的start()方法来启动线程。
class MyThread extends Thread { public void run() { System.out.println("线程运行中"); } } public class ThreadExample { public static void main(String[] args) { MyThread t = new MyThread(); t.start(); // 启动线程 } }
- 实现Runnable接口:创建一个类来实现Runnable接口,实现它的run()方法,然后创建Thread类的实例,将Runnable接口实现类的实例作为构造器参数传递,调用Thread类的实例的start()方法启动线程。
class MyThread extends Thread { public void run() { System.out.println("线程运行中"); } } public class ThreadExample { public static void main(String[] args) { MyThread t = new MyThread(); t.start(); // 启动线程 } }
- 实现Callable接口:创建一个Callable接口的实现类,并实现call()方法。然后创建Callable实现类的实例,使用FutureTask类来包装Callable对象,使用FutureTask对象作为Thread对象的target创建并启动新线程,调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
class MyCallable implements Callable<String> { public String call() throws Exception { return "线程执行结果"; } } public class CallableExample { public static void main(String[] args) throws Exception { MyCallable callable = new MyCallable(); FutureTask<String> futureTask = new FutureTask<>(callable); new Thread(futureTask).start(); System.out.println(futureTask.get()); // 获取结果 } }
10. 实现Runnable接口和实现Callable接口的区别?
答:他们主要的区别体现在方法签名和返回值、异常处理和提交与执行方式上。
- 方法签名和返回值:Runnable的run()方法有返回值,Callable的call()方法有返回值。
- 异常处理:run()方法不能抛出受检异常,只能处理运行时异常。call()方法可以声明抛出异常。
- 提交和执行:Runnable通过Thread或ExecutorService执行。Callable通过ExcutroService执行,并返回Future对象以获取结果。
参考资料
- JavaGuide
- 牛客网-Java面试宝典