乐观锁和悲观锁
设计上解决线程安全的一种思想
乐观锁
设计上总是乐观的认为数据修改大部分场景都是没有线程并发修改,少量情况下才存在,线程安全上采取版本号控制(用户自己判断版本号,并处理)
悲观锁
悲观的认为总是有其他线程并发修改,每次都是加锁操作。
CAS(Compare and Swap)
实现:自旋尝试设置值的操作
无锁操作,乐观锁
技术背景:当线程执行的任务量较小时,使用synchronized(多个线程同时竞争对象锁)保证多线程安全时,效率较低,竞争失败的线程很快的在阻塞态和被唤醒态之间转化,影响性能。
使用CAS的前提:代码块执行速度非常快
目的:在安全的前提下提高效率。(例如:保证线程安全的修改变量)
原理:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。
public final class Unsafe{
public final int getAndSetInt(Object var1,long var2,int var4){
int var5;
do{
var5 = this.getIntVolatile(var1,var2);
}while(!this.compareAndSwapInt(var1,var2,var5,var4));
return var5;
}
}
真实值:主内存中变量的实际值
旧值:拷贝到工作内存中的值
修改值:在旧值基础上的修改值
给定参数:内存中的实际值,之前拷贝到线程内的值,修改值,版本号。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS的缺陷
ABA问题
因为CAS会先检查旧值有没有变化。若在其检查前,其他线程将值从A变回B,又从B变回A,当其区检查时,会发现旧值并没有变化依然为A,但是实际上已经发生了变化。
解决方法
采取乐观锁的设计,引入版本号做控制
jdk中,采取CAS实现的API:
- java.util.concurrent.auomic,原子性的并发包下的api
- synchronized中,多个线程不同时间点执行同步代码块时,jdk优化会采取CAS
- 1.8中ConncurrentHashMap实现,put操作时,若结点是null,采取CAS
自旋会浪费大量的处理器资源
自旋的实现:
- 循环死等
- 可中断的方式-interrupt
- 判断循环次数,达到阈值退出
- 判断循环的总耗时,达到阈值退出
缺点:
- 如果代码块(修改值(设置值))不能很快的执行,线程就一直处于运行态循环执行CAS,性能消耗较大
- 线程数量较多时,导致不能很快的执行完,或者cpu在很多线程间切换,对性能消耗大。
创建线程的第三种方式:
Callable 方式结合Future,可以获取线程的执行结果
//代码
synchronized锁实现原理
**实现原理:**锁定对象的对象头,jdk内部使用monitor机制,编译为字节码时,会生成monitorenter、monitorexit指令。(字节码中会包含一个monitor指令以及多个monitor指令。这是因为java虚拟机要确保所获得的锁在正常执行路径和异常执行路径上都能被解锁)
对象头锁状态:
- 无锁
- 偏向锁
- 轻量级锁
- 重量级锁
Synchronized锁不能降级只能升级:提高获得锁和释放锁的效率
原因:级别低的锁的功能都能被级别高的锁保证,这时如果降级就没有必要,得不偿失。
JVM对synchronized的优化方案:根据不同场景,使用不同的锁机制
- 偏向锁:针对同一个线程,再次申请已持有的对象锁(最乐观的一个锁,从始至终只有一个线程竞争锁)(当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程
在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS
竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程)
- 等到等待竞争后才释放的机制 - 轻量级锁:大概率在同一个时间点,只有一个线程申请对象锁
- 实现原理:CAS - 重量级锁 :JVM统计在同一个时间点上,如果多个线程竞争同一个对象锁的概率很大。
- 实现原理:使用操作系统的mutex锁
- 缺点:会涉及到操作系统调度从用户态到内核态的转化,开销非常大,线程会被阻塞、唤醒
synchronized锁优化
锁粗化
此处的StringBuffer是类变量,可以被多个线程操作
当JVM检测到有一一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁个解锁操作,即在最后一次append方法调用完成后进行解锁。
public class Test{
//StringBuffer是线程安全的
private static StringBuffer sb = new StringBuffer();
public static void main(String[] args) {
sb.append("a");
sb.append("b");
sb.append("c");
//到最后一次才释放锁
}
}
锁消除
此处的StringBuffer是方法内的局部变量
是线程私有的,不需要加锁。
public class Test{
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
sb.append("a").append("b").append("c");
}
}
代码逃逸:即代码可能被其他线程执行或数据被其他线程操作。
死锁
产生原因:(同步本质)一个线程等待另外一个线程执行完毕后才能继续执行。但是如果现在相关的几个线程彼此之间都在等待对方,就会造成死锁
至少有两个线程互相申请的对象锁,造成互相等待的局面,彼此都无法继续
执行。
后果:线程阻塞等待,无法向下执行
解决方法:
- 资源一次性分配(破坏请求与保持条件)
- 可剥夺资源:在线程满足条件时,释放已占有的资源
- 资源有序分配:系统为每类资源赋予一个编号,每个线程按照编号递增的次序请求资源
实际中检测死锁的手段:
使用jdk的监控工具,比如jconsole、jstack查看线程状态
避免死锁的方法:银行家算法
- 当一个顾客对资金的最大需求量不超过银行家现有的资金时就可接纳该顾客;
- 顾客可以分期贷款, 但贷款的总数不能超过最大需求量;
- 当银行家现有的资金不能满足顾客尚需的贷款数额时,对顾客的贷款可推迟支付,但总能使顾客在有限的时间里得到贷款;
- 当顾客得到所需的全部资金后,一定能在有限的时间里归还所有的资金
Lock体系
jdk提供的一种除过Synchronized之外的加锁方式,定义了锁对象来进行锁操作。
Lock锁的特点:虽然它失去了像synchronized关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及锁等多种synchronized关键字所不具备的同步特性。
Lock的使用:
Lock lock = new ReentrantLock();
lock.lock();//设置当前线程的同步状态,并在队列中保存线程及线程的同步状态
//设置成功,往下执行
try{
.......
}finally{
lock.unlock();//线程出队列
}
注意:Lock必须抵用unlock()方法释放锁,因此再finally块中释放锁,而synchronized同步块执行完成或者遇到异常锁会自动释放。
Lock锁实现的原理:AQS
AQS(AbstractQueuedSynchronizer):队列式的同步器
实现原理:双端队列保存线程及线程同步状态。并通过CAS提供设置同步状态的方法:(如ReentrantLock实现时,调用ReentranLock实现时,调用lock.lock( )操作,不不停的设置线程同步状态)
关于队列:(1)双端队列(2)AQS中保存了队列的头尾结点
Lock锁---->AQS------->CAS(设置AQS中的线程同步状态)
Lock锁的特点
1.提供公平锁和非公平锁(是否按照入队的顺序设置线程同步状态):多个线程申请加锁操作时,是否按照时间顺序来加锁
2.AQS提供的独占式和共享式设置同步状态(独占锁、共享锁)
独占式:只允许一个线程获取到锁
共享式:一定数量的线程共享式获取锁
本质:设置线程的同步状态(CAS)
3.待带Reentrant关键字的lock包下的APL:可重入锁
允许多次获取同一个Lock对象的锁
提供的读写API:ReentrantReadWriteLock
支持公平性、重入、锁降级
使用场景:多线程执行某个操作时:允许读读并发/并行执行,不允许读写,写写并发/并行执行。如多线程读写文件读读并发、读写、写写互斥
读锁和写锁之间,只能降级不能升级
优势:针对读读并发、提高运行效率
Condition:线程间通信
(1)通过lock.new Condition()获取Condition对象
(2)调用condition.wait()阻塞当前线程,并释放锁(=synchronized锁对象.wait( ))
(3)调用Condition对象.signal()/signalAll()通知之前阻塞的线程(=synchronized锁对象.notify()/notifyAll())
AQS的实现/应用
ThreadLocal
使用场景:隔离线程间的变量,保证每个线程是使用自己线程内的变量副本
代码推荐写法:
- 定义类变量:static ThreadLocal<保存的数据类型>threadLocal = new ThreadLocal<>();//只是一个,但操作时绑定线程的
- 当有线程设置值时,在线程结束前,remove
new Thread(()->{
try{
threadLocal.set(值);
}finally{
threadLocal.remove();
}
}
原理:Thread对象中都有自己的ThreaLocalMap,调用ThreadLocal对象设置值set(value)、获取值get()、删除值remove()、都是对当前线程中的ThreadLocalMap对象的操作,所以每个变量是线程隔离的。
为什么Entry要继承弱引用?
Entry[] table中存放的K是属于弱引用
弱引用:被弱引用关联的对象的生存期是在下一次垃圾回收之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
若果Entry没有继承弱引用类型(K),导致线程没有使用值时,也一直有引用指向V,产生内存泄漏。
假设线程长时间没有执行完,K是强引用:ThreadLocal对象一直不能被回收,V也没办法使用,导致内存泄漏
设置K为弱引用的好处:降低内存泄漏的风险
每次垃圾回收,只要没有其他强引用指向ThreadLocal对象,就回收。ThreadLocalMap实现时,检查键为null时,就会把V变量设置为null,v指向的对象就没有引用了,就可以回收。