文章目录
0. 前言
程序员面试本是一件再平常不过的事情,记得刚毕业的时候面试题背的滚瓜烂熟。但是在职程序员面试却是另一回事了,我们往往没有太多时间复习,特别是大龄程序员,工作日忙于工作,周末还要照顾家庭,一旦面临被优化的风险就很被动,难以在短时间内复习并找到工作。不要问我是怎么知道的,都是切身体会,在复习的过程中我也走了不少弯路,所幸最终结果令自己满意。
为了不让和我一样的程序员遇到同样的问题,我打算写这一系列的文章,这些文章不会像其他面经一般大而全,这些文章仅记录我在复习过程中认为重要的知识点,如果能帮助到你就太好了。
1. Java中线程同步的方式有哪些
一句话回答:Java线程同步可以通过多种方式实现,包括同步方法、同步代码块、显式锁、原子变量、volatile关键字、ThreadLocal、不可变对象、并发数据结构、阻塞队列以及同步辅助类等。
细节解释:
- 同步方法:通过
synchronized
关键字自动管理方法访问的同步,确保同一时间只有一个线程可以执行该方法。 - 同步代码块:使用
synchronized
关键字和指定对象,对特定代码段进行同步,提供更细粒度的控制。 - 显式锁(Lock):使用
Lock
接口及其实现类(如ReentrantLock
)来手动控制锁的获取与释放,支持更复杂的同步需求。 - 原子变量:利用
java.util.concurrent.atomic
包中的原子类,通过无锁的CAS操作保证变量的原子性。 - volatile关键字:确保变量的内存可见性,但不保证复合操作的原子性。
- ThreadLocal:为每个线程提供独立的变量副本,实现线程间的数据隔离。
- 不可变对象:设计对象为不可变,确保了对象在构造后状态不变,从而天然线程安全。
- 并发数据结构:使用
java.util.concurrent
包中的线程安全数据结构,如ConcurrentHashMap
,以提高并发程序的性能。 - 阻塞队列(BlockingQueue):用于实现生产者-消费者模式,自动处理线程间的同步问题。
- 同步辅助类:如
CountDownLatch
、CyclicBarrier
、Semaphore
等,用于复杂的线程同步场景。
2. volatile关键字和JMM
一句话回答:volatile
是Java关键字,用于保证变量的可见性,而Java内存模型(JMM)定义了变量如何被线程在内存中访问,包括它们在不同线程之间的可见性、原子性和有序性。
细节解释:
- volatile关键字:
volatile
修饰的变量能够保证每次访问都是对最新值的直接读取,确保变量的可见性。- 当一个线程修改了一个
volatile
变量的值,新值对其他线程立即可见。 volatile
变量不能保证复合操作的原子性,如递增操作i++
。
- Java内存模型(JMM):
- JMM定义了Java程序中各种变量(线程共享变量)的访问规则,包括但不限于线程如何从主内存读取和写入变量值。
- 主内存(Main Memory)是所有线程共享的内存区域,每个线程有自己的工作内存(Working Memory),用于保存对共享变量的副本。
- JMM规定了在什么条件下一个线程对共享变量的修改对其他线程可见,以及如何进行变量的同步。
- 内存屏障(Memory Barrier):
volatile
变量的读写操作在JMM中带有内存屏障,确保指令重排序时不会把volatile
变量的写操作指令重排序到内存屏障之前。
- 有序性:
- JMM允许编译器和处理器对指令进行重排序,但
volatile
变量的读写操作会作为一个happens-before的依据,确保特定操作的有序性。
- JMM允许编译器和处理器对指令进行重排序,但
- 原子性:
- 尽管
volatile
变量的单个读写操作是原子的,但复合操作(如自增)不是原子的,需要其他同步机制来保证。
- 尽管
- 应用场景:
volatile
适用于状态标志(flag)等场景,用来指示某个线程状态的改变,确保所有线程都能看到这一改变。
- 与锁的比较:
- 与锁相比,
volatile
的使用更轻量级,因为它不需要获取和释放锁,但锁提供了更全面的同步机制,包括原子性和复合操作的同步。
- 与锁相比,
3. 常见的容器类,哪些是非线程安全,哪些想线程安全
一句话回答:Java容器类分为线程安全和非线程安全两大类,其中非线程安全容器性能较高,但需手动同步;线程安全容器则在设计时就考虑了同步问题。
细节解释:
3.1 非线程安全容器
- ArrayList:基于动态数组实现,提供快速访问,但并发环境下需手动同步。
- LinkedList:基于双向链表实现,适合频繁插入和删除操作,同样需要外部同步。
- HashMap:通过哈希表存储键值对,提供快速查找,但在多线程环境下需采取额外同步措施。
- HashSet:基于HashMap实现,提供快速元素查找和插入,多线程使用前需同步。
3.2 线程安全容器
- Vector:类似于ArrayList,但所有操作都是同步的,适用于多线程环境。
- Stack:继承自Vector,实现了线程安全的栈操作。
- Hashtable:与HashMap类似,但所有操作都是同步的,性能较低,通常被ConcurrentHashMap替代。
- ConcurrentHashMap:通过分段锁技术提高并发访问性能,适用于高并发场景。
- CopyOnWriteArrayList:在写操作时复制数组,适用于读多写少的场景,实现线程安全。
- CopyOnWriteArraySet:基于CopyOnWriteArrayList实现的线程安全Set。
- ArrayBlockingQueue:使用锁和条件变量实现线程安全的有界阻塞队列。
- LinkedBlockingQueue:使用ReentrantLock和Condition实现线程安全的无界或有界阻塞队列。
- PriorityBlockingQueue:基于优先级堆的无界阻塞队列,所有操作都是同步的。
4. 同步类和JUC包类的性能比较(举例:Hashtable和ConcurrentHashMap)
一句话回答:Hashtable和ConcurrentHashMap是Java中两种不同的线程安全的Map实现,它们在实现线程安全的方式和性能表现上各有特点。
细节解释:
- Hashtable:
- 通过
synchronized
关键字实现线程安全,对所有方法进行了同步,导致在多线程环境下性能较低。 - 适用于线程间需要高度同步且并发量不是很高的场景。
- 通过
- ConcurrentHashMap:
- 在JDK 1.7中采用分段锁机制,通过细分锁的粒度提高并发性能。
- JDK 1.8中摒弃了Segment的概念,采用Node数组配合链表和红黑树,并使用
synchronized
和CAS操作来保证线程安全,整体性能较Hashtable有显著提升。 - 适用于高并发场景,能够提供更高的吞吐量和更低的延迟。
性能比较:
- 在高并发场景下,ConcurrentHashMap通常比Hashtable有更好的性能表现,因为它的锁粒度更细,减少了锁的竞争。
- 在单线程或低并发场景下,Hashtable由于锁的开销较小,可能在某些情况下提供与ConcurrentHashMap相近或略优的性能。
- 在数据结构操作方面,ConcurrentHashMap的扩容操作设计为线程安全的,能够在多线程环境下稳定运行,而Hashtable的扩容则需要在锁定状态下进行。
5. 死锁是什么,如何避免
一句话回答:死锁是多个线程在执行过程中,因争夺资源而造成的一种僵局,其中每个线程都持有一定的资源并等待其他线程释放它们当前需要的资源,而其他线程也处于等待状态,如果没有外力干预,这些线程都无法继续执行。
细节解释:
5.1 死锁发生的必要条件
- 互斥条件:至少有一个线程必须持有一个资源,并且还有其他资源在等待获取。
- 占有和等待条件:线程至少持有一个资源,同时又在等待获取其他线程持有的资源。
- 不可抢占条件:资源不能被抢占,只能由占有它的线程释放。
- 循环等待条件:存在一个线程—资源的循环链,链中的每个线程都在等待下一个线程所占有的资源。
5.2 避免死锁的策略
- 破坏互斥条件:允许一定条件下资源的共享,但这在很多情况下不现实。
- 破坏占有和等待条件:要求线程在开始执行前一次性申请所需的所有资源。
- 破坏不可抢占条件