Java面试-线程安全篇
一、synchronized关键字:
- 基本使用与作用:通过抢票代码示例,展示了
synchronized
作为对象锁,可避免多线程超卖或抢到同一张票问题,保证代码原子性,同一时刻只有一个线程获得锁,其他线程阻塞。 - 底层原理:底层是
monitor
,通过分析字节码信息,可看到monitor enter
和monitor exit
,分别对应上锁和解锁,且有两个monitor exit
以防异常导致锁未释放。 monitor
工作机制:monitor
有wait set
、entry list
、owner
三个属性。线程尝试获取锁时,若owner
为none
,则拥有锁;否则进入entry list
等待,释放锁后唤醒entry list
中线程争抢。调用wait
方法的线程进入wait set
。- 锁升级:
monitor
是重量级锁,因涉及用户态和内核态切换,性能低。JDK1.6后引入偏向锁和轻量级锁。一个线程重复获取锁时用偏向锁;两个线程交替获取锁用轻量级锁;多线程竞争时用重量级锁。 - 三种锁对比:偏向锁性能最好,首次加锁记录线程ID并改标志,后续重入只需判断;轻量级锁每次加锁记录都需CAS操作;重量级锁涉及用户态和内核态切换,性能最低。
二、Java内存模型(JMM)面试题:
- 概念:定义了共享变量在多线程读写操作的行为规范,保证内存读写指令正确性。
- 内存划分:分为工作内存和主内存。工作内存是线程私有,存储线程内私有数据,线程间无法互相访问;主内存是共享区域,存储共享变量,多线程同步数据需通过主内存。
- CAS操作面试题:
- 概念与原理:全称
compare and swap
,体现乐观锁思想,在无锁情况下保证线程操作共享数据的原子性。线程操作共享数据时,先读取主内存数据到工作内存,修改后与主内存数据比对,相同则更新,不同则自旋重试。 - 底层实现:依赖
unsafe
类调用操作系统底层CAS指令,unsafe
类提供compare and swap object
、int
、long
三个本地方法。 - 与锁的对比:CAS是乐观锁,不怕其他线程修改共享变量,通过自旋重试;
synchronized
是悲观锁,上锁后防止其他线程修改。
- 概念与原理:全称
三、volatile`关键字:
-
含义:可修饰共享变量,保证线程间可见性和禁止指令重排序。
-
线程间可见性:通过代码示例,展示了未加
volatile
时,线程修改共享变量,其他线程不可见,原因是JIT编译器优化。可通过加VM参数禁用JIT或加volatile
关键字解决,项目开发中推荐使用volatile
关键字。 -
指令重排序与volatile关键字:
- 指令重排序概念:代码执行顺序可能与编写顺序不一致,通过测试代码可能出现结果为10的情况判断是否发生重排序,需使用高并发多线程测试工具。
- volatile解决重排序:在共享变量读写时加入不同屏障,阻止其他读写操作越过屏障。如加在y上能解决问题,加在x上不行。
- volatile使用技巧:写变量时让volatile修饰的变量在代码最后位置,读变量时让其在代码最上面。
- volatile特性总结:保证线程间可见性,禁止指令重排序。
四、AQS(抽象队列同步器):
- AQS定义与对比:是JUC中提供的锁机制,与synchronized对比,它是Java API,需手动开关锁,在锁竞争激烈时性能更好。
- 基本工作机制:内部有volatile修饰的state,0表示无锁,1表示有锁。线程请求锁需修改state,失败则进入队列等待,队列为先进先出的双向队列。
- 保证原子性:多个线程抢资源时用CAS设置保证原子性。
- 公平锁与非公平锁:非公平锁是新线程与队列中线程共同抢资源;公平锁是新线程到队列尾部等待,按队列顺序获得锁。
- AQS总结:是队列同步器,维护双向队列,用state控制锁,使用CAS保证原子性。
五、ReentrantLock(可重入锁):
- 特点:可中断、可设置超时时间、可设置公平锁、支持多个条件变量,与synchronized一样支持重入。
- 实现原理:底层使用CAS和AQS,构造函数可创建公平锁或非公平锁。
- 工作方式:与AQS类似,用CAS修改状态抢锁,抢到锁将线程挂到属性中,释放锁唤醒队列中线程。
- 总结:是可重入锁,底层用CAS和AQS,支持公平和非公平锁。
六、synchronized与Lock对比:
- 语法层面:synchronized是关键字,由JVM提供,C++实现,退出同步块自动释放锁;Lock由JDK提供,Java实现,需调用unlock释放锁。
- 功能层面:都属悲观锁,有互斥同步和锁重入功能,但Lock有公平锁、可打断、可超时、多条件变量等功能,通过代码演示验证。
- 性能层面:无竞争时synchronized有优化,性能还行;竞争激烈时Lock性能更好。
七、死锁问题:
- 死锁产生条件:一个线程同时获取多把锁易发生死锁,如线程T1持有a锁等b锁,线程T2持有b锁等a锁。
- 死锁诊断工具:使用jps输出JVM中运行的所有进程状态信息,使用jstack查看Java进程内的线程堆栈信息;还可使用jconsole和VisualVM等可视化工具。
八、ConcurrentHashMap:
- JDK1.7结构:采用分段的数组加列表实现,数组不可扩容,添加数据时用ReentrantLock加锁,性能较低。
- JDK1.8结构:采用数组加链表加红黑树结构,添加新节点用CAS自旋锁,有链表或红黑树时用synchronized锁首节点,性能更好。
- 总结:不同版本底层数据结构和加锁方式不同,1.8加锁力度更细。
九、并发程序问题根源:
- 原子性:一个线程在CPU中的操作不可暂停或中断,如抢票代码可能出现超卖,可通过synchronized或Lock锁保证。
- 可见性:一个线程对共享变量修改后让另一个线程可见,可加volatile关键字解决,加锁也可但性能不高。
- 有序性:处理器可能对代码进行指令重排,加volatile关键字可禁止指令重排序。