常见锁的策略
锁策略具体的情况
一.悲观锁VS乐观锁
判断条件:加锁的时候,预测当前锁冲突的概率是大还是小
预测当前锁冲突概率大,后续要做的工作往往就会更多.加锁开销就更大(时间,系统资源)=>悲观锁
预测当前锁冲突概率不大,后续要做的工作往往就更少.加锁开销就更小(时间,系统资源)=>乐观锁
synchronized是属于既是乐观锁,也是悲观锁,它支持自适应.
synchronized能够自动的统计出当前的锁冲突的次数,进行判定当前是锁冲突概率低,
还是概率高当冲突概率低的时候,就是按照乐观锁的方式来执行的.(速度更快)
当冲突概率高的时候,就会升级成悲观锁的方式来执行.(做的工作更多)
二.重量级锁VS轻量级锁
判断条件:做的事情多少,
做的事情多少叫重量级锁,做的事情少,叫轻量级锁。
一般来说,悲观锁,往往就是重量级锁加锁过程做的事情多,重量
一般来说,乐观锁,往往就是轻量级锁加锁过程做的事情少,轻量
synchronized是属于既是.重量级锁VS轻量级锁,它支持自适应.
三.自旋锁VS挂起等待锁
自旋锁
void lock(){
while (true){
if (){
continue;
}
获取到锁
break;
}
}
消耗了更多的cpu资源但是一旦锁被释放就能第一时间拿到锁拿到锁的速度更快,由于一直在执行这样的操作,所以一直消耗cpu。
挂起等待锁
挂起等待锁,是重量级锁的一种典型实现方式
当尝试加锁,并且锁被占用了,出现锁冲突,就会让当前这个尝试加锁的线程,被挂起.(阻塞状态)此时这个线程就不会参与调度了,直到这个锁被释放,然后系统才能唤醒这个线程,去尝试重新获取锁
(拿锁慢,节省cput的消耗)消耗的时间更长一旦线程被阻塞了,啥时候被唤醒这个过程是不可控的,可能会经历很长很长的时间。
synchronized轻量级锁部分,基于自旋锁实现;重量级锁部分,基于挂起等待锁实现.
四.可重入锁vs不可重入锁
synchronized就是可重入锁,一个线程,针对这把锁,连续加锁两次,不会死锁
不可重入锁,就会造成连续加锁两次之后会死锁。
五.公平锁炎VS非公平锁
公平锁:严格按照先来后到的顺序来获取锁哪个线程等待的时间长,哪个线程就拿到锁
非公平锁:若干个线程,各凭本事,随机的获取到锁和线程等待时间无关了。
公平锁:相当于买饭,先排队的人,先吃饭,后排队的人后买饭
非公平锁,像抢票,所有人在同一时间,一起抢票。
synchronized属于非公平锁多个线程,尝试获取这个锁
六.互斥锁VS读写锁
互斥锁:给读操作加锁:读的时候,不能写给写操作加锁:写的时候,不能读。
java的读写锁是这样设定的:
1)读锁和读锁之间,不会产生互斥
2)写锁和写锁之间,会产生互斥
3)读锁和写锁之间会产生互斥
日常开发中,有很多场景,景,属于“读多,写少”,用读写锁提高效率。
synchronized实现原理
synchronized即使悲观锁,也是乐观锁,既是轻量级锁,也是重量级锁.轻量级锁是自旋锁实现,重量级锁是挂起等待锁实现.是可重入锁,不是读写锁,是非公平锁.
synchronized 的"自适应"锁升级的过程:
1)未加锁的状态 (无锁)--->代码中开始调用执行synchronized->(2)
2)偏向锁--->遇到锁冲突
3)轻量级锁--->冲突进一步提升
4)重量级锁
偏向锁的作用:能不加锁,就不加锁,能晚加锁,就晚加锁。
就像买房子,刚开始没加锁的状态的时候,就是没买,然后跟房地产的人说,再看看,这时候就相当于偏向锁,等到有人想要买房子的时候,就会提升为轻量锁,直接将房子叫了定金,如果这时候还有很多人买,冲突加剧,就会变成重量级锁。
锁消除
最典型的,就只在一个线程里,使用synchronized.
由于编译器优化,需要保证优化后的逻辑和优化前是要等价的这里做的是比较保守的.能够起到的作用有限的。
就是将很多的步骤合并,使其简化。
最终目的:提高效率
锁粗化
锁的粒度
加锁的范围内,,包含多少代码包含的代码越多,就认为锁的粒度就越粗反之,锁的粒度就越细。
synchronized (this){
fun1();
}
synchronized (this){
fun2();
}
synchronized (this){
fun3();
}
合并
synchronized (this){
fun1();
fun2();
fun3();
}
synchronized的优化策略
1)锁升级[重要]=>高频考点.(把偏向锁,理解好,解释清楚)
2)锁消除
3)锁粗化
CAS
这是一条cpu指令,就可以完成比较和交换这样的一套操作下来(原子的)。
boolean cas(内存地址,寄存器1,寄存器2)
{
if (内存地址==寄存器1){
把 内存地址的值和 寄存器2 的值,进行交换
return true;
}
return false;
}
用来赋值
在java 代码中也就可以使用CAS操作了
只要代码有了加锁,此时代码就和“高性能”。
import java.util.concurrent.atomic.AtomicInteger;
public class Test5 {
private static AtomicInteger count=new AtomicInteger(0);
public static void main(String[] args) {
Thread t1=new Thread(()->{
for (int i=0;i<5000;i++){
count.getAndIncrement();//count++;
count.incrementAndGet();//++count;
count.getAndIncrement();//count--;
count.decrementAndGet(); //--count;
count.getAndAdd(10);//count+=10;
}
});
}
}
--
此处我们的代码中,没有用到任何的加锁操作的,
这一套,基于CAS,不加锁来实现线程安全代码的方式,也称为“无锁编程
还可以基于CAS来封装成自旋锁(自旋锁也是基于CAS来实现的),
这样做,其实也就失去了“无锁编程”的意义了.
public class SpinLock{
private Thread owner=null;//持有锁的线程是谁未加锁状态,此时owner就是null.
public void lock(){
while (!CAS(this.owner-->此处的比较和赋值是原子的~~就可以借助这样的逻辑来进行行加锁的实现,null-->如果当前前owner的值为null就把当前调用lock方法的线程引l用赋值到owner里面此时CAS返回true,再来!取反,就是false,循环结束了,Thread.currentThread()))
{
}
}
public void unlock(){
this.owner=null;
}
}
如果当前owner不为null,此时CAS不会进行任何交换/赋值直接返回false,再来!取反,就是true,就要继续循环,再判定一次~~
CAS的ABA问题属于CAS的一个重要注意事项.
CAS 这里核心是"比较-发现相等-交换"=>潜台词发现相等=>数据中间没有发生过任何改变
此处CAS期望的是,中间没有其他线程修改过这个数据实际上,不一定.可能会出现,其他线程,把这个数据,从A->B->A看起来好像没变,好像没人改,其实是已经变过了,但是影响不大。