引言:
噫嘘唏,难乎其哉,锁道之烦,烦比需求翻。
前提了解:
-
CAS算法:
Compare And Swap(比较与交换),是一种无锁算法,设置到3个值:V(需要操作的内存值)、A(需要与操作的内存
值进行比较的值)、B(写入的新值),只有当V=A的时候,才会将B更新到V;
举个例子理解,当我们要重新设置某某账户的密码时,会让你输入原始密码验证是否正确,只有正确的时候,新密码才会设置成功;此处就是将手动输入的原始密码与数据库保存的原始密码匹配是否一致,如果一致,则更新新密码;如果更新失败,会循环读取比较
CAS存在的问题:
- ABA问题。拿上述例子举例,如果原始密码是123456期间有人将密码改成654321后又改回到了123456;这样我们拿123456去比较,依旧是能匹配成功的,但是已经不是我们最初需要比较的数据;
- CPU开销问题。如果CAS一直更新失败,会自选保持一直尝试,导致CPU的开销;
1. 锁对比
1.1 乐观锁与悲观锁
1.1.1 乐观锁
乐观锁其实本质是无锁操作,可以理解为非常乐观,大大咧咧,认为肯定不会有其他线程修改自己要使用的数据,所以不在资源 上加锁,只是在更新资源时,通过CAS算法来比较替换更新数据;
1.1.2 悲观锁
悲观锁,与乐观锁相反,认为自己操作的资源肯定有其他线程来操作修改,所以在获取到数据时会加锁,防止其他线程操作;
1.1.3 总结
查询多时,适合用乐观锁进行无锁操作,最大化性能;
修改多时,适合用悲观锁,保证数据安全;
Synchronized和Lock都是悲观锁。
1.2 公平锁与非公平锁
1.2.1 公平锁
公平锁,指的是线程会按照申请锁的先后顺序进入队列等待,整体的性能较低,只有队首的线程才能分配到时间片运行,后续的 其他线程进入阻塞状态;
1.2.2 非公平锁
非公平锁,指的是多个线程操作同步方法时,会直接尝试获取锁,获取不到才会进入队列排到队尾,可以理解初始化时有获得插队的机会。
注意点:
1. 此处,如果尝试获取锁失败,会进入AQS阻塞队列,上图非公平锁情况下,D如果首次尝试获取锁失败,则会进入到队列的尾部,此时,如果没有其他新建的线程出现,那么下一次抢占到时间片执行的必然是A线程;
2. 在AQS阻塞队列中,如果某个线程获取到了锁,但是在方法中调用了await()方法释放锁之后,会进入AQS条件队列,此队列的线程,只有当阻塞队列中某个线程拿到时间片运行,并且调用了signal()或者signalAll()方法,才会重新唤醒进入到阻塞队列等待分配时间片
1.3 独享锁与共享锁
1.3.1 独享锁
独享锁也叫排他锁、互斥锁,即该锁一次只能被一个线程所持有,如果线程对某个同步资源加上排它锁后,则其他线程不能再 对同步资源加任何类型的锁。获得排它锁的线程对同步资源有读写操作权限。
1.3.2 共享锁
共享锁,指该锁可被多个线程所持有。如果线程对同步资源加上共享锁后,其他线程只能对同步资源再加共享锁,不能加独享 锁。获得共享锁的线程对同步资源只能做读取,不能修改。
1.4 自旋锁与适应性自旋锁
1.4.1 自旋锁
线程执行是靠等待CPU分配时间片运行的,如果线程执行的业务逻辑执行的时间很短,甚至小于CPU调度切换线程状态的时间, 我们就不必让线程进入阻塞状态,而是在锁被某个线程占用时,设置其他线程重试获取锁;自旋锁就是设置当锁资源被占用时, 其他线程自旋重试,自旋锁默认重试次数是10次,可以使用-XX:PreBlockSpin来修改。
- 自定义自旋锁
//自定义自旋锁
public class TestReentrantLock {
//原子类
AtomicReference<Thread> atomicReference = new AtomicReference<Thread>();
//加锁方法
public void czLock(){
Thread thread = Thread.currentThread();
while (!atomicReference.compareAndSet(null,thread)){
System.out.println("一直尝试获取锁,自旋");
}
}
/*
*此处cas方法中的null,就是期待Thread的值是null,是的话则更新成功为当前线程。当
*第一个线程进入czLock方法时,当前Thread初始化值为null,能更新成功,即获取到
*锁。此时如果有其他线程想获取锁,Thread已经被赋值,不是null,则会获取锁失败。直
*到第一个线程调用czunLock方法将Thread赋值为null释放锁。
*
*/
public void czUnLock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
}
}
1.4.2 适应性自旋锁
线程设置拿不到锁自旋后,如果锁被占用的时间过长,那么自选就会浪费CPU的资源,因此引入了自适应自旋锁。自适应指的是 自旋的时间不是固定的,而是根据“可靠性”来允许自旋的时间。如果对于某个自旋的锁,经常被成功获取到,并且持有锁的线程 正在执行,则默认该锁被自旋获取的可靠性很高,则允许的自旋持续更长时间;相反,如果自旋获取锁的成功率很低,下次尝试 自旋获取锁时,直接进入则色。
1.5 可重入锁与不可重入锁
//锁住的是同一个对象,执行完first方法后无需释放锁,可以直接拿到second的锁。
public class TestReentrantLock {
public synchronized void first(){
System.out.println("外层方法");
second();
}
public synchronized void second(){
System.out.println("内层方法");
}
}
1.5.1 可重入锁
可重入锁又叫做递归锁,如果锁住的是同一个对象或者class,同一个线程在外层方法获取锁的时候,再进入内层同步方法会自 动获取锁,不会因为之前已经获取过还没释放而阻塞,可以有效避免死锁。
1.5.2 不可重入锁
即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。
2.Synchronized详解
2.1 重要概念
Monitor:
- Monitor是一种同步工具,每一个java对象都会有一把内置的锁,称为内置锁或者Monitor锁;
- Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步;
- 对于一个monitor对象,只能够被一个线程持有,在一个线程退出同步块时,线程释放monitor对象,它的作用是把CPU缓存数据(本地缓存数据)刷新到主内存中,从而实现该线程的行为可以被其它线程看到。在其它线程进入到该代码块时,需要获得monitor对象,它在作用是使CPU缓存失效,从而使变量从主内存中重新加载,然后就可以看到之前线程对该变量的修改。
- Sync是个悲观锁,在操作同步资源时,会给该同步资源加锁;
- Sync是个重量级锁,在某个线程获取锁后,其余访问同步资源的线程会阻塞,直到该线程释放锁;
2.2 Sync对象锁与类锁
-
对象锁
对象锁无论是哪种写法都是针对同一对象,访问其他对象实例不受影响。
-
类锁
2.3 Sync锁状态
锁一共有4种状态,级别由低到高依次是无锁—偏向锁—轻量级锁—重量级锁;
锁状态只能升级不能降级;
-
无锁:
无锁,即不对共享资源进行锁定,多个线程都可以操作同一个资源,但同时只有唯一线程可以修改成功;
-
无锁内部是通过CAS算法实现,类似乐观锁中的版本比较替换;
-
JUC包(java.util.concurrent)下的原子类,就是通过CAS操作实现无锁同步安全;
-
-
偏向锁:
偏向锁,指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。持有偏向锁的线程不会主动释放锁,只有当遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,并且偏向锁升级为轻量级锁。
JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。 -
轻量级锁:
轻量级锁,指的是,当锁被某个线程所持有时,其他尝试获取锁的线程不会进入阻塞状态,而是自旋等待。只有当自旋超过10次(默认次数)失败后,才会进入阻塞状态,并且锁升级为重量级锁。
-
重量级锁:
重量级锁,即当某个线程持有同步资源的锁时,其他访问同步资源尝试获取锁的线程,直接进入阻塞状态。
2.2 Sync与Lock对比
- sync本质是java关键字,lock是juc包下的一个接口;
- sync修饰的代码块,线程执行结束后自动释放锁,lock需要手动释放锁;
- sync修饰的代码块,全程不可干预锁操作,无法获取锁状态;lock可以获取到锁状态;
- sync修饰的代码块,某个线程获取到锁阻塞后,另外一个线程会持续等待;lock修饰的代码块,遇到其他线程阻塞,不会持续等待;
- sync适用于少量代码同步;lock使用多量同步;
- sync,可重入锁,互斥锁,非公平锁,不可中断;lock,可重入锁,互斥锁,可中断,可设置是否公平锁。