目录
一、锁策略
1、悲观锁和乐观锁
2、轻量级锁和重量级锁
3、自旋锁和挂起等待锁
4、互斥锁和读写锁
5、可重入锁和不可重入锁
6、公平锁和非公平锁
二、cas和synchronized 优化过程
1、CAS(compare and swap)
(1)、原子类
(2)、自旋锁
2、synchronized实现策略:
(1)、锁升级
一、锁策略
1、悲观锁和乐观锁
乐观锁和悲观锁不是具体类型的锁而是指两种不同的对待加锁的态度,这两个锁面对锁冲突的态度是相反的。
- 乐观锁:认为不存在很多的并发操作,因此不需要加锁。
- 悲观锁:认为存在很多并发操作,因此需要加锁。
2、轻量级锁和重量级锁
重量级锁和轻量级锁是通过结果去看待加锁解锁的过程开销。做的工作多消耗资源多,做的工作少消耗资源少。因此我们会认为乐观锁是轻量级锁,悲观锁是重量级锁。
- 重量级锁:是进入内核态的加锁逻辑,资源开销较大。
- 轻量级锁:是纯用户态的加锁逻辑,资源开销较小。
3、自旋锁和挂起等待锁
- 自旋锁:是一种轻量级锁,自旋锁类是一直反复查看当前锁是否就绪,CPU不停空转会消耗大量地CPU。
- 挂起等待锁:是一种重量级锁,该锁不会像自旋锁一样一直查看锁状态而是先去做别的事情,过一会再来看当前锁是否就绪。
4、互斥锁和读写锁
- 互斥锁:当两个线程竞争同一把锁时会产生锁冲突进而阻塞等待。
- 读写锁:根据代码实际的逻辑进行加锁,有读锁和写锁两种锁。读锁和读锁之间不会产生锁竞争;读锁和写锁之间会产生锁竞争;写锁和写锁之间,会产生锁竞争。如果代码中读的场景多写的场景少时,读写锁相比于互斥锁优化了效率、减少了不必要的锁竞争。
5、可重入锁和不可重入锁
- 不可重入锁:是指同一个线程对同一把锁连续加锁两次,产生了死锁。
- 可重入锁:是指同一个线程在外层方法获取锁,进入内层方法会自动获取锁。
注ReentrantLock和Synchronized都是可重入锁。
6、公平锁和非公平锁
公平是指遵循先来后到的原则的,因此遵守先来后到就是公平锁,不遵守先来后到的就是非公平锁。
例:t1,t2,t3三个线程竞争同一把锁,谁先来的谁就拿到锁的情况叫公平锁。如果三个线程随机一个拿到锁,后来的线程可能会先拿到锁的情况叫非公平锁。
- 公平锁:指多个线程按照申请锁的顺序来获取锁。
- 非公平锁:指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
注:操作系统默认的锁的调度是非公平的。系统对于线程的调度是随机的,自带的synchronized这个锁是非公平的。想要实现公平锁需要在synchronized的基础上,加个队列来记录这些加锁线程的顺序。
二、cas和synchronized 优化过程
1、CAS(compare and swap)
寄存器A的值和内存K的值进行对比,如果值相同就把寄存器B的值和内存K的值进行交换
//伪代码
boolean CAS(address,exceptValue,swapValue){
if(&address==exceptedValue{
&address=swapValue;
return true;
}
return false;
}
注:CAS操作是一条CPU指令(不必加锁就能够保证线程安全)并非是上述这段代码,这一条指令就能完成上述这段代码的功能。
(1)、原子类
实现原子类,标准库里提供了AtomInteger类,该类可以保证++--时的线程安全
class AtomicInteger{
private int value;
public int getAndIncrement(){
int oldValue=value;
while(CAS(value,oldValue,oldValue+1)!=true){
oldValue=vlaue;
}
return oldValue;
}
}
oldValue可以被视为寄存器,如果发现value和oldValue的值相同,此时就把oldValue+1设置到value中,相当于++,然后CAS返回true,循环结束。反之如果value和oldValue不相同,CAS什么都不做并返回false,进入循环,重新设置oldValue的值。
即:此处的CAS就是在确认当前的value是否发生过改变,如果没变过才能自增,如果变过了就先更新再自增。
例:
之前的线程不安全是因为一个线程不能及时的感知另一个线程对内存的修改。
如:t2在自增时,先读后自增。此时在自增之前t1已经自增过了,t2是在1的基础上自增的,就出现了问题。
使用CAS之后,t2在自增前会先检查寄存器的值和内存的值是否一致
(2)、自旋锁
反复检查当前的锁状态,看锁是否解开。通过CAS看当前锁是否被某个线程持有,如果这个锁已经被别的线程持有,那么就自选等待。如果这个锁没有被别的线程持有,那么就把owner设为当前尝试加锁的线程
public class SpinLock{
private Thread owner=null;
public void lock(){
while(!CAS(this.owner,null,Thread.currentThread()){
}
}
public void unlock(){
this.owner=null;
}
}
如果当前owner是null,比较就成功,就把当前线程的引用设置到owner中,加锁完成循环结束。比较不成功意味着owner非空,锁已经有线程持有了。此时CAS就什么也不做直接返回false,循环继续进行,此时这个循环不停尝试询问锁是否释放。
好处:一旦锁释放就能立即获取锁。
坏处:消耗大量CPU资源。
乐观锁一般情况下锁冲突概率低,实现成自旋锁比较合适。
注:CAS的关键是对比内存和寄存器的值是否相同,但这对于aba问题会有一定概率出问题。
如何解决aba问题呢?aba问题的关键是值会反复横跳
- 可以通过约定数据只能单方向变化(只能增加或减小)
- 也可以通过引入另一个版本号变量,约定版本号只能增加,每次修改都会增加一个版本号。而每次CAS对比时,就不是对比数值本身,而是对比版本号。
2、synchronized实现策略:
(1)、锁升级
偏向锁:
只是先让线程针对锁有一个标记。如果整个代码执行过程中都没遇到别的线程竞争这个锁,此时就不用真的加锁了。但要是有别的线程尝试来竞争这个锁,此时偏向锁就立即升级成真的锁(轻量级锁),此时别的锁只能等待。既保证了效率,又保证了线程安全。
自旋锁:
速度快但是消耗大量cpu。自旋时cpu快速空转,如果当前锁竞争激烈,多个线程都在自旋,cpu的消耗就非常大。既然如此,就升级成重量级锁,在内核中进行阻塞等待(意味着线程要暂时放弃cpu,由内核进行后续调度)
注:运行时jvm做出的优化手段,主流的jvm实现只能升级不能降级。