java基础知识面试题四多线程
1. 什么是线程
-
线程(thread):进程可进一步细化为线程,是程序内部的
一条执行路径
。一个进程中至少有一个线程。-
一个进程同一时间若
并行
执行多个线程,就是支持多线程的。 -
线程作为
CPU调度和执行的最小单位
。 -
一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来
安全的隐患
。
-
2. 线程和进程有什么区别
进程:对应一个运行中的程序。
线程:运行中的进程的一条或多条执行路径。
3. 多线程使用场景
-
手机app应用的图片的下载
-
迅雷的下载
-
Tomcat服务器上web应用,多个客户端发起请求,Tomcat针对多个请求开辟多个线程处理
4. 如何在Java中出实现多线程?
方法 1:继承 Thread
类
- 创建一个子类 继承自
Thread
。 - 重写
run()
方法,在其中定义线程要执行的代码。 - 创建线程实例 并调用
start()
方法启动线程。
示例:
class MyThread extends Thread {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + " - Count: " + i);try {Thread.sleep(500); // 暂停500毫秒} catch (InterruptedException e) {e.printStackTrace();}}}
}
public class ThreadExample {public static void main(String[] args) {MyThread thread1 = new MyThread();MyThread thread2 = new MyThread();thread1.start(); // 启动线程1thread2.start(); // 启动线程2}
}
方法 2:实现 Runnable
接口
- 创建一个类 实现
Runnable
接口。 - 重写
run()
方法,定义线程要执行的代码。 - 创建
Thread
实例,将Runnable
对象作为参数传递给Thread
。 - 调用
start()
方法启动线程。
示例:
class MyRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println(Thread.currentThread().getName() + " - Count: " + i);try {Thread.sleep(500); // 暂停500毫秒} catch (InterruptedException e) {e.printStackTrace();}}}
}
public class RunnableExample {public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();Thread thread1 = new Thread(myRunnable);Thread thread2 = new Thread(myRunnable);thread1.start(); // 启动线程1thread2.start(); // 启动线程2}
}
5. Thread类中的start()和run()有什么区别?
start()
:创建并启动一个新线程,在新线程中执行run()
方法,支持并发执行。run()
:是线程执行的逻辑,如果直接调用则不会启动新线程,而是在当前线程中执行。
6. 启动一个线程是用run()还是start()?
start()
7. Java中Runnable和Callable有什么不同?
与之前的方式的对比:与Runnable方式的对比的好处
> call()可以有返回值,更灵活
> call()可以使用throws的方式处理异常,更灵活
> Callable使用了泛型参数,可以指明具体的call()的返回值类型,更灵活有缺点吗?如果在主线程中需要获取分线程call()的返回值,则此时的主线程是阻塞状态的。
8. 什么是线程池,为什么要使用它?
线程池是一个管理、控制多个工作线程的技术,用于减少线程的创建与销毁次数,提高系统资源的使用效率。在 Java 中,线程池由 java.util.concurrent
包中的 Executor
框架提供,通过 ThreadPoolExecutor
实现具体的线程池管理。
线程池的作用
- 减少资源消耗:线程池复用已创建的线程,避免频繁创建、销毁线程带来的系统资源消耗。
- 提高响应速度:当有新任务时,不必等待线程创建即可执行,减少了任务的等待时间。
- 提高线程管理的可控性:线程池可以根据系统资源情况设置线程数量的上限和下限,避免过多线程导致资源耗尽。
- 提供定时任务和周期性任务执行的功能:线程池可以定期或周期性地执行任务,适用于需要周期性运行的任务。
线程池的工作原理
- 任务提交:任务被提交给线程池后,线程池会将其放入任务队列中。
- 线程执行任务:线程池中的空闲线程会从任务队列中取出任务并执行。如果没有空闲线程而线程数量低于最大线程数,线程池会创建新线程。
- 任务完成后:线程将返回线程池中待命,等待执行新的任务。
Java 中的线程池类型
Java 提供了几种常见的线程池:
FixedThreadPool
:固定大小的线程池,适用于执行长期任务和控制线程数量的场景。CachedThreadPool
:适合执行大量、短期的异步任务。线程池会根据需要创建新线程,空闲线程超过 60 秒会被销毁。ScheduledThreadPool
:支持定时和周期性执行任务。SingleThreadExecutor
:单线程化的线程池,保证所有任务按顺序执行。
9. sleep() 和 yield()区别?
sleep()
:使线程暂停执行,进入阻塞状态,直到指定时间结束,线程会释放 CPU 资源。yield()
:让当前线程暂时放弃 CPU 时间,转为就绪状态,以允许其他线程执行,但不一定会立即导致其他线程的执行。
10. 线程创建的中的方法、属性情况?
在 Java 中,线程的创建和管理涉及多个方法和属性。这里主要讨论通过 Thread
类和 Runnable
接口创建线程时常用的方法和属性。
1. 通过 Thread
类创建线程
方法
start()
:启动线程,调用run()
方法。在新线程中执行代码。run()
:线程执行的主体代码。如果直接调用run()
,则不会启动新线程。sleep(long millis)
:使当前线程休眠指定的时间,进入阻塞状态。join()
:使当前线程等待直到被调用的线程完成执行。interrupt()
:中断线程的执行。setPriority(int priority)
:设置线程的优先级(范围为 1 到 10)。getName()
:获取线程的名称。setName(String name)
:设置线程的名称。
属性
Thread.MIN_PRIORITY
:最小优先级,值为 1。Thread.NORM_PRIORITY
:默认优先级,值为 5。Thread.MAX_PRIORITY
:最大优先级,值为 10。Thread.State
:线程的状态(如 NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED)。
2. 通过 Runnable
接口创建线程
方法
run()
:实现Runnable
接口时,定义线程执行的逻辑。在Thread
对象中通过start()
方法启动线程。- 创建
Thread
对象:在创建Thread
时,将实现了Runnable
的类的实例传递给Thread
构造函数。
11. 线程的生命周期?
总结
- Java 线程的生命周期主要包括 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING 和 TERMINATED 状态。
- 线程的状态转移依赖于线程的执行情况、等待条件和锁的获取情况。理解这些状态对调试和优化多线程应用至关重要。
12. 线程的基本状态以及状态之间的关系?
线程的基本状态及其相互关系在 Java 中主要体现在以下几种状态:
1. NEW(新建状态)
- 描述:线程被创建但尚未启动。此时,线程并没有分配 CPU 资源。
- 状态转移:通过调用
start()
方法,线程从 NEW 状态转变为 RUNNABLE 状态。
2. RUNNABLE(可运行状态)
- 描述:线程处于可运行状态,可以被调度执行,但并不一定在运行。此状态包括两个子状态:正在运行和就绪。线程调度器决定哪个线程在 CPU 上运行。
- 状态转移:
- 当线程调用
start()
时,从 NEW 转换为 RUNNABLE。 - 当线程执行
sleep()
、wait()
、join()
或其他阻塞操作时,线程会从 RUNNABLE 转移到 WAITING 或 TIMED_WAITING 状态。 - 当线程获取到锁后,可能会从 BLOCKED 状态转移回 RUNNABLE。
- 当线程调用
3. BLOCKED(阻塞状态)
- 描述:线程在尝试获取一个已被其他线程持有的锁时,进入 BLOCKED 状态。此时线程无法继续执行。
- 状态转移:当线程获取到所需的锁时,BLOCKED 状态会转移到 RUNNABLE 状态。
4. WAITING(等待状态)
- 描述:线程在等待另一个线程执行特定操作(如
wait()
、join()
或LockSupport.park()
)时,进入 WAITING 状态。此状态下线程不会占用 CPU 资源。 - 状态转移:
- 当调用的线程被唤醒(如通过
notify()
或notifyAll()
)时,或者被中断时,线程将转移回 RUNNABLE 状态。
- 当调用的线程被唤醒(如通过
5. TIMED_WAITING(计时等待状态)
- 描述:线程在等待特定时间后自动返回的状态。这可以由调用
Thread.sleep(millis)
、Object.wait(timeout)
、Thread.join(timeout)
等引起。 - 状态转移:在指定的时间到达后,线程会转移回 RUNNABLE 状态。
6. TERMINATED(终止状态)
- 描述:线程已完成执行,或由于异常终止。此时线程的生命周期结束,无法再执行。
- 状态转移:线程在完成
run()
方法的执行或抛出未捕获异常后进入此状态。
状态之间的关系
下面是线程状态及其之间转移的简要概述:
NEW -> RUNNABLE (通过 start())
RUNNABLE -> BLOCKED (等待锁)
RUNNABLE -> WAITING (调用 wait()/join())
RUNNABLE -> TIMED_WAITING (调用 sleep()/wait(timeout)/join(timeout))
BLOCKED -> RUNNABLE (获取到锁)
WAITING -> RUNNABLE (被唤醒或中断)
TIMED_WAITING -> RUNNABLE (时间到)
RUNNABLE -> TERMINATED (完成或异常)
13. stop()和suspend()方法为何不推荐使用?
stop():一旦执行,线程就结束了,导致run()有未执行结束的代码。stop()会导致释放同步监视器,导致线程安全问题。
suspend():与resume()搭配使用,导致死锁。
14. Java 线程优先级是怎么定义的?
在 Java 中,线程优先级是通过 Thread
类中的常量来定义的,主要用于帮助线程调度器决定线程的执行顺序。线程优先级的范围是 1 到 10,具体的常量定义如下:
线程优先级常量
Thread.MIN_PRIORITY
:最小优先级,值为 1。Thread.NORM_PRIORITY
:默认优先级,值为 5。Thread.MAX_PRIORITY
:最大优先级,值为 10。
设置线程优先级
可以通过 setPriority(int priority)
方法来设置线程的优先级。优先级较高的线程会比优先级较低的线程获得更多的 CPU 时间,但这并不保证高优先级的线程一定会先执行,具体行为依赖于底层操作系统的线程调度策略。
15. 你如何理解线程安全的?线程安全问题是如何造成的?
线程安全的理解
线程安全是指在多线程环境中,多个线程同时访问某个对象或资源时,不会导致数据不一致或状态错误的情况。当一个类或方法被称为线程安全时,意味着它能够在多线程环境下正常工作,不需要外部同步机制来确保正确性。
线程安全的特点
- 原子性:线程安全的操作是不可分割的,要么完全执行,要么完全不执行。
- 可见性:当一个线程对共享变量进行修改时,其他线程能够立即看到这个修改。
- 一致性:多线程操作某个对象时,状态始终保持一致。
线程安全问题的产生原因
线程安全问题通常源于以下几个方面:
-
共享资源的访问:
- 多个线程同时访问和修改共享资源(如变量、对象或数据结构)时,可能会导致数据冲突或不一致。
-
不适当的同步机制:
- 如果对共享资源的访问没有适当的同步(如使用
synchronized
关键字、Lock
接口等),则多个线程可能会同时读取和修改数据,导致状态错误。
- 如果对共享资源的访问没有适当的同步(如使用
-
顺序执行的假设:
- 多线程程序通常假设操作是按顺序执行的,但实际执行可能并不遵循这一假设。比如,一个线程在读取数据时,另一个线程可能已经修改了这些数据。
-
不可变对象的使用不足:
- 如果使用的对象不是不可变的,多个线程同时修改同一个对象的状态,会导致不一致。
-
中间状态的可见性:
- 在线程间共享的变量在一个线程中修改后,另一个线程未必能立即看到该修改,可能导致读取到中间状态。
16. 多线程共用一个数据变量需要注意什么?
在多线程环境中共享数据变量时,需要注意同步控制、原子性、可见性、避免死锁、减少共享状态、确保数据一致性以及使用合适的并发数据结构。这些注意事项可以帮助开发者避免多线程编程中的常见问题,确保程序的稳定性和正确性。
17. 多线程保证线程安全一般有几种方式?
在多线程环境中保证线程安全可以通过使用同步机制、显式锁、原子变量、线程安全的集合、线程局部变量、无锁编程以及良好的并发设计模型等多种方式来实现。选择适当的方法应根据具体应用场景和性能需求进行。
18. 用什么关键字修饰同步方法?
synchronized
19. synchronized加在静态方法和普通方法区别
- 普通方法的
synchronized
锁定当前对象的实例,适用于保证同一实例的线程安全。 - 静态方法的
synchronized
锁定类的Class
对象,适用于保证类级别的线程安全。选择使用哪种方式取决于具体的应用场景和设计需求。
20. Java中synchronized和ReentrantLock有什么不同
synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放对同步监视器的调用。
Lock是通过两个方法控制需要被同步的代码,更灵活一些。
Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高。
21. 当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?
需要看其他方法是否使用synchronized修饰,同步监视器的this是否是同一个。
只有当使用了synchronized,且this是同一个的情况下,就不能访问了。
22. 线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?
同步一定阻塞;阻塞不一定同步。
23. 什么是死锁,产生死锁的原因及必要条件
1. 如何看待死锁?
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
我们编写程序时,要避免出现死锁。2. 诱发死锁的原因?
- 互斥条件
- 占用且等待
- 不可抢夺(或不可抢占)
- 循环等待以上4个条件,同时出现就会触发死锁。3. 如何避免死锁?
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
24. Java中notify()和notifyAll()有什么区别
notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。(如果被wait()的多个线程的优先级相同,则
随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
25. 为什么wait()和notify()方法要在同步块中调用
因为调用者必须是同步监视器。
26. wait()和sleep()有什么区别?调用这两个函数后,线程状态分别作何改变?
相同点:一旦执行,当前线程都会进入阻塞状态不同点:
> 声明的位置:wait():声明在Object类中sleep():声明在Thread类中,静态的
> 使用的场景不同:wait():只能使用在同步代码块或同步方法中sleep():可以在任何需要使用的场景
> 使用在同步代码块或同步方法中:wait():一旦执行,会释放同步监视器sleep():一旦执行,不会释放同步监视器
> 结束阻塞的方式:wait(): 到达指定时间自动结束阻塞 或 通过被notify唤醒,结束阻塞sleep(): 到达指定时间自动结束阻塞
26. 手写一个单例模式(Singleton),还要安全的
下面是一个线程安全的单例模式(Singleton)的实现,采用的是 双重检查锁定(Double-Checked Locking)的方法。这种方法在多线程环境中既保证了线程安全,又避免了不必要的同步开销。
单例模式实现
public class Singleton {// 使用 volatile 关键字保证可见性和禁止指令重排private static volatile Singleton instance;// 私有构造函数,防止外部实例化private Singleton() {// 防止反射攻击if (instance != null) {throw new IllegalStateException("Already instantiated");}}// 获取实例的方法public static Singleton getInstance() {// 第一次检查if (instance == null) {synchronized (Singleton.class) {// 第二次检查if (instance == null) {instance = new Singleton();}}}return instance;}
}
解释
- 私有构造函数:确保外部无法直接创建实例。
volatile
关键字:用于确保instance
在多线程环境中的可见性,防止指令重排带来的问题。- 双重检查锁定:
- 第一次检查
instance
是否为null
,如果是,进入同步块。 - 在同步块内再次检查
instance
是否为null
,如果仍然是null
,则创建实例。这种方式可以避免在每次调用getInstance()
时都进行同步,从而提高性能。
- 第一次检查
使用示例
public class Main {public static void main(String[] args) {Singleton singleton1 = Singleton.getInstance();Singleton singleton2 = Singleton.getInstance();System.out.println(singleton1 == singleton2); // 输出 true,确保是同一实例}
}