1. 说说线程安全问题
多线程并发下,产生安全问题要满足以下三个条件:
- 多线程并发
- 共享数据
- 共享数据修改
为什么会有多线程下,线程安全的问题?
这里就涉及到Java虚拟机(简称JVM)的相关知识了。JVM启动后,本地内存分配给JVM一定的内存空间,称为“主内存”;而线程间又有各自独立的“工作内存”,工作内存中的数据来源于主内存数据的拷贝。正因为这个拷贝,工作内存的数据它不是原版数据,工作内存可能被其他线程所修改,这时候就有数据不一致的问题。
开发中,如何解决线程安全问题?
核心思想:
- 加锁
- 将共享数据调整为:局部变量、不可变对象、ThreadLocal封装共享数据、CAS进行共享数据修改、volatile修饰(只保证可见性、禁止指令重排,不保证原子性)
2. volatile实现原理
volatile修饰的变量具有如下特性:
- 可见性
- 有序性(禁止指令重排)
什么是可见性?
可见性:多个线程访问同一个共享变量时,其中一个线程对这个共享变量值的修改,其他线程能够立刻获得修改以后的值
什么是有序性?
有序性:编译器和处理器为了优化程序性能而对指令序列进行重排序,也就是你编写的代码顺序和最终执行的指令顺序是不一致的。
但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile如何保证可见性?
总结为一句话:通过内存屏障和禁止指令重排保证可见性。
具体分析如下:
JMM内存模型下,数据操作通过如下8种方式:
- 1、
read
(读取):从主内存读取数据 - 2、
load
(载入):将主内存的数据写入工作内存 - 3、
use
(使用):从工作内存读取数据计算 - 4、
assign
(赋值):将计算好的值重新赋值到工作内存中 - 5、
store
(存储):将工作内存的值写入主内存 - 6、
write
(写入):将store过去的变量值赋值给主内存中的变量 - 7、
lock
(锁定):将主内存变量枷锁,标识未线程独占状态 - 8、
unlock
(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
volatile修饰的变量在写入操作时,在写入后,加入5、store(存储)
内存屏障。(写入后,将工作内存
中的数据写入主内存
)
volatile修饰的变量在读取操作时,在读取前,加入2、load(载入)
内存屏障。(读取前,从主内存
中读取数据刷到工作内存
)
如上一来,volatile读取的数据是主内存
中数据,一定是最新的,因此能够保证可见性。
3. synchronized实现原理
synchronized
可以用来修饰 代码块 或 方法。
-
修饰方法
同步方法的常量池有
ACC_SYNCHRONIZED
标识,线程访问方法时会判断是否有ACC_SYNCHRONIZED
标识。如果有,则先获取监视器锁,然后执行方法,最后释放监视器锁。
如果有,且获取监视器锁失败,则会阻塞,直到监视器锁被其他线程释放后,获取到监视器锁。
在发生异常的情况且没有处理异常,则在异常抛出前会释放监视器锁。
-
修饰代码块
修饰代码块时,底层会加入monitorenter
和monitorexit
指令。
每个对象都有一个锁次数的计数器,未被锁的计数器为0.
当线程进入代码块时,执行monitorenter
指令,锁次数计数器 +1,当该线程再次获取锁时,则再次+1。
当执行monitorexit
释放锁时,锁次数计数器-1。
直到锁次数计数器为0时,锁被释放,此时其他线程能够得到该代码块的锁。
4. synchronized和lock的区别
1、synchronized
是关键字,lock
是接口。
2、synchronized
是隐式加锁,lock
是API层面的加锁。
3、synchronized
可以作用在方法和代码块,lock
只能作用在代码块。
4、synchronized
是阻塞式加锁,lock
支持非阻塞式加锁。
5、synchronized
没有超时机制,lock
中的tryLock
支持超时机制。
6、synchronized
不可中断,lock
中的lockInterruptibly()
中断获取的锁。
7、synchronized
底层采用monitor
;lock
底层采用AQS。
8、synchronized
是非公平锁;lock
支持非公平锁与公平锁。
9、synchronized
唤醒方式是notify/notifyAll
;lock
唤醒方式是condition
。
5. CAS乐观锁
CAS:Compare AND Swap,比较并交换。是一种通过乐观锁保证变量操作原子性的机制。
该方法包含3个操作数:内存位置、预期原值、新值,先通过 内存位置定位变量,比较 预期原值与当前 内存位置中的值是否一致;一致则会进行修改;不一致则不做如何操作。
优点:
资源竞争不大的情况下,系统开销小。
缺点:
1、ABA问题
2、竞争大的情况下,CPU开销大
6. ABA问题
主线程通过CAS在取出指定 内存位置的值后,比较 预期原值并修改操作之前,有一定的时间差;在这个时间差内,其他线程将A修改为B,又将B修改为A,此时主线程并不会发觉内存位置的值已被修改过,会继续进行 比较预期原值并修改操作。
尽管主线程的CAS操作完成,但这个过程是有风险的。
解决方案:
加上版本号解决ABA问题:只有版本号一致的情况下才能进行修改操作,每次修改操作都对 版本号执行+1操作。
7. 乐观锁的业务场景及实现方式
适用场景为:冲突不频繁的业务下。
现有的数据库中,对于多数表会增加一个version
字段,该字段能够有效避免数据修改的ABA问题。