【多线程】多线程(10):常见锁策略,锁原理,CAS
【常见的锁策略】
【乐观锁vs悲观锁】
乐观锁:加锁时假设出现锁冲突的概率不大——接下来围绕加锁要做的工作,就会更少
悲观锁:加锁时假设出现锁冲突的概率很大——接下来围绕加锁要做的工作,就会更多
【轻量级锁vs重量级锁】
轻量级锁:加锁的开销比较小,要做的工作相对更少
重量级锁:加锁的开销比较大,要做更多的工作
//通常悲观锁做的重,乐观锁做的轻
【自选锁vs挂起等待锁】
自选锁:悲观锁/重量级锁的一种典型实现
挂起等待锁:乐观锁/轻量级锁的一种典型实现
【公平锁vs非公平锁】
公平锁:加锁优先级为来后到
非公平锁:加锁优先级为概率均等
【可重入锁vs不可重入锁】
一个线程针对一把锁连续加锁2次,可能出现死锁,但若把锁设定为「可重入」就可以避免死锁
【读写锁】
若多个线程同时读一个变量,无线程安全问题
但一个线程读/一个线程写/两个线程都写,则会产生线程安全问题
因此产生「读写锁」,把读操作和写操作区分开来进行加锁
1.读加锁 2.写加锁
读写锁提供了2种加锁的API
加读锁:
加写锁:
(解锁的API是一样的)
如果两个线程都是按照读方式加锁,此时不会产生锁冲突
如果两个线程都是按照写方式加锁,此时会产生锁冲突
如果一个线程读锁,一个线程写锁,会产生锁冲突
即:读操作加锁时,读锁和读锁之间不发生互斥;写操作加锁时,读锁和写锁之间,写锁和写锁之间会互斥
该锁适合于频繁读,不频繁写的场景
【synchronized的锁原理】
【synchronized锁策略】
对于乐观/悲观,它是自适应的
对于重量/轻量,它是自适应的
对于自旋/挂起等待,它是自适应的
它是非公平锁
它是可重入锁
【synchronized加锁过程】
刚开始使用synchronized加锁,首先锁会处于“偏向锁”状态,遇到线程之间的锁竞争,升级到“轻量级锁”
进一步统计竞争出现的频次,达到一定的激烈程度之后,升级到“重量级锁”
synchronized加锁时,会经历无锁——偏向锁——轻量级锁——重量级锁的过程
【偏向锁和锁升级】
“偏向锁”不是真正的加锁(真的加锁开销比较大),偏向锁只是做个标记(标记的过程轻量而高效)
当出现锁竞争时,偏向锁会立即升级为轻量级锁(进行实质性加锁)
锁竞争激烈时,轻量级锁会立即升级为重量级锁
锁升级的过程中「不可逆」
【总结】
上述锁升级的过程,是为了能让synchronized这个锁很好地适应不同的场景,可以降低程序员的使用负担
【锁消除(编译器的优化策略)】
编译器会对写的synchronized代码作出判定,判定这个地方是否确实需要加锁,若没必要,则会自动把synchronized干掉
【锁粗化(编译器的优化策略)】
每次加锁可能会涉及到阻塞等待
锁粗化,就是把多个「细粒度」的锁,合并成一个「粗粒度」的锁,最大程度上减少阻塞等待带来的效率降低
【锁的粒度】
加锁的代码块中,代码越多,粒度越粗;代码越少,粒度越细
//算粒度看的是「实际执行的代码数量」,因此某些时候虽然代码块中只有几行代码,但调用的方法中代码很多,因此粒度粗
【CAS】
全称“compare and swap”,比较和交换
比较内存和cpu寄存器中的内容,如果发现相同,就进行交换(交换的是内存和另一个寄存器的内容)
【具体解释】
1.“一个内存的数据”和“两个寄存器中的数据”进行操作(寄存器1和寄存器2)
2.比较内存和寄存器1中的值,是否相等,若不相等,则无事发生,若相等,就交换内存和寄存器2的值
//此处一般只关心内存交换后的内容
//虽然叫做“交换”,实际上达成的效果可以看作是“赋值”
CAS的关键不在于其内部进行了什么操作,而是在于通过「一个cpu指令」完成了上述的一系列操作
【用途:基于CAS实现“原子类”】
int/long在进行++或--时,都不是原子的
基于CAS实现的原子类,对int/long等这些类型进行了封装,从而可以原子的完成++或--等操作
java标准库中提供了原子类的实现
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
for(int i = 0;i < 50000;i++){
count.getAndIncrement(); //count++;
// count.incrementAndGet(); //++count;
// count.getAndDecrement(); //count--;
// count.decrementAndGet(); //--count;
// count.getAndAdd(x); //count+=10;
}
});
Thread t2 = new Thread(() ->{
for(int i = 0;i < 50000;i++){
count.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
【如何通过CAS实现原子类?】
可看伪代码
value是内存数据,oldvalue是寄存器中的数据
1.把内存中的数据读取到寄存器中
2.while循环,在CAS中执行比较和交换的操作,比较value和oldvalue是否相同
若相同,意味着没有其他代码穿插到这两个代码之间执行,此时可以安全地修改变量中的内容:将寄存器2的值(oldvalue+1)交换(赋值)到value中
若不相同,意味着有其他代码穿插到这两个代码之间执行,此时就需要重新加载内存中的值到寄存器中(执行oldvalue=value)