Java多线程面试题(一)
- 前言
- 1、在 Java 中守护线程和本地线程区别?
- 2、线程与进程的区别?
- 3、什么是多线程中的上下文切换?
- 4、死锁与活锁的区别,死锁与饥饿的区别?
- 5、Java 中用到的线程调度算法是什么?
- 6、为什么使用 Executor 框架?
- 7、在 Java 中 Executor 和 Executors 的区别?
- 8、如何在 Windows 和 Linux 上查找哪个线程使用的 CPU 时间最长?
- 9、什么是 Executors 框架?
- 10、什么是阻塞队列?阻塞队列的实现原理是什么?
- 11、什么是 Callable 和 Future?
- 12、什么是 FutureTask?
- 13、什么是并发容器的实现?
- 14、多线程同步和互斥有几种实现方法,都是什么?
- 15、什么是竞争条件?你怎样发现和解决竞争?
- 16、你将如何使用 thread dump?你将如何分析 Thread dump?
- 17、为什么我们调用 start()方法时会执行 run()方法?
- 18、Java 中你怎样唤醒一个阻塞的线程?
- 19、什么是不可变对象,它对写并发应用有什么帮助?
- 20、为什么使用 Executor 线程池框架?
- 总结
前言
最新的 Java 面试题,技术栈涉及 Java 基础、集合、多线程、Mysql、分布式、Spring全家桶、MyBatis、Dubbo、缓存、消息队列、Linux…等等,会持续更新。
如果对老铁有帮助,帮忙免费点个赞,谢谢你的发财手!
1、在 Java 中守护线程和本地线程区别?
Java中任何线程都可以设置为守护线程和用户线程,通过方法 Thread.setDaemon(true);
true 则把该线程设置为守护线程,默认false为用户线程;
要注意的是Thread.setDaemon() 必须在 Thread.start()之前调用,否则运行时会抛出异常。
两者的区别:就是判断虚拟机(JVM)何时离开
用户线程就是如果有任何一个用户线程未结束,Java虚拟机是不会结束的。
守护线程就是如果只剩守护线程未结束,那么Java虚拟机就会结束了。
2、线程与进程的区别?
首先定义不一样:进程是一个在内存中运行的应用程序,而线程是进程中的一个执行任务;
一个线程只可以属于一个进程,但一个进程能包含多个线程, 多个线程可共享数据。
3、什么是多线程中的上下文切换?
是指线程数大于给程序分配的CPU数量时,为了让各个线程都有执行的机会,就需要切换使用CPU;
不同的线程切换使用CPU发生的切换数据等 就是上下文切换。
4、死锁与活锁的区别,死锁与饥饿的区别?
- 死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
产生死锁的必要条件:
1、独占资源:一个资源每次只能给一个进程使用(比如写操作)
2、占有且申请:进程在申请新的资源的同时,保持对原有资源的占有
3、不可夺取:资源申请者不能强行从资源占有者手中夺取资源,资源只能由占有者自愿释放
4、循环等待:线程1等待线程2占有的资源,线程2等待线程1占有的资源,形成了一个进程等待回路。
典型案例:哲学家共进午餐问题 - 活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。活锁有可能自行解开,死锁则不能。
饥饿:是指一个可运行的进程被调度器无限期地忽视,而不能被调度执行的情况。
5、Java 中用到的线程调度算法是什么?
有两种调度模型:分时调度模型和抢占式(java默认使用)调度模型。
- 分时调度模型: 平均分配每个线程占用的 CPU 的时间片。
- 抢占式调度模型: 让优先级高的线程占用CPU,如果线程优先级相同,那么就随机选择一个线程,可以通过 setPriority(1~10)设置优先级
6、为什么使用 Executor 框架?
Executor是一个多线程管理框架,创建线程的方式有三种Thread、Runnable、Callable
调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
接使用 new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。
7、在 Java 中 Executor 和 Executors 的区别?
Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
Executor 接口对象能执行我们的线程任务。
ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
使用 ThreadPoolExecutor 可以创建自定义线程池。
Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用 get()方法获取计算的结果。
8、如何在 Windows 和 Linux 上查找哪个线程使用的 CPU 时间最长?
使用 jstack 找出消耗 CPU 最多的线程代码。
9、什么是 Executors 框架?
Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。
无限制的创建线程会引起应用程序内存溢出,所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程,利用Executors 框架可以非常方便的创建一个线程池。
10、什么是阻塞队列?阻塞队列的实现原理是什么?
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素
11、什么是 Callable 和 Future?
Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
可以认为是带有回调的 Runnable。
Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable 用于产生结果,Future 用于获取结果。
12、什么是 FutureTask?
在 Java 并发程序中 FutureTask 表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和获取运算结果等方法,只有当运算完成的时候结果才能取回,如果运算尚未完成, get 方法将会阻塞。当一个线程需要等待另一个线程把某个任务执行完后它才能继续执行,此时可以使用FutureTask。
13、什么是并发容器的实现?
我们先从并发容器的由来谈起。Java的集合容器框架中,主要有四大类别:List、Set、Queue、Map,我们熟知的这些集合类ArrayList、LinkedList、HashMap这些容器都不是线程安全的,如果有多个线程并发地访问这些容器时,就会出现问题。因此Java提供了同步容器供用户使用。
- 同步容器可以简单地理解为通过synchronized来实现同步的容器,比如Vector、Hashtable以及SynchronizedList等容器,这样做的代价是削弱了并发性,当多个线程共同竞争容器级的锁时,吞吐量就会降低,因此为了解决同步容器的性能问题,所以才有了并发容器。
- 并发类容器是专门针对多线程并发设计的,采用了CAS算法和部分代码使用synchronized锁保证线程安全,只对操作的位置进行同步操作,但是其他没有操作的位置其他线程仍然可以访问,提高了程序的吞吐量。
java.util.concurrent包中提供了多种并发类容器,比如ConcurrentHashMap、CopyOnWriteArrayList CopyOnWriteArraySet等等。
CAS算法它可以不使用锁而保证多线程安全,所以CAS也是一种无锁算法,也是乐观锁的提现。
14、多线程同步和互斥有几种实现方法,都是什么?
线程同步是指一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
线程互斥是指当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。
15、什么是竞争条件?你怎样发现和解决竞争?
在java多线程中,当两个或以上的线程对同一个数据进行操作的时候,可能会产生“竞争条件”的现象。这时候需要加锁,来解决多线程操作同一个数据时可能产生的问题。加锁方式有两种,一h种是Lock对象来对语句快进行加锁,另一种是通过synchronized 关键字来对方法进行加锁。以上两种方法都可以有效解决多线程中存在的竞争条件的问题。
16、你将如何使用 thread dump?你将如何分析 Thread dump?
Thread Dump是非常有用的诊断Java应用问题的工具,
1、可以查找内存泄露;2、可以发现死锁线程。
然后分析Thread Dump的各个部分:
1、头部信息;2、线程类型;3、线程优先级;4、jvm线程id;5、线程状态
注意看这个:
- 初始化状态(New)
在调用start()方法之前 - 就绪状态(Runnable)
调用了start()方法,该线程就进入就绪状态,只有处于就绪状态的线程才能获得CPU的使用权。 - 运行状态(Running)
线程获得CPU的使用权,执行程序代码。 - 阻塞(超时)状态(Blocked)
线程因为某些原因放弃 CPU,暂时停止运行。比如执行了某个对象的 wait()方法、或者执行了sleep()方法,或者调用了其他线程的 join()方法。 - 死亡状态(Dead)
线程的run()运行结束,调用stop()方法或者在运行过程中出现了未捕获的异常时,线程进入死亡状态Thread Dump是非常有用的诊断Java应用问题的工具,
1、可以查找内存泄露;2、可以发现死锁线程。
然后分析Thread Dump的各个部分:
头部信息;2、线程类型;3、线程优先级;4、jvm线程id;5、线程状态。
17、为什么我们调用 start()方法时会执行 run()方法?
在调用start()方法后,线程并不能立即开始执行run()方法,而是处于就绪状态(Runnable),等待线程调度程序为其分配CPU时间片,并使其进入运行状态(Running),当线程获得CPU资源后,就会自动调用其对应的run()方法,开始执行线程代码。
Thread类中的start()方法实际上是一个异步调用,它会立即返回并继续执行下一条语句,而不会等待线程执行完毕。
但是如果你直接调用 run()方法,它不会创建新的线程,只会把 run 方法当作普通方法去执行 。
18、Java 中你怎样唤醒一个阻塞的线程?
在 Java 中使用 Object 类的 wait()和 notify()方法实现线程阻塞和唤醒。
wait、notify方法是针对对象的,而且必须在 synchronized 块或方法中被调用。
调用wait()方法都会导致线程阻塞,阻塞的同时也会释放该对象的锁,相应地,调用notify()方法则将解除该对象阻塞的线程,但它需要重新获取该对象的锁,直到获取成功才能往下执行。
19、什么是不可变对象,它对写并发应用有什么帮助?
不可变对象是指对象一旦被创建,它的状态(即对象属性值)就不能改变。
比如 String、8种基本类型的包装类等。(不可变对象永远是线程安全的)。
20、为什么使用 Executor 线程池框架?
线程池就是为了管理线程的生命周期,提高效率,优点如下:
- 1、可以复用线程,从而减少线程对象的反复创建和销毁。
- 2、可有效控制最大并发线程数,提高系统资源使用率。
- 3、框架中还有定时、定期、并发数控制等功能。
综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。
总结
都已经看到这里啦,赶紧收藏起来,祝您工作顺心,生活愉快!