深入理解Java多线程(二):多线程问题学习总结
学了一段时间多线程方面的知识了总感觉掌握的知识有些散乱,在网上搜了一些面试问题总结梳理一下。
多线程
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