学了一段时间多线程方面的知识了总感觉掌握的知识有些散乱,在网上搜了一些面试问题总结梳理一下。
多线程
java中有几种方法可以实现一个线程?
Java中有四种方式实现一个线程。
- 通过实现一个Runnable
- 继承Thread并重写run方法
- 继承Callable,用Future接收可以实现异步调用
- 使用线程池(executer)提交作业
如何停止一个正在运行的线程?
使用suspend可以挂起一个线程,使用stop可以终结一个线程,但这些方法已经不推荐使用,存在安全性问题,停止一个线程最好的方式是让它自然结束,常用的方式是使用一个volatile变量来控制线程是否继续运行。
suspend、stop不再推荐使用的原因是容易引发死锁,当一个线程持有锁的时候它被挂起或暂停掉了,此时锁还没有释放,其他线程无法获取所就会出现思索,最好的方式是让线程自然结束。
notify()和notifyAll()有什么区别?
notify唤醒在该对象监视器上的一个线程,而notifyAll唤醒所有线程。
这两个方法都是本地方法,都需要在同步代码块中使用。
sleep()和 wait()有什么区别?
- sleep()是Thread类的静态方法, wait()是Object类的方法
- sleep()让线程阻塞, 让出CPU, 但不释放锁; wait()使当前线程释放锁, 并进入等待队列
- sleep()可以在任意方法内使用, 而wait只能在同步代码里使用(同步方法或同步快中)
- sleep()方法使用时需要捕获InterruptedException异常, wait()/notify()/notifyAll()则不需要
参考:
[1] https://www.cnblogs.com/116970u/p/11506663.html
[2] https://blog.csdn.net/qq_42928918/article/details/88404713
[3] https://www.cnblogs.com/ConstXiong/p/11993363.html
什么是Daemon线程?它有什么意义?
Deamon是守护线程,守护线程是为其他线程服务的线程。在系统中如果仅有守护线程那么系统将终止。
java如何实现多线程之间的通讯和协作?
- volatile和synchronized
- wait、notify、join
- PipedInputStream & PipedOutputStream
- ThreadLocal
start()方法和run()方法的区别
- start方法最终会调用本地方法start0,只有调用start方法后才具有线程的特性;run方法是一个无参无返回值的方法,在创建Thread时使用lambda或任意无参无返回值可以代替
- 通过start方法执行run和直接调用run方法的区别是:
- 通过start方法执行run方法可以交叉执行run方法代码,单独执行run方法,其方法不能交叉执行,即代码不是共享的
- 执行调用run方法执行时是当前线程执行了代码,而start是一个新的线程
一个线程如果出现了运行时异常会怎么样
- 如果一个线程出现异常他会让出锁,很可能会造成数据缺失。
- 如果是在synchronized同步方法或同步块中, 线程会主动释放掉锁
Java中如何获取到线程dump文件
死循环、死锁、阻塞、页面打开慢等问题,打印线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:
(1)获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java
(2)打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid
另外提一点,Thread类提供了一个 getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈,
https://blog.csdn.net/weixin_30376453/article/details/97711928
如何在两个线程之间共享数据
线程之间通信可以通过volatile声明共享变量、synchronized同步的代码可通过wait/notify方式通信; 使用Exchanger类实现在两个之间类交换数据。
Java中常用的锁工具有:ReentrantLock、ReentrantWriteReadLock
Java中常用的同步工具:Semophare、CyclicBarrar、Phaser、CountDownLatch
ThreadLocal有什么用
ThreadLocal是线程的一个本地变量,通过ThreadLocal为线程设置与线程绑定的数据,ThreadLocal设置的数据(ThreadLocalMap)是在Threa的对象上的。
一个线程可以设置多个ThreadLocal数据,一个ThreadLocal可以为多个线程设置变量,但只能为一个线程设置一个变量,因为ThreadLocal以自己的hashCode为键存入Map。
为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用
这是JVM的规定。
调用这些方法不是通过线程对象,而是锁对象来调用的,如果不再同步代码块中调用锁对象的方法就没有意义,因为不会有其他线程和当前线程竞争而阻塞。
wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别
wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。
怎么检测一个线程是否持有对象监视器
通过Thread的holdsLock方法返回的结果检测。
Thread.holdsLock(lock);
synchronized和ReentrantLock的区别
synchronized是实现同步的关键字,ReetrantLock是实现Lock接口的一个锁工具类。
它们都能实现同步,都是可重入锁。
synchronized具有锁升级行为,在同步时可能是偏向锁、轻量级锁、重量级锁。ReentrantLock使用volatile+CAS方式实现非阻塞同步。
怎么唤醒一个阻塞的线程
lock.notify();
lock.notifyAll();
LockSupport.unPark();
什么是多线程的上下文切换
上下文切换是要CPU要执行另一个线程要保持当前线程寄存器里的信息,把另一个线程加载进来。
如果你提交任务时,线程池队列已满,这时会发生什么
这里区分一下:
-
如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关
系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务 -
如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy
参考:
[1] https://www.cnblogs.com/programb/p/13021272.html
Java中用到的线程调度算法是什么
抢占式调度算法,这是操作系统使用较多的调度方式,其他调度方式还是有:FIFO调度、优先级调度、非抢占式调度、时间轮转调度。
Thread.sleep(0)的作用是什么
sleep() 方法让线程进入阻塞,让出CPU,但不释放锁。
Thread.sleep(0)的作用,就是“触发操作系统立刻重新进行一次CPU竞争”,竞争的结果可能是当前线程仍然获得CPU控制权,也可能是别的线程获得CPU控制权。
什么是自旋
自旋的意思是线程在CPU空转而不进行作业处理,也就是空循环,使用CAS进行非阻塞同步时就是这种操作。
线程类的构造方法、静态块是被哪个线程调用的
线程类的构造方法、静态块是被 new 这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑,那么举个例子,假设 Thread1 中 new 了
Thread2,main 函数中 new 了 Thread1,那么:
- Thread1 的构造方法、静态块是 main 线程调用的,Thread1 的 run()方法是 Thread1 自己调用的
- Thread2 的构造方法、静态块是 Thread2 调用的,Thread2 的 run()方法是 Thread2 自己调用的
参考:
[1] https://www.cnblogs.com/programb/p/13019225.html
同步方法和同步块,哪个是更好的选择
同步块更好,同步方法一般比同步块粒度大,意味这同步所需要的时间更多,相应的性能更低。
锁
什么是可重入锁(ReentrantLock)?
可重入锁是同一个线程可以多次持有的锁。在Java中synchronized是可重入锁,ReetrantLock也是可重入锁。
ReetrantLock是java.util.concurrent.locks
包下的一个锁对象,实现了Lock接口,内部实现了公平锁和非公平锁。
ReentrantLock实现了公平锁和非公平锁,默认是非公平锁。
当一个线程进入某个对象的一个synchronized的实例方法后,其它线程是否可进入此对象的其它方法?
要看synchronized加锁的位置。
- 如果是加在方法上那不允许其他线程再进入这个方法,这些方法公用这个实例为锁对象,进去前需要获得锁
- 如果是在这个方法内部对一部分代码加锁(synchronized)则允许进入方法而不允许进入同步代码块。
synchronized和java.util.concurrent.locks.Lock的异同?
synchronized是Java的一个同步关键字,可以用在方法上或者直接使用一个对象为一部分代码加锁,Lock是一个接口,定义了实现同步的操作,如lock,tryLock、unLock等,实现Lock的对象有ReetrantLock。
synchronized和Lock的实现类都是实现同步,synchronized实现同步依赖的是操作系统的锁,线程切换、线程获取锁、释放锁开销较大,JDK6对synchronized进行了改进,把原来直接使用操作系统的锁改为了锁升级的方式,当竞争锁的线程数到达一个阈值就会升级为操作系统锁(重量级锁);而实现Lock的对象加锁的方式主要是CAS+volatile,这样在不进入操作系统内核态就可以实现非阻塞同步。
乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
乐观锁、悲观锁并不是一种真是存在的锁,而是一种锁的设计思想。
悲观锁是一种悲观思想,它总认为最坏的情况可能发生:数据数据会被其他人修改,所以悲观锁在持有数据是总会把资源或数据锁住,其他线程请求这个资源时就会阻塞,比如数据库的表锁、行锁、读锁、写锁,都是在操作之前把资源先上锁。Java中的synchronized和ReentrantLock等独占锁也是悲观锁思想的实现,因为无论synchronized和ReenstrantLock是否持有资源它都会尝试去加锁。
乐观锁的思想与悲观锁的思想相反,它总任务资源和数据不会被别人就该,所以读取不会上锁,但乐观锁在进行写入前会判断当前数据是否被修改过。乐观锁一般来说有两种实现:版本号机制和CAS。乐观锁适用于多读类型,可以提高吞吐量。Javajava.util.concurrent.atomic
包下面就是使用了乐观锁的思想实现的(CAS)。
参考:
[1] https://blog.csdn.net/weixin_39968722/article/details/110363573
并发框架
SynchronizedMap和ConcurrentHashMap有什么区别?
SynchronizedMap和ConcurrentHashMap都能保证线程安全。
SynchronizedMap本质时对Map的包装,使用了静态代理模式,代理Map的读写方法以保证操作的安全性。
ConcurrentHashMap是一个安全的并发容器,内部使用分段锁提高了并发性能。
CopyOnWriteArrayList可以用于什么应用场景?
CopyOnWriteArrayList是CopyOnWrite写时拷贝思想设计的容器,写操作很重,读操作很轻,适合有大量读、少量写操作的并发访问情况。
几个要点
- 实现了List接口
- 内部持有一个ReentrantLock lock = new ReentrantLock();
- 底层是用volatile transient声明的数组 array
- 读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array
CopyOnWriteArrayList 透露的思想
- 读写分离,读和写分开
- 最终一致性
- 使用另外开辟空间的思路,来解决并发冲突
参考:
[1] https://www.cnblogs.com/myseries/p/10877420.html
CyclicBarrier和CountDownLatch的区别
CountDownLatch提供了一个线程安全的计数器,功能类似与Thread的join()方法的功能,实现父线程在子线程执行完后执行操作,父线程在此期间进行阻塞,在使用CountDownLatch时一般初始化为线程的数量,在线程执行run()方法中调用计数器减小方法,当计数器的值大于0时CountDownLatch的await会阻塞。
CyclicBarrier(循环栅栏)是一个可以复用的计数器,计数器大于0时阻塞进程,当计数器到达0就会释放这一批线程执行作业,还可以使用带有回调的方法,但是如果计数器的数值不到达0,那么已经阻塞的线程将不会执行。举个例子:每个线程代表一个跑步运动员,当运动员都准备好后,才一起出发,只要有一个人没有准备好,大家都等待!
区别:
- CountDownLatch: 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
- CyclicBrrier: N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
参考:
[1] https://blog.51cto.com/u_3265857/2336194
ConcurrentHashMap的并发度是什么
ConcurrentHashMap使用了分段锁,最多支持16段,因此ConcurrentHashMap的并发度是16,意味着最多可由16个线程同时访问。
/**
* The default concurrency level for this table.
*/
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
FutureTask是什么
https://blog.csdn.net/qq_39654841/article/details/90631795
什么是AQS
AQS是AbstractQueuedSynchronizer,juc下的一个同步器,里面封装了同步的方法,使用时继承AQS,并重写一部分方法就可以实现一个同步器(锁),AQS同步是采用的是CAS+voliatile相较于synchronized在执行时间短的同步下效率较高。
AQS如何优化
- 避免死锁;
- 减少锁的持有时间;
- 减少锁的粒度;
- 锁的分离(读写分离);
- 尽量使用无锁的操作,比如原子操作(Atomic系列类)、volatile关键字;
线程安全
什么叫线程安全?servlet是线程安全吗?
线程安全指在不使用额外的额外的同步工具程序能在任意多线程环境下均能得到正确的结果。
在Java种不可变对象如String、包装数据类型都是线程安全的,通过锁工具能保证线程安全。
servlet不是线程安全的,感兴趣可参考: Servlet是线程安全的吗?
同步有几种实现方法(怎样包装同步)?
- synchronized
- CAS + volatile
- ReentrantLock、ReentrantReadWriteLock等锁类实现同步
- 同步的容器,如:SynchronizedMap、ConcurrentHashMap...
volatile有什么用?能否用一句话说明下volatile的应用场景?
1.volatile的作用:
- 禁止指令重排序
- 使多线程间共享的变量可见
- 用volatile声明CAS比较时的值;声明线程中检查的控制变量。
请说明下java的内存模型及其工作流程。
Java内存模型是线程读写内存数据的规范,线程向内存读写数据由JMM控制。
为什么代码会重排序?
代码编译时会对无依赖的操作调整顺序以提高执行效率。
重排分为三部分:编译器重排、CPU重排、内存访问重排
单例模式的线程安全性
单例模式的安全性是保证任何情况下只创建一个线程,一些单例在多线程情况下会创建多个对象
单例模式有:
- 懒汉单例 - 在编译时编译时类加载进行初始化,线程安全
- 饿汉单例 - 在第一次调用时初始化,线程不安全,多线程环境下会创建多个对象
- 枚举单例 - 通过JVM类加载机制保证了线程安全
- DCL单例(双检锁单例,DCL Double Check Lock) - 是饿汉单例的改进,线程安全
高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
- 并发高,任务执行短的业务,线程数设置为N+1(N核心数),减少线程上线文切换开销
- 并发不高,任务执行时间长的业务,有两种情况
- IO密集型,这是候应加大线程数量,以充分利用CPU,处理更多业务
- 计算密集型,这时候线程数量应该尽量少,尽量和核心数保持一致如N+1,减少线程上下文切换,让CPU集中资源处理计算
- 并发高、业务执行时间长,解决这个问题不在线程池而在于架构,第一步看看这些业务是否可以增加缓存,增加服务器是第二步,最后,执行业务时间的问题需要分析一些,看看能否使用中间件对任务进行拆分解耦。
其他
生产者消费者模型的作用是什么
https://www.cnblogs.com/Rivend/p/12071128.html
Java编程写一个会导致死锁的程序
public class DeadLock1 {
public static void main(String[] args) {
final Object lock1 = new Object();
final Object lock2 = new Object();
Thread t1 = new Thread(()->{
synchronized (lock1){
System.out.println("t1 start");
synchronized (lock2){
System.out.println("t1 inner");
}
System.out.println("t1 end");
}
},"t1");
Thread t2 = new Thread(()->{
synchronized (lock2){
System.out.println("t2 start");
synchronized (lock1){
System.out.println("t2 inner");
}
System.out.println("t2 end");
}
},"t2");
t1.start();
t2.start();
}
}
HashMap在并发环境下put方法会导致链表或红黑树形成环路,一直对环遍历,会导致put方法阻塞跑满CPU。
以下代码有几率发生线程安全问题,多运行次就会出现。
// HashMap并发问题,put空循环
// 若不出现循环,最终main会进行输出
public class DeadLock2 {
static Map<String,String> map = new HashMap<>();
public static void main(String[] args) {
List<Thread> threadList = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
threadList.add(new Thread(()->{
for (int i1 = 0; i1 < 10000; i1++) {
map.put(UUID.randomUUID().toString(),UUID.randomUUID().toString());
}
}));
}
for (Thread t : threadList) {
t.start();
}
for (Thread t : threadList) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("所有线程执行完毕");
}
}
并发编程推荐文章:
[1] http://ifeve.com/talk-concurrency/
问题来源及参考:
[1] http://ifeve.com/javaconcurrency-interview-questions-base/
[2] http://ifeve.com/javaconcurrency-interview-questions-combat/
[3] https://blog.csdn.net/weixin_30376453/article/details/97711928
评论区