并发编程(线程安全)面试题及原理
1. Synchronized
1.1 底层原理
Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再根获取这个【对象锁】时就会阻塞住
-
synchronized底层由monitor实现
-
monitor结构
synchronized关键字的底层原理?
- Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
- 它的底层由monitor实现的,monitor是jvm级别的对象(C++实现),线程获得锁需要使用对象(锁)关联monitor
- 在monitor内部有三个属性,分别是owner、entrylist、waitset
- 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程
1.2 锁升级
Monitor实现的锁属于重量级锁,你了解过锁升级吗?
- Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
- 在JDK1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
1.2.1 重量级锁
对象怎么关联上monitor的?
通过ptr…30(指针)关联了monitor
1.2.2 轻量级锁
在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。
加锁流程
- 在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
- 通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
- 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。
- 如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程 - 遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
- 如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue.
- 如果Lock Record的Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀重量级锁。
1.2.3 偏向锁
轻量级锁每添加一个记录,都要进行CAS操作。
偏向锁只会在第一次添加锁的时候,做CAS操作,性能比轻量级锁好。
总结:
Monitor实现的锁属于重量级锁,你了解过锁升级吗?
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被—个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
synchronized | 描述 |
---|---|
重量级锁 | 底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。 |
轻量级锁 | 线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性 |
偏向锁 | 一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令 |
一旦锁发生了竞争,都会升级为重量级锁 |
2. JMM
JMIM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性
工作内存互相独立,不会出现线程安全问题(线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存)
主内存共享内存,可能会出现线程安全问题
工作内存同步数据时要通过主内存进行同步
3. CAS
- CAS的全称是:Compare And Swap(比较再交换),它体现的一种乐观锁的思根,在无锁情况下保证线程操作共享数据的原子性。
- 在JUC(java.util.concurrent)包下实现的很多类都用到了CAS操作
- AbstractQueuedSynchronizer(AQS框架)
- AtomicXXX类
- 在操作共享变量的时候使用的自旋锁,效率上更高一些
- CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现
3.1 数据交换流程
一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功(自旋次数多影响效率,所以设置循环阈值)
3.2 底层实现
底层用c/c++实现——native(本地方法/系统)修饰
3.3 乐观锁与悲观锁
- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
- synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
4. volatile
一旦一个共享变量(类的成员变量、类的静争态成员变量)被volatile修饰之后,那么就具备了两层语义
- 保证线程间的可见性
- 禁止进行指令重排序
4.1 保证线程间的可见性
用volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见
JIT(即时编译器)主要针对热点代码优化
4.2 禁止进行指令重排序
用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果(代码跳着走-先执行第二行再执行第一行)
volatile使用技巧:
- 写变量让volatile修饰的变量的在代码最后位置
- 读变量让volatile修饰的变量的在代码最开始位置
5. AQS
全称是 AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架
AQS与Synchronized的区别
synchronized | AQS |
---|---|
关键字,c++语言实现 | java 语言实现 |
悲观锁,自动释放锁 | 悲观锁,手动开启和关闭 |
锁竞争激烈都是重量级锁,性能差 | 锁竟争激烈的情况下,提供了多种解决方案 |
AQS常见的实现类 |
- ReentrantLock 阻塞式锁
- Semaphore 信号量
- CountDownLatch 倒计时锁
5.1 基本工作机制
一个锁只能被一个线程持有,竞争失败的进入FIFO队列等待(先进先出的双向队列——内部是双向链表)
5.2 原子性
state状态由cas设置,保证原子性
5.3 公平锁与非公平锁
AQS可以实现公平锁和非公平锁
(插队)新的线程与队列中的线程共同来抢资源,是非公平锁
(排队)新的线程到队列中等待,只让队列中的head线程获取锁,是公平锁
6. ReentrantLock
ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:
- 可中断
- 可以设置超时时间
- 可以设置公平锁
- 支持多个条件变量
- 与synchronized一样,都支持重入
实现原理:
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
7. synchronized和Lock有什么区别
- 语法层面
synchronized 是关键字,源码在jvm 中,用c++语言实现
Lock 是接口,源码由jdk提供,用java 语言实现
使用 synchronized 时,退出同步代码块锁会自动释放,而使用Lock 时,需要手动调用unlock 方法释放锁 - 功能层面
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量
Lock 有适合不同场景的实现,如 ReentrantLock,ReentrantReadWriteLock(读写锁) - 性能层面
在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
在竞争激烈时,Lock 的实现通常会提供更好的性能
8. 死锁
8.1 产生条件
一个线程需要同时获取多把锁,这时就容易发生死锁
8.2 死锁诊断
当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和jstack
-
jps:输出JVM中运行的进程状态信息
-
jstack:查看java进程内线程的堆栈信息
-
可视化工具jconsole、VisualVM也可以检查死锁问题
9. ConcurrentHashMap
9.2 JDK 1.7
- JDK1.7底层采用分段的数组+链表实现
- JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
9.2 JDK 1.8
- JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
- JDK1.8采用CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好
10. Java程序中怎么保证多线程的执行安全
Java并发编程三大特性:原子性、可见性、有序性
10.1 原子性
一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行
10.2 可见性
让一个线程对共享变量的修改对另一个线程可见
10.3 有序性
指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的