目录
1.常见的锁策略
2:Synchronized原理
2.1:加锁工作工程
2.2:其他的优化操作
2.2.1:锁消除
2.2.2:锁粗化
3.CAS
3.1:实现原子类
3.2:CAS中的ABA问题
3.2.1:什么是ABA问题
3.2.2:存在的BUG问题。
3.2.3:解决办法
1.常见的锁策略
1.乐观锁vs悲观锁
乐观锁:预期锁冲突的概率很低。乐观锁认为别的线程不会同时修改数据,所以不会上锁。
悲观锁:预期锁冲突的概率很高。每次去拿数据的时候,认为别的线程也会同时修改数据,所以每次拿数据的时候都会上锁。
2.读写锁vs互斥锁
读写锁:
加读锁:如果代码只进行读这个操作,就加读锁。
加写锁:如果代码只进行修改这个操作,就加写锁。
解锁:
针对读锁和读锁之间,是不存在互斥关系,读锁和写锁,写锁和写锁才存在互斥关系。
读写锁特别适合于"频繁读,不频繁写"的场景中。
互斥锁
互斥锁只有加锁和解锁这两个步骤,只要两个线程对同一个对象加锁,就会产生互斥。
3.重量级锁vs轻量级锁
重量级锁:做了更多的事情,开销也很大。加锁机制重度依赖了OS提供了mutex。通常情况下,悲观锁一般是重量级锁。
轻量级锁:做了较少的事情,开销也比较小。加锁机制尽可能不适用mutex,而是尽量在用户态代码完成,如果实在搞不定,再使用mutex。
4.挂起等待锁vs自旋锁
挂起等待锁:当莫个线程没有申请到锁的时候,此时该线程会被挂起来,即加入到等待队列中等待,当锁释放的时候,就会被唤醒,重新竞争锁。往往通过内核的一些机制来实现的。这是重量级锁的一种体现。
自旋锁:当莫个线程没有申请到锁的时候,该线程不会被挂起来,原地转圈圈,每隔一段时间来检测锁释放被释放,如果锁被释放了,那就竞争锁;如果没有释放,过一会儿再来检测。一旦锁被其他线程释放,就能第一时间获取到锁。
自旋锁是一种典型的轻量级锁的实现方式。
优点:没有放弃cpu,不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁。
缺点:如果锁被其他线程持有的时间比较久,那么就会持续的消耗cpu资源,(而挂起来等待的时候是不消耗cpu的).
5.公平锁vs非公平锁
公平锁:是遵循先来后到(多个线程在等待一把锁的时候)。
非公平锁:是不遵循先来后到(多个线程在等候一把锁的时候,获取到锁的概率是均等的。)
6.可重入锁vs不可重入锁
针对同一个锁对象连续锁两次,不会产生死锁,那就是可重入锁,会产生死锁,那就是不可重入锁。
2:Synchronized原理
1.是一个乐观锁,也是一个悲观锁(根据锁竞争的激烈程度,自适应)。
2.不是读写锁,只是一个普通互斥锁。
3.是一个轻量级锁,也是一个重量级锁(根据锁竞争的激烈程度,自适应.)
4.轻量级锁的部分属于自旋锁来实现,重量级的部分基于挂起等待锁来实现。
5.非公平锁。
6.可重入锁。
2.1:加锁工作工程
1)偏向锁:
首个线程加锁,就会进入偏向锁状态。偏向锁不是真的“加锁”,只是做了一个标记。偏向锁本质上相当于“延迟加锁”,能不加锁就不加锁,尽量来避免不必要的加锁开销。但是该做的标记还是的做,否则无法区分何时需要真正的加锁。
这样的好处:就是后续如果没有其他线程竞争的时候,就不加锁,避免加锁带来的解锁。
偏向锁:主要用来优化同一个线程多次申请同一个锁的竞争
举一个例子:
我是一个渣男,看见了一个漂亮的女孩,想让她做我女朋友,但不想最后我不和她好的时候,她不跟我分手(跟这个女孩解锁),所以我决定搞暧昧,我和她假装是男女朋友,但实则我并没有给这个女孩表白,但一旦有其他男孩追这个女孩,我就立马表白,给这个女孩进行加锁。
2.2:其他的优化操作
2.2.1:锁消除
在单线程的时候,就不用使用锁,利用你使用了StringBuffeer Vector这个在标准库中进行了加锁,这时候编译器+JVM会自己判定锁是否可以给消除,如果可以,就直接消除。
2.2.2:锁粗化
这里的粗化,加锁的范围扩大。
锁的粒度越细,多个线程之间的并发度越高,加锁和解锁的开销也就越大。
public static void main(String[] args) {
Object lock=new Object();
Thread t=new Thread(()->{
for (int i=0;i<10;i++){
synchronized (lock){//锁细化
System.out.println(i);
}
}
});
Thread t1=new Thread(()->{
synchronized (lock){//锁粗化
for (int i=0;i<10;i++){
System.out.println(i);
}
}
});
t.start();
t1.start();
}
3.CAS
CAS:全称:Compare and swap。意思就是比较并交换。
我们假设线程中cpu中的数据为V,内存中的旧数据为P,要修改的数据为B。
1.要比较P 和V 是否相等。(比较)
2.如果相等,就将B写入到P里面(交换)
3.返回操作是否成功。
3.1:实现原子类
public static void main(String[] args) throws InterruptedException {
AtomicInteger num=new AtomicInteger(0);
Thread t=new Thread(()->{
for (int i = 0; i <5000 ; i++) {
num.getAndIncrement(); //num++;
}
});
Thread t1=new Thread(()->{
for (int i = 0; i <5000 ; i++) {
num.getAndIncrement();
}
}) ;
t.start();
t1.start();
t.join();
t1.join();
System.out.println(num.get()); //这个方法可以得到原子类内部的数值
}
3.2:CAS中的ABA问题
3.2.1:什么是ABA问题
假设存在两个线程 t 和 t1,有一个共享变量 num。初始值为10
线程 t 想使用CAS把num的值改为20。但是在这期间。t1 线程把num的值改成15,之后又改成10。
那么这时候线程 t 是否要更新num的值为20(线程 t 的num的值是否和内存num的值相等)
3.2.2:存在的BUG问题。
假如当你要向别人付款50元,你只有100元,由于网卡,你第一次点击确认没有反应,你又点击了一次。这时候生成了两个线程,当线程一完成付款的时候,你剩余50元,线程二在阻塞等待,这时候,你的好伙伴给你转账50元。你余额100。当线程二,拿自己线程中的值和内存的值进行比较相等,又扣除50。这样你就扣除了两次。
3.2.3:解决办法
给要修改的值,引入版本号。在CAS比较数据当前值和旧值的同时,也要比较版本号是否符合预期。
1.CAS操作在读取旧值的同时,也要读取版本号。
2.如果当前的版本号和读取的版本号相同,则修改数据,并将版本号加一。
3.如果当 前的版本号要低于读取的版本号,则操作失败。
线程二最后读取的value值相同,但版本号不同。所以线程二操作失败。
总结:
以上就是我总结的锁的相关知识和CAS的相关问题,若有错误之处,请留言纠错,若感觉不错。请一键三连。