我直接开始爆肝!!!
文章目录
目录
一、Synchronized 锁策略
1.乐观锁:认为接下来的任务大概率不会有锁竞争 (正常情况下乐观锁的效率更高)
悲观锁:认为接下来的任务锁竞争的概率很大~
2.轻量级锁:加锁和解锁的速度比较快,更高效 //乐观锁很可能是个轻量级锁
重量级锁 :加锁和解锁的速度比较慢 //悲观锁很可能是个重量级锁
3.自旋锁(轻量级锁的实现):如果加锁失败,就一直围绕这个锁,一直访问锁是否被占用,一旦释放就可以第一时间获取到这个锁
挂起等待锁(重量级锁的实现):如果加锁失败,就不会再看这个锁,去给其他的加锁。
4.互斥锁:synchronized就是互斥锁 ,加锁就是单纯的加锁(进代码块),出代码块就自动解锁
读写锁:锁分为读锁和写锁,读锁不会有线程安全问题,所以就不会有锁竞争,速度很快,除了写锁之外,写锁和写锁之间,写锁和读锁之间都有线程安全问题 ,读写锁一般使用于一写多读的情况~
5.可重入锁:如果一个线程对锁加锁两次,如果不死锁就是可重入锁
不可重入锁:如果一个线程对锁加锁两次,如果死锁就是不可重入锁
6.公平锁:线程是遵循先来后到原则的,解锁之后就,按照线程来的顺序加锁
不公平锁:线程不遵循先来后到原则,解锁之后,多个线程随即调度,互相竞争锁
二、synchronized特点
1.既是悲观锁又是乐观锁
2.既是轻量级锁又是重量级锁
3.轻量级锁是基于自旋锁实现的,重量级锁是基于挂起等待实现的
4.synchronized是非公平锁
5.synchronized是可重入锁
6.synchronized是互斥锁不是读写锁
三、synchronized 优化
1.synchronized的关键策略:锁升级
这里需要注意的就是偏向锁:偏向锁并不是真的加锁,而是在锁的头部进行一个 标记(属于那个线程),如果没有锁竞争就不会真正的加锁,只有出现锁竞争,才会取消偏向锁,真正加锁(轻量级锁/自旋锁),从而降低程序的开销
2.锁消除
锁消除就是编译器来判断当前的锁是否可以消除,一个程序代码中,使用了synchronized,但是可能并没有在多线程环境下,此时编译器就会做出优化,将synchronized消除,从而省去加锁解锁的开销
3.锁粗化
一段代码中如果频繁出现加锁解锁的情况,但是可能并没有其他线程来抢占这个锁,这种情况编译器就会自动优化,避免频繁的加锁解锁浪费资源。把加锁和解锁整成一次,虽然并发的程度降低了,但是你频繁的加速和解锁的开销是要大于这个的
四、CAS是什么?
CAS全称是Compare and swap 就是比较和交换,一个CAS涉及到以下操作~
比较寄存器A和内存M 的 数值,如果相同,就讲寄存器B的值交换给内存M(其实就是赋值,寄存器中的值不用在意)
CAS的伪代码: 伪代码是不能正常运行的,只是方便理解~
expectValue就是寄存器A,swapValue就是寄存器,比较内存和寄存器A的数值是否相同,如果相同就把寄存器B的值赋值给内存
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
这里的伪代码,并非原子的,如果是多线程运行,可能就会出现BUG
但是事实上我们的CAS操作,只是一条CPU指令(原子的)(靠CPU的支持),这一条指令就可以完成上面这段代码的功能!!
我们就可以使用CAS来让我们不加锁,也能保证线程安全了~~
五、CAS实现的操作
1.实现原子类
原子类是可以保证线程执行的时候++,-- 的时候线程安全的类
先来看一下原子类怎么用吧~~
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
AtomicInteger num=new AtomicInteger(0); //自带的原子类(原子类的操作都是线程安全的)
Thread t1=new Thread(()->{
for (int i = 0; i <50000; i++) {
num.getAndIncrement(); //原子类内置的++方法,都是安全的 想当于num++
num.incrementAndGet(); //相当于++num
// num.decrementAndGet(); //相当于--num
// num.getAndDecrement(); //相当num--
}
});
Thread t2=new Thread(()->{
for (int i = 0; i <50000; i++) {
num.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(num.get()); //get方法获取数值
}
}
这样我们使用原子类,就可以不用加锁也可以实现线程安全的++ -- 等方法的操作了
那么CAS是如何实现原子类的呢??
伪代码:
class AtomicInteger {
private int value; //初始化的数值
public int getAndIncrement() {
int oldValue = value; //oldValue 可以视为是个寄存器 此处用变量表示
while ( CAS(value, oldValue, oldValue+1) != true) { //判断value和oldValue 是否相等
oldValue = value; //如果相等就交换oldValue+1(相当于++了)
} //和oldValue的值 然后CAS返回true结束循环~
return oldValue; //如果不相同,则CAS啥都不干返回false 进入循环
} //重新设置oldValue的值
}
上述代码就可以用CAS实现原子类~,原子类并不涉及加解锁操作,所以可以更高效的完成多线程的自增自减操作~
上述代码还有需要注意的地方:为什么我上面已经int oldValue = value,我下面还要CAS对比value和oldValue呢??这里不是一定是一样的嘛???
事实上 这里的value和oldValue还真可能不一样~ 想象一个场景
如果t1线程,运行到赋值完了,where之前,t2线程来了,t2线程执行,执行完了,此时的value就已经+1了,回到t1线程,此时的value就和之前的oldValue不同了~
所以此时我们需要判断两次,如果真因为多线程导致value不一样了,那就判断false 进入循环体,再次赋值就OK了
2.实现自旋锁
伪代码:
public class SpinLock {
private Thread owner = null; //用来记录当前的锁被那个线程持有
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){ //如果是空的就立即加锁 然后结束
} //如果锁被别持有了就一直等待,判断
} //一解锁就立即加锁~(一释放就能立即获取) //如果被占用,就会一直忙等
public void unlock (){
this.owner = null;
}
}
六、CAS的ABA问题
有一种情况~,如果我的Value数值本来是100,然后我线程A给他减了50,线程B又给他加了50,数值前后是一样的,但是这到底算变没变过呢???
虽然这种情况大多数不会出现问题,但是难免还是会有特殊情况:滑稽老铁,去银行存钱,账户里有100,老铁要取出50块钱,此时两个线程 t1 t2,他俩都能执行这个任务,我们期望的是线程1执行完,账户扣了50 剩余50,另一个线程在t1扣钱的时候再等待,轮到他是时候,发现存款已经变成50了,就不再扣钱了~
这是我们期望的,但是如果t1线程在扣钱的时候,扣完了变成了50,就在t2线程还没执行的时候,滑稽哥的朋友给他转了50扣钱,此时存款又变成100了,线程t2一看,前面那老哥没干活啊,还得看我,又给你扣了五十块钱,虽然结果都是50扣钱,但是这样是扣了你两次,这就出现BUG了~~
如何解决ABA问题
要解决ABA问题其实很简单,我们只需要定义一个版本号,来确定当前数值便没变过,如果发生修改操作,版本号就+1(或者-1),后面线程要执行的时候对比一下版本号(如果之前的版本号小于现在的,那么就不能执行)就可以了。
带入到刚才滑稽老哥的场景,初始版本号为1,第一次线程t1和t2都获取,任务要将100扣款为50,当前版本号为1,线程1执行成功,存款余额为50,版本号变为2,线程1执行完,在线程2还没执行的时候,滑稽朋友存款50,版本号变为3,此时到t2线程,来判断版本号和开始获取的1,如果版本号小于当前版本号,就操作失败~
总结
以上介绍本文的全部内容了,如果有任何问题欢迎私信改正或交流哦~欢迎大佬们.感谢您的支持