悲观锁:
大多数时间看,存在线程冲突(悲观地看待问题),每次都先加锁,再释放锁
乐观锁:
大多数时间看,没有线程冲突的(乐观地看待问题),每次都不加锁(Java层面看),每次都直接执行修改数据的操作,返回修改是否成功的结果,程序自行处理逻辑
冲突:同一个时间,多个线程并发并行的执行某个代码
不加锁:从cpu层面看,是加锁的
例子:
一个房间,上课时间比较长,容易出现冲突,使用悲观锁
一个房间,搬东西,速度很快,不容易冲突,使用乐观锁
Synchronized初始使用乐观锁策略,当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略
乐观锁的一个重要功能就是要检测出数据是否发生访问冲突,我们可以引入一个”版本号“来解决
Java层面:无锁,直接修改某个共享变量
boolean result = update(store,100,1)//直接修改,不加锁
返回true:主存store修改成功——线程1
返回false:主存store不做任何操作——线程2
这里名词虽然也包含锁,但实际也是Java层面无锁的操作
while(true){
boolean result = 乐观锁的方式修改数据;
if(result){
break;
}
}
自旋的方式,执行乐观锁修改数据的操作
绝大多数时间,没有线程冲突不代表完全没有冲突
也提供一种解决冲突的方案:反复尝试修改数据(自旋名称的由来)
一般来说,乐观锁都考虑结合自旋锁来操作
如果满足乐观锁的使用条件:冲突概率比较小,且即使发生冲突,也能够很快的得到执行,就可以使用。
悲观锁: 加锁,执行,释放锁
因为代价比较大,所以也叫重量级锁
乐观锁: Java层面无锁,cpu层面加锁
Java语言层面看代价小,也叫轻量级锁
乐观锁属于一种解决冲突的设计思想:尝试修改,直接返回修改结果
实现上:Java中是CAS(修改变量),程序+数据库也可以使用乐观锁来解决冲突
如两个用户同时修改页面上的学生信息这条数据(就是客户端并发冲突):这里数据库类似之前操作主存,先读到页面,再修改,写回数据库
解决方法:数据库表设计一个version字段,修改的时候判断一下
CAS:Java提供的对变量修改操作的乐观锁实现
同一个线程可以重复申请到同一个对象的锁
同一个时间,只有一个线程申请到锁
线程获取到锁不是以申请锁的时间先后顺序来获取到锁
排队买票就是公平锁(按时间顺序买到票)
不排队买票,大家划拳买票,就是非公平锁
非公平锁的缺陷: 可能出现线程饥饿的现象
优点: 效率更高
ReentrantReadWriteLock(适用于读多写少的场景)
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReadLock readLock = readWriteLock.readLock();//读锁
WriteLock writeLock = readWriteLock.writeLock();//写锁
在某些场景下,使用读写锁,两个锁,就比较有用:
比如web项目,多个客户端,对一个服务端文件进行操作,可能有读,写(修改,删除)
如果全部都加一把锁:读读、读写、写写都是互斥的
实际期望提高效率:读读可以是并发,另外两个互斥
我们写的代码就可以是:
readLock.lock()
读文件
readLock.unlock()
writeLock.lock()
写操作
writeLock.unlock()
英文全称为Compare and Swap:比较并交换
是JDK提供的一种乐观锁的实现,能够满足线程安全的条件下,以乐观锁的方式来修改变量
1.JDK提供了一个unsafe的类,来执行CAS的操作
2.JDK中unsafe提供的CAS方法,又是基于操作系统提供的CAS方法(这个又是基于cpu提供的CAS硬件支持,且使用了lock加锁)
简单地说,unsafe是基于操作系统和cpu提供的CAS来实现的
比较常见的:Java的Java.util.concurrent.atomic包下的都是使用了CAS来保证线程安全的++,- -,!flag这种操作
这里使用CAS vs synchronized
CAS效率高,synchronized效率低
原因:++,- -,!flag这种操作执行速度非常快,很快能得到执行
在没有引入版本号的情况下,CAS是基于变量的值,在读和写的时候来比较
读:主存读到工作内存
写:工作内存写回主存
如何解决ABA问题?
引入版本号:每次修改操作,版本号+1,比较的时候,还需要比较读和写的时候,版本号是否一样
JDK中,提供了一个叫AtomicStampedReference的类,里边可以包装其他类(里边用成员变量设置为我们要修改的数据),里边提供了版本号管理
synchronized作用: 基于对象头加锁的方式,实现线程间的同步互斥 (对同一个对象进行加锁的多个线程,同一个时间只有一个线程获取到锁,相当于线程间获取锁,执行同步代码是互斥的)
synchronized加锁操作,可能涉及对象头状态升级的过程(性能/效率从低到高):
无锁: 没有任何线程申请到该对象的锁
偏向锁: 第一次进入的线程,或是这个线程再次申请同一个对象锁
轻量级锁: CAS+自旋:出现线程冲突(竞争),但冲突概率比较小
重量级锁: 真实地进行加锁(使用操作系统mutex进行加锁),冲突比较严重
锁升级过程,但是不能够降级
原理:
1.JVM把synchronized这样的Java代码编译为class字节码之后,实际是一个monitor机制:
monitorenter(相当于对象头的监视器锁:加锁)
…同步代码
monitorexit(退出:释放锁)
…(JVM执行一些指令)
monitorexit(退出:释放锁)
两个exit指令:因为同步代码可能出现异常
monitor中,还保存了加锁的次数
synchronized同一个线程重入申请这个对象锁,次数+1,释放锁其实是次数-1,等次数=0,就真实的释放锁
总结:
synchronized编译为class字节码,是基于monitor机制来实现对对象头加锁:一个monitor enter和两个monitorexit字节码指令保证即使出现异常也能释放锁,使用计数器来设置重入的次数