- 乐观锁 or 悲观锁
- 乐观锁即蹲坑不锁门,只会在更新的时候判断有没有其他线程去更改数据,有的话就回滚
典型: - 悲观锁即进厕所立马锁门,其他线程来了即阻塞,进到阻塞队列中,等待主线程蹲坑完毕后,按顺序获取
典型:synchronized 和 ReentrantLock
- 独占锁 or 共享锁
- 顾名思义,独占锁即获取之后,既能读数据也能写数据,其他线程无法获取锁且无法加任意类型的锁,即阻塞住了
- 共享锁可同时被多个线程获取,但只能读数据,不能写数据
- 互斥锁 or 读写锁
- 互斥锁保证了某一数据只能同时被一个线程操作,有唯一性 and 排他性
- 读写锁:实际上为两个锁(读锁和写锁)读锁为共享锁,写锁为独占锁
- 公平锁 or 非公平锁
- 公平锁指的是线程之间按照到来的先后顺序依次获取锁
- 而非公平锁为,线程想获取锁了,无视队列,直接试图获取锁,强硬霸道,弱肉强食,弊端为有可能出现饥饿状态(即无线程成功获取到锁)
Java中synchronized为非公平锁,ReentrantLock可设置,但默认为非公平锁
/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(false);
- 可重入锁
我之前一直以为只有Redis可以实现,现在知道了ReentrantLock(可重入锁)顾名思义,synchronized也可以实现
好处:实现递归取锁,避免死锁,体现灵活性与可控性 - 自旋锁
个人理解:尝试获取锁,获取不到的话不会直接将线程挂起,而是采取自旋操作,进入一个忙循环,循环尝试获取锁
好处:减少线程被挂起的几率,因为线程被挂起需要耗费大量的资源,但是长时间进入忙循环会耗费比挂起更高的性能,所以不适合长时间自旋的场景 - 分段锁 Segment
即更细粒度的锁,当需要进行数据操作的时候,只要对需要操作的模块加锁即可,不用对整体加锁
CurrentHashMap底层就使用的Segment - 锁升级
即无锁 --》 偏向锁 --》轻量级锁 --》 重量级锁
- 偏向锁:会偏向第一个访问的线程,通常在不存在多线程竞争的时候默认上这把锁,后续若访问该资源的线程过多,通过控制对象Mark Word的标志位来判断,对象头存储的线程id是否与当前线程id一致
- 轻量级锁:竞争变得较为激烈,即升级为轻量级锁,承认线程竞争的存在,但一般压力较小,获取不到会采用自旋锁那一套
- 重量级锁:自旋超过一定次数,即升级为重量级锁,没获取到的阻塞,去阻塞队列排队
- 锁优化
- 锁粗化
顾名思义,将多个小锁合并为一个大锁,锁范围扩大
粗化前
private static final Object LOCK = new Object();
for(int i = 0;i < 100; i++) {
synchronized(LOCK){
// do some magic things
}
}
粗化后
synchronized(LOCK){
for(int i = 0;i < 100; i++) {
// do some magic things
}
}
应该很好理解吧。。。。
- 锁消除
即已上锁的线程,虚拟机编译器在运行时检测到几乎没啥线程竞争了,从而将锁消除
例子:方法test本身就为线程安全的
public String test(String s1, String s2){
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(s1);
stringBuffer.append(s2);
return stringBuffer.toString();
}
StringBuffer的线程安全在此场景显得有点点多余,故虚拟机自动进行了锁消除
StringBuffer.class
// append 是同步方法
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
最后,即大佬总结的神图