一、基础知识
1. 为什么要使用并发编程
- 充分利用多核CPU的计算能力;
- 方便进行业务拆分,提升系统并发能力和性能:在特殊的业务场景下,先天的就适合于并发编程。现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。
2. 并发编程有什么缺点
- 并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、线程安全、死锁等问题
3. 并发编程三个必要因素是什么?
- 并发编程三要素(线程的安全性问题体现在):
- 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
- 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
- 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
出现线程安全问题的原因:
- 线程切换带来的原子性问题;
- 缓存导致的可见性问题;
- 编译优化带来的有序性问题;
解决办法:
- JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
- synchronized、volatile、LOCK,可以解决可见性问题
- Happens-Before 规则可以解决有序性问题
4 多线程的劣势
- 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
- 多线程需要协调和管理,所以需要 CPU 时间跟踪线程;
- 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
5 线程和进程区别
- 进程
- 一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
- 线程
- 进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
- 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位;
- 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小;
- 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;
- 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的;
- 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
- 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行;
6 上下文切换
- 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
- 概括来说就是:当前任务在执行完 CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
- 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
- Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
7 守护线程和用户线程有什么区别呢?
- 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
- 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的“佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作
- main 函数所在的线程就是一个用户线程啊,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。
- 比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。
注意事项: - setDaemon(true)必须在start()方法前执行,否则会抛出 IllegalThreadStateException 异常
- 在守护线程中产生的新线程也是守护线程
- 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
- 守护 (Daemon) 线程中不能依靠 finally块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行
8 线程死锁
9. 形成死锁的四个必要条件
- 互斥条件:在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等待,直至占有资源的进程用完释放。
- 占有且等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
- 不可抢占条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A在等B,B在等C,C在等A)
10. 避免线程死锁
- 避免一个线程同时获得多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
11. 创建线程的四种方式
- 继承Thread类:
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run()方法正在执
行...");
}
- 实现 Runnable 接口
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run()方法执行
中...");
}
- 实现 Callable 接口;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
System.out.println(Thread.currentThread().getName() + " call()方法执行
中...");
return 1;
}
- 使用匿名内部类方式
public class CreateRunnable {
public static void main(String[] args) {
//创建多线程创建开始
Thread thread = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("i:" + i);
}
}
}
);
thread.start();
}
}
12. runnable 和 callable 区别
13. 线程的 run()和 start()区别
- 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。
- start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start()只能调用一次。
- start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码;
此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束,
此线程终止。然后CPU再调度其它线程。
14. 用 start() 方法时会执行 run() 方法,为什么不能直接调用 run() 方法?
15. Callable 和 Future
16. FutureTask
FutureTask 表示一个异步运算的任务。 FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是 Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。
17. 线程的状态
18. Java 中线程调度算法
19. 线程调度器(Thread Scheduler)和时间分片(Time Slicing )
20. sleep() 和 wait() 区别
21. 为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?
22. 为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?
23. Thread 类中的 yield 方法有什么作用?
24. 为什么 Thread 类的 sleep()和 yield ()方法是静态的?
25. sleep()方法和 yield()方法区别
26. 停止一个正在运行的线程
27. interrupted 和 isInterrupted 方法区别
28. 阻塞式方法
29. 唤醒一个阻塞的线程
30. notify() 和 notifyAll() 区别
31. 在两个线程间共享数据
32. 如何实现多线程之间的通讯和协作
33. 同步方法和同步块,哪个是更好的选择?
34. 如果你提交任务时,线程池队列已满,这时会发生什么?
35. 线程优先级
36. 线程类的构造方法、静态块是被哪个线程调用的
37. Java 中怎么获取一份线程 dump 文件?你如何在 Java 中获取线程堆栈?
- Dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中。
- 在 Linux 下,你可以通过命令 kill -3 PID (Java 进程的进程 ID)来获取 Java应用的 dump 文件。
- 在 Windows 下,你可以按下 Ctrl + Break 来获取。这样 JVM 就会将线程的 dump 文件打印到标准输出或错误文件中,它可能打印在控制台或者日志文件中,具体位置依赖应用的配置。
38. 一个线程运行时发生异常会怎样?
- 如果异常没有被捕获该线程将会停止执行。
- Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。
- 当一个未捕获异常将造成线程中断的时候,JVM会使用Thread.getUncaughtExceptionHandler()来查询线程UncaughtExceptionHandler并将线程和异常作为参数传递给 handler 的 uncaughtException()方法进行处理。
39 . Java 线程数过多会造成什么异常?
40. 多线程的常用方法
二 、并发理论
1. 线程通信
2. 重排序
3. 并发关键字 synchronized ; synchronized 底层实现原理
4. 使用 synchronized 关键字
5. synchronized可重入的原理
6. 自旋
7. 多线程中 synchronized 锁升级的原理
8. synchronized、volatile、CAS 比较
- (1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
- (2)volatile提供多线程共享变量可见性和禁止指令重排序优化。
- (3)CAS 是基于冲突检测的乐观锁(非阻塞)
9. synchronized 和 Lock 区别
- 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
- synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁;
- synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁;
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到;
10. synchronized 和 ReentrantLock 区别
11. volatile 关键字
12. Java 中能创建 volatile 数组吗?
13. volatile 变量和 atomic 变量有什么不同?
14. volatile 能使得一个非原子操作变成原子操作吗?
15. synchronized 和 volatile 的区别是什么?
16. final不可变对象,它对写并发应用有什么帮助?
17. 乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
18. CAS
19. CAS 会产生什么问题
21. 原子类
22. 原子类的常用类
- AtomicBoolean
- AtomicInteger
- AtomicLong
- AtomicReference
23. Atomic原理
- Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
24. 死锁与活锁的区别,死锁与饥饿的区别?
三、线程池
1. 线程池
2. ThreadPoolExecutor
3. Executors
4. 线程池四种创建方式
5. 在 Java 中 Executor 和 Executors 的区别?
6. 四种构建线程池的区别及特点
7. 线程池状态
8. 线程池中 submit() 和 execute() 方法区别
9. 线程组,为什么在 Java 中不推荐使用?
10. ThreadPoolExecutor饱和策略有哪些?
11. 线程池的执行原理
12. 合理分配线程池大小
四、并发容器
1. 并发容器
- Vector、ConcurrentHashMap、HasTable
- 一般软件开发中容器用的最多的就是HashMap、ArrayList,LinkedList ,等等
- 但是在多线程开发中就不能乱用容器,如果使用了未加锁(非同步)的的集合,你的数据就会非常的混乱。由此在多线程开发中需要使用的容器必须是加锁(同步)的容器。
2. Vector
3. ConcurrentHashMap,和HashTable的不同
4. Java 中的同步集合与并发集合有什么区别?
5. SynchronizedMap 和 ConcurrentHashMap 区别
6. CopyOnWriteArrayList
合适读多写少的场景。
7. CopyOnWriteArrayList 的设计思想
五、并发队列
1. 什么是并发队列:
消息队列很多人知道:消息队列是分布式系统中重要的组件,是系统与系统直接的通信
并发队列是什么:并发队列多个线程以有次序共享数据的重要组件
2. 并发队列和并发集合的区别:
队列遵循 “ 先进先出 ” 的规则,可以想象成排队检票,队列一般用来解决大数据量采集处理和显示 的。
并发集合就是在多个线程中共享数据的
3. 怎么判断并发队列是阻塞队列还是非阻塞队列
在并发队列上 JDK 提供了 Queue 接口,一个是以 Queue 接口下的 BlockingQueue 接口为代表的阻塞 队列,另一个是高性能(无堵塞)队列。
4. 阻塞队列和非阻塞队列区别
当队列阻塞队列为空的时,从队列中获取元素的操作将会被阻塞。
或者当阻塞队列是满时,往队列里添加元素的操作会被阻塞。
或者试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。
试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来
5. 常用并发列队的介绍:
六、并发工具类