并发编程面试题
在 java 中守护线程和本地线程区别
用户线程是程序创建的线程。
由jvm创建的线程是守护线程,比方说垃圾收集线程。
死锁与活锁的区别 ,死锁与饥饿的区别?
死锁 :是指两个或两个以上的进程( 或线程)在执行过程中 , 因争夺资源而造成的一种互相等待的现象 ,若无外力作用 , 它们都将无法推进下去。
活锁 :任务或者执行者没有被阻塞 , 由于某些条件没有满足 , 导致一直重复尝试 ,失败 , 尝试 ,失败。
饥饿 :一个或者多个线程因为种种原因无法获得所需要的资源 ,导致一直无法执行的状态。
如何在 Windows 和 Linux 上查找哪个线程使用的 CPU 时间最长?
top和jstack命令
Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?
Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。
synchronized只支持非公平锁。
什么是 Callable 和 Future?
Callable是有返回值的,这个返回值可以使用Future拿到。
在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别?
CyclicBarrier 可以重复使用 , 而 CountdownLatch 不能重复使用。
CountDownLatch是门栓的意思,常用于并发编程中,它可以让多个线程都阻塞在一个地方,直到所有线程任务都执行完成。
常用方法:countDown方法和await方法。
await方法会的作用就是等待所有的线程都执行完了。countDown方法就是当某个线程执行完了任务之后就将count值进行减1.
应用场景:
并行任务同步:CountDownLatch用于协调多个并行任务的完成情况,确保所有任务都完成后再继续执行下一步操作。
多任务汇总:CountDownLatch可以用于统计多个线程的完成情况,以确定所有线程都以完成工作。
资源初始化:CountDownLatch可以用于等待资源的初始化完成,以边在初始化完成后开始使用。
CountDownLatch可以阻塞主线程,适用于需要同步返回的接口。
CyclicBarrier是循环栅栏的意思,他的特点是可以循环使用,当多个线程都到达同一个指定点的时候,再同时执行。
应用场景:CyclicBarrier可以用于将复杂的任务分配给多个线程执行,并在所有线程完成工作后触发后续的操作。
CyclicBarrier无法阻塞主线程,不适合在需要同步返回的接口中使用。CyclicBarrier适用于异步任务,尤其需要对子线程的执行结果做汇聚计算的更为适合,在大部分场景下,CountDownLatch能实现的功能,都能使用CyclicBarrier实现。
如何停止一个正在运行的线程?
使用 interrupt 方法终止线程,不推荐使用stop方法。
interrupt方法:会中断线程的休眠。说白了就是叫醒你起来继续干活。
stop方法:强行终止线程,可能会损坏正在处理的数据。
notify()和notifyAll()有什么区别?
notify只能唤醒一个线程,notifyall唤醒所有的线程。
乐观锁和悲观锁的理解及如何实现 ,有哪些实现方式?
cas就是一种乐观锁的实现方式。
什么叫线程安全?servlet 是线程安全吗?
Servlet 不是线程安全的。
SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程。
可以使用ThreadLocal来实现多线程并发访问的问题。
volatile 有什么用?能否用一句话说明下 volatile 的应用场景?
volatile 保证内存可见性和禁止指令重排
在 java 中 wait 和 sleep 方法的不同?
最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。
什么是 ThreadLocal 变量?
Thread Local是线程隔离的变量,它让每个线程都拥有自己独立的变量副本。
什么是线程池? 为什么要使用它?
创建线程要花费昂贵的资源和时 间, 如 果任 务来 了才 创建 线 程那 么响 应时 间会 变长 ,
为了避免这些问题 , 在程序启动的时候就创建若干线程来响应处理 , 它们被称为线程池 , 里面的线程叫工作线程 。进而提高了程序的处理效率。
怎么检测一个线程是否拥有锁?
Thread类里面有一个holdsLock()方法
你如何在 Java 中获取线程堆栈?
jstack命令
Thread 类中的 yield 方法有什么作用?
使当前线程从执行状态( 运行状态) 变为可执行状态( 就绪状态)。
这玩意叫线程的礼让。
Java 中 ConcurrentHash Map 的并发度是什么?
TODO下来看看
Java 中 Semaphore 是什么?
Semaphore是信号量的意思,常用于并发编程中。用于控制同时访问某个资源的线程数量。Semaphore维护了一个计数器,线程可以通过acquire方法来获取许可证,可以通过release方法来释放许可证。当许可证数量为0的时候,再来线程将被阻塞。
Semaphore底层也支持公平锁和非公平锁。当使用公平锁的时候,下一个被执行任务的线程一定是等待时间最久的那个线程。
应用场景:
限流:Semaphore可以用于限制对共享资源的并发访问数量,以控制系统的流量。
资源池:Semaphore可以用于实现资源池,以维护一组有限的共享资源。
Java 中的 ReadWriteLock 是什么?
ReentrantReadWriteLock是读写锁,常用于java编程中。它允许多个线程同时读取数据,但只允许一个线程写入数据。在某些情况下,比方说读多写少的场景中,使用这个类可以提高程序的性能和可靠性。
应用场景:主要用于读多写少的场景。比如:在数据库连接池中,多个线程可能需要同时访问数据库,但是只有一个线程能够执行写操作,如插入、更新或删除数据。使用reentrantreadwritelock可以在保证数据一致性的同时提高并发性能。
你对线程优先级的理解是什么?
1 代表最低优先级 , 10 代表最高优先级。
同步方法和同步模块 ,哪个是更好的选择?
同步块是更好的选择 , 因为它不会锁住整个对象( 当然你也可以让它锁住整个对象) 。
同步方法会锁住整个对象 , 哪怕这个类中有多个不相关联的同步块 , 这通常会导致他们停止执行并需要等待获得这个对象上的锁。
什么是 CAS
CAS 是 compare and swap 的缩写 , 即我们所说的比较交换
Java中基于Unsafe的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令。
CAS 的问题
ABA问题
不能保证代码块的原子性
CAS 机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。 比如需 要保证 3
个变量共同进行原子性的更新 , 就不得不使用 synchronized 了。
造成CPU利用率的增加
什么是Future?
通过实现 Callback 接口 , 并用Future 可以来接收多线程的执行结果。
Future 表示一个可能还没有完成的异步任务的结果 , 针对这个结果可以添加 Callback 以便在任务执行成功或失败后做出相应的操作。
package java.util.concurrent;
public interface Future<V> {
boolean cancel(boolean var1);
boolean isCancelled();
// 判断线程任务有没有执行完
boolean isDone();
// 阻塞等待结果
V get() throws InterruptedException, ExecutionException;
// 阻塞等待结果,有超时时间
V get(long var1, TimeUnit var3) throws InterruptedException, ExecutionException, TimeoutException;
}
ThreadPoolExecutor#submit() 返回 Future对象。
什么是 AQS
AQS 是 AbustactQueuedSynchronizer 的简称 , T他是一个抽象类,JUC下面的很多内容都是基于AQS来实现的。
首先,AQS中提供了一个由volatile修饰,并且采用CAS方式修改的int类型的state变量。
其次,AQS红维护了一个双向链表,有head,有tail,并且每个节点都是Node对象。
其底层提供了获取锁和释放锁等等的逻辑。
state表示线程的同步状态,也叫做锁的持有计数。
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大 量的同步器 , 比如我们提到的 ReentrantLock , Semaphore ,其他的诸如 ReentrantReadWriteLock ,SynchronousQueue , FutureTask 等等皆是基于 AQS 的。
AQS 支持两种同步方式 :
1 、 独占式
2 、 共享式
这个我们可以看aqs的源码中Node节点定义的类型能够看到,说白了就是你追加的是独占节点还是共享节点。
FutureTask 是什么
这个其实前面有提到过,FutureTask 表示一个异步运算的任务。
FutureTask 里面可以传 入一个 Callable 的具体实现类 , 可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。
当然 ,由于 FutureTask 也是 Runnable 接口的实现类 ,所以 FutureTask 也可以放入线程池中。
Java 中用到的线程调度算法是什么
抢占式
并发编程三要素(面试有被问到)
原子性、有序性、可见性
怎样在多线程运行过程中获取它的运行状态(面试有被问到)
1.ExecutorService类它的submit方法执行完成之后,会返回一个future 对象,然后我们可以通过future.get方法拿到返回结果。
2.我们可以创建一个FutrueTask 的任务,然后将这个任务放到Thread线程池中进行执行,最后我们可以通过futuretask.get()方法,拿到线程执行的结果。
3.当然我们创建线程的时候可以使用实现callable接口的方式,也是能够拿到线程的执行结果的。
4.我们也可以通过completableFutrue这个类的一些方法传入一个异步任务,然后通过它的get方法也是能够拿到多线程的一个执行结果的。
提高QPS(Queries Per Second,每秒查询率)的手段主要包括以下几个方面(面试有被问到):
- 硬件优化:
- 提升CPU性能:选择高性能的多核心CPU,增加处理能力。
- 扩展内存容量:增加服务器内存容量,提高内存访问速度,防止因内存不足而导致的性能瓶颈。
- 使用固态硬盘(SSD):SSD的读写速度比传统机械硬盘更快,可以提高数据读写效率。
- 增加网络带宽:通过升级网络设备或增加带宽,提高服务器对外访问的速度,降低网络延迟12。
- 软件优化:
- 选择高性能的Web服务器:如Nginx或Apache,提供更高的QPS能力。
- 调整服务器参数:如TCP连接数、文件描述符数等,以提高并发处理能力。
- 数据库优化:包括索引优化、查询优化、事务处理优化等,减少数据库操作对服务器性能的影响。
- 使用缓存技术:如Redis或Memcached,将常用数据缓存在内存中,减少对数据库的访问,提高响应速度。
- 负载均衡:
- 使用负载均衡器:将请求分发到多台服务器上处理,提高整体处理能力。
- 水平扩展:通过增加服务器数量,配置负载均衡器,实现水平扩展,提高整体QPS能力。
- 分布式架构:
- 数据分片:将数据分片存储在多台服务器上,提高查询和写入的速度。
- 任务分片:将任务拆分成多个子任务,分配给不同的服务器处理,提高并行处理能力。
- 服务拆分:将不同功能的服务拆分成独立的模块,分布在不同的服务器上,提高整体处理能力。
- 代码优化:
- 优化代码逻辑:检查代码中的性能瓶颈,对耗时的操作进行优化,使用更高效的算法和数据结构。
- 异步处理:对于一些不影响主流程的操作,采用异步方式进行处理,避免阻塞主线程。
- 监控与维护:
- 建立性能监控系统:实时监测系统的QPS、响应时间、服务器负载等指标,及时发现性能问题并进行优化。
通过这些手段的综合应用,可以有效提升系统的QPS,满足高并发场景下的需求。
线程调用两次start方法会有什么问题(面试有被问到)
会报错,会抛出IllegalThreadStateException异常。
如果线程正在运行,你又调了一次start方法,就相当于让线程在启动一次,这不符合逻辑,我们可以根据在调用start方法的源码看出,
if (threadStatus != 0)
throw new IllegalThreadStateException();
多线程之间的通信(面试有被问道)
可以使用volitile关键字进行共享变量的修饰。
Thread类里面有一个inheritableThreadLocals的变量,我们可以通过这个变量的get方法来获取到父线程的一个值。
我们也可以将要操作的共享数据放入到消息队列中,这样多个线程操作共享变量,也相当于是进行了线程间的通信了。
线程通讯指的是多个线程之间通过共享内存或消息传递等方式来协调和同步它们的执行。在多线程编程中,通常会出现多个线程需要共同完成某个任务的情况,这时就需要线程之间进行通讯,以保证任务能够顺利地执行。
线程通讯的实现方式主要有以下两种:
共享内存:多个线程可以访问同一个共享内存区域,通过读取和写入内存中的数据来进行通讯和同步。
消息传递:多个线程之间通过消息队列、管道、信号量等机制来传递信息和同步状态。
常见场景
线程通讯的常见场景有以下几个:
1.多个线程共同完成某个任务:例如一个爬虫程序需要多个线程同时抓取不同的网页,然后将抓取结果合并保存到数据库中。这时需要线程通讯来协调各个线程的执行顺序和共享数据。
2.避免资源冲突:多个线程访问共享资源时可能会引发竞争条件,例如多个线程同时读写一个文件或数据库。这.时需要线程通讯来同步线程之间的数据访问,避免资源冲突。
3.保证顺序执行:在某些情况下,需要保证多个线程按照一定的顺序执行,例如一个多线程排序算法。这时需要线程通讯来协调各个线程的执行顺序。
4.线程之间的互斥和同步:有些场景需要确保只有一个线程能够访问某个共享资源,例如一个计数器。这时需要使用线程通讯机制来实现线程之间的互斥和同步。
实现方法
线程通讯的实现方法有以下几种:
- 等待和通知机制:使用 Object 类的 wait() 和 notify() 方法来实现线程之间的通讯。当一个线程需要等待另一个线程执行完某个操作时,它可以调用 wait() 方法使自己进入等待状态,同时释放占有的锁,等待其他线程调用 notify() 或 notifyAll() 方法来唤醒它。被唤醒的线程会重新尝试获取锁并继续执行。
- 信号量机制:使用 Java 中的 Semaphore 类来实现线程之间的同步和互斥。Semaphore 是一个计数器,用来控制同时访问某个资源的线程数。当某个线程需要访问共享资源时,它必须先从 Semaphore 中获取一个许可证,如果已经没有许可证可用,线程就会被阻塞,直到其他线程释放了许可证。
- 栅栏机制:使用 Java 中的 CyclicBarrier 类来实现多个线程之间的同步,它允许多个线程在指定的屏障处等待,并在所有线程都达到屏障时继续执行。
- 锁机制:使用 Java 中的 Lock 接口和 Condition 接口来实现线程之间的同步和互斥。Lock 是一种更高级的互斥机制,它允许多个条件变量(Condition)并支持在同一个锁上等待和唤醒。
# 具体代码实现
public class ConditionDemo {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private volatile boolean flag = false;
public static void main(String[] args) {
ConditionDemo demo = new ConditionDemo();
new Thread(demo::waitCondition).start();
new Thread(demo::signalCondition).start();
}
private void waitCondition() {
lock.lock();
try {
while (!flag) {
System.out.println(Thread.currentThread().getName() + " is waiting for signal.");
condition.await();
}
System.out.println(Thread.currentThread().getName() + " received signal.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private void signalCondition() {
lock.lock();
try {
Thread.sleep(3000); // 模拟等待一段时间后发送信号
flag = true;
System.out.println(Thread.currentThread().getName() + " sends signal.");
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
在这个示例中,创建了一个 Condition 对象和一个 Lock 对象,然后创建了两个线程,一个线程等待 Condition 信号,另一个线程发送 Condition 信号。
等待线程在获得锁后,判断标志位是否为 true,如果为 false,则等待 Condition 信号;如果为 true,则继续执行后面的任务。
发送线程在获得锁后,等待一段时间后,将标志位设置为 true,并且发送 Condition 信号。 运行以上程序的执行结果如下:
Thread-0 is waiting for signal. Thread-1 sends signal. Thread-0 received signal.
从上面执行结果可以看出,等待线程在等待 Condition 信号的时候被阻塞,直到发送线程发送了 Condition 信号,等待线程才继续执行后面的任务。Condition 对象提供了一种更加灵活的线程通信方式,可以精确地控制线程的等待和唤醒。