1.常见的锁策略
1.乐观锁与悲观锁
乐观锁:预测当前锁冲突概率不大,后续要做的工作往往就很少,加锁开销就更小
悲观锁:预测当前锁冲突概率大,后续要做的工作往往就很多,加锁开销就更大
2.重量级锁与轻量级锁
重量级锁:加锁过程中做的事情多
轻量级锁:加锁过程中做的事情少
3.自旋锁与挂起等待锁
自旋锁:轻量级锁的一种典型实现方式,例如使用while循环检测锁是否被占用,没释放就继续执行循环(cpu空转),释放了就获取锁
挂起等待锁:重量级锁的一种典型实现方式,借助系统中的线程调度机制,当尝试加锁,并且锁被占用了,出现锁冲突,就会让当前这个尝试加锁的线程,被挂起(进入阻塞状态),此时该线程就不会参与调度了,直到锁被释放时,系统才能唤醒这个线程并且尝试重新获取锁
自旋锁拿到锁的速度更快(消耗了更多的cpu资源,但是一旦锁被释放,就能第一时间拿到锁),挂起等待锁拿到锁的速度更慢(更节省cpu,但一旦线程被阻塞了,什么时候被唤醒是不可控的,可能消耗很长时间)
4.可重入锁与不可重入锁
可重入锁:一个线程针对一把锁连续加锁两次不会死锁
不可重入锁:一个线程针对一把锁连续加锁两次会死锁
5.公平锁与非公平锁
公平锁:严格按照先来后到的顺序获取锁(哪个线程等待时间长,哪个线程就拿到锁)
非公平锁:随机获取到锁,和线程等待时间无关
6.互斥锁与读写锁
互斥锁:一个线程获取到锁后,另一个线程也尝试加这个锁,就会阻塞等待(发生锁冲突)
读写锁:给读操作和写操作分别加锁 (1)读锁和读锁之间,不会产生互斥 (2)写锁和写锁之间会产生互斥 (3)读锁和写锁之间会产生互斥
2.synchronized优化
2.1 synchronized的性质
基于上述的锁策略,我们来对synchronized锁的性质进行一个总结。
1) synchronized既是乐观锁也是悲观锁,它能够自动统计出当前的锁冲突的次数,判断当前锁冲突概率的高低。当冲突概率低的时候,就按乐观锁的方式来执行,当冲突概率高的时候,就会升级成悲观锁的方式来执行
2) synchronized即是轻量级锁也是重量级锁,随着锁冲突的进一步提升,可以由轻量级锁升级为重量级锁;synchronized轻量级锁部分,基于自旋锁实现;重量级锁部分,基于挂起等待锁实现
3) synchronized是不可重入锁
4) synchronized是非公平锁,当多个线程尝试获取这个锁时,按照概率均等的方式来获取
5) synchronized是互斥锁
2.2 锁升级
锁升级的过程: | 偏向锁:首次对对象进行加锁时,不进行真正的加锁,而是做一个“标记”,如果没有别的线程尝试对这个对象加锁,就保持此状态直到解锁,从而降低程序开销,一旦真的涉及到其他的线程竞争,再取消偏向锁状态,进入轻量级锁状态 |
2.3 锁消除
锁消除是一种优化策略。当代码中写了加锁操作时,编译器与JVM会当前的代码是否真的需要进行加锁做出判定,如果不需要加锁,就会自动把加锁操作优化掉 (编译器优化需要保证优化后的逻辑和优化前等价)
2.4 锁粗化
锁粗化也是一种优化策略。当一段逻辑需要进行频繁加锁解锁时,编译器就会自动把多次细粒度的锁合并成一次粗粒度的锁。
锁的粒度:加锁的代码块内代码越多,锁的粒度越粗,反之,锁的粒度越细
3.CAS
3.1 CAS的简要介绍
CAS,全称为compare and swap,意为比较和交换,在进行比较后将内存地址和寄存器中的值进行交换,这个操作过程在整体上是一条cpu指令,CAS的流程可以想象成一个方法
boolean cas(address,reg1,reg2){//address内存地址,reg1,reg2寄存器
if(address == reg1){
//交换address内存地址中的值和reg2寄存器中的值
//一般更关心交换交换后内存地址中的值,所以可以大致认为是把reg2寄存器中的值赋给内存地址
return true;
}
return false;
}
3.2 CAS的应用
1) 原子类
Java中有一些类对CAS进行了进一步封装。典型的就是"原子类"。针对原子类,我们来介绍其中的一种,AtomicInteger类,对int进行封装,保证++,--,+=等操作是原子的。
import java.util.concurrent.atomic.AtomicInteger;
public class atomic {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count.getAndIncrement();//等价于count++
//count.incrementAndGet();等价于++count
//count.getAndDecrement();等价于count--
//count.decrementAndGet();等价于--count
//count.getAndAdd(10);等价于count+=10
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count=" + count);
}
}
运行结果如下:
可以看到在没有加锁的情况下,结果仍然是正确的。基于CAS,原子类没有通过加锁的方式来实现线程安全代码,这种方式称为“无锁编程”,但只适用于一些特殊场景。
2) 自旋锁
public class SpinLock {
private Thread owner=null;//持有锁的线程,未加锁状态时,owner为null
public void lock() {
//通过 CAS 看当前锁是否被某个线程持有.
//如果owner的值为空,就把调用lock方法的线程赋值给owner,CAS返回true,取反为false,循环结束
//如果owner的值不为空,CAS不会进行交换,返回false,取反为true,继续循环进行判定
while (!CAS(this.owner, null, Thread.currentThread())) {
}
}
public void unlock(){
this.owner=null;
}
}
3.3 ABA问题
使用CAS编写代码时,其比较操作需要检查当前内存的值是否被其他线程修改,如果被修改就要稍后重新尝试,如果没被修改,接下来就可以直接修改,但当前内存的值不变不代表当前内存的值没有改变过,另一个线程可能把这个值从A修改为B再重新修改为A,在这种极端情况下,可能会产生bug,我们通过一个存款取款的例子来理解一下
当只有t1和t2线程进行操作时,不会产生问题,但假设有一个线程t3在t2线程执行完之后存款500,使banlance变为1000,导致t1线程执行if语句时balance与oldbanlance的值相等,使CAS比较成立并且又扣款500,最后导致取了500,余额却扣款了1000。这个过程就属于ABA问题产生的bug。
那么如何上述ABA问题呢?可以通过引入版本号,在每次操作余额时,版本号++,同时通过CAS判断版本号,这样就能通过版本号是否改变来判断数据是否改变,从而有效避免ABA问题。