乐观学习,乐观生活,才能不断前进啊!!!
我的主页:optimistic_chen
欢迎大家访问~
创作不易,大佬们点赞鼓励下吧~
文章目录
前言
经过前面几次博客的总结,对于多线程编程,我们有了一定了解,接下来我们会更加深入了解的关键是 · 锁 ·,针对不同情况下,我们将采用不同的锁策略,对以后工作合理使用锁更加得心应手。
几种锁策略
悲观锁与乐观锁
首先, 这里不针对某一种具体的锁,而是某个锁具有“悲观”或者“乐观”特性
悲观锁:假设线程对锁的竞争十分激烈,就需要对这种情况额外做一些操作。
<举个例子>假设办公室只有一个老师,但是有10个同学同时想要问问题,就会出现一个同学想要问题时,老被其他某一个同学占用。这个时候就需要额外操作来问问题了。 |
乐观锁:假设线程对锁的竞争不激烈,一般不会出现冲突
<举个例子>假设目前只有两个同学,只有一个老师,两个同学需要问老师的时间很可能不会冲突,正常问问题就行。 |
总结:这里描述的都是加锁时(问题)的场景。
重量级锁与轻量级锁
之前有说过,锁的特性是“原子性”的,这种特性的根本是CPU硬件发出的“微指令”
重量级锁:在悲观场景下,付出更多的代价解决问题 ——》更低效
不断的在内核态与用户态之间切换,调用大量资源
轻量级锁:在乐观场景下,付出较小的代价解决问题—— 》更高效
尽可能少的在内核态与用户态之间切换,减少资源消耗
总结:用来遇到不同场景下的不同解决方案
挂起等待锁与自旋锁
挂起等待锁:如果获取锁失败,直接挂起等待.实现重量级锁的典型表现。
属于操作系统内核级别的操作,加锁的时候有竞争,使该线程进入阻塞状态,后续需要内核唤醒。虽然获取锁的时间更长,但是这个过程不消耗CPU资源。(竞争激烈)
自旋锁:如果获取锁失败,⽴即再尝试获取锁, ⽆限循环, 直到获取到锁为⽌.轻量级锁的典型表现
属于应用程序级别的操作,加锁的时候很少有竞争,一般不进入阻塞,而是通过忙等来等待。虽然回获取锁的时间更短,但是忙等的过程中一直消耗CPU资源。(竞争不激烈)
那我们之前的synchronized是乐观还是悲观锁呢?
成年人的世界当然都要了~, 它既是乐观锁又是悲观锁。属于自适应锁,也就是看情况而论。 synchronized作为大佬设计好的作品,为我们提前做好了一切准备。 如果竞争不激烈,此时synchronized就会按照轻量级锁(自旋)使用;如果竞争激烈,此时synchronized就会按照重量级锁(挂起等待)使用。 |
普通互斥锁与读写锁
普通互斥锁就是synchronized正常加锁、解锁
读写锁:读方式加锁、写方式加锁、解锁
<举个例子>如果你给读和写都加上普通互斥锁,意味着锁冲突非常严重。而读写锁就能确保读锁和读锁之间不会互斥(阻塞),保证线程安全的前提下,降低锁冲突的概率,提高效率。 |
适用于读多、写少的情况,典型的例子就是教务系统。
可重入锁与不可重入锁
之前说synchronized时,它就是一个“可重入锁”,就是一个线程,一把锁,连续加锁多次,是否会死锁。
判定标准很明确:
1. 锁要记录当前是哪个线程拿到的这把锁
2. 使用计数器,记录当前线程加锁了多少次,在合适时候进行解锁。
如果一个线程,一把锁,连续加锁多次,没有变成死锁,那它就是可重入锁;反之就是不可重入锁。
公平锁与非公平锁
公平锁:遵循先来后到原则,是先来谁先获取锁。
非公平锁:遵循概率相等原则,谁都有可能获取锁(随机)。
我们知道操作系统内部的线程调读就是随机,如果没有任何限制,锁就是非公平锁;如果要实现公平锁,就需要依赖额外的数据结构,记录先后顺序。
synchronized原理
锁升级
锁消除
编译器优化的一种体现,前面提到编译器优化是为了提出 volatile 关键字,这里是为了判定当前代码逻辑是否需要加锁,如果不需要,但是你写了synchronized,就会自动把synchronized去掉。(在编译器100%确认的情况触发)
锁粗化
锁的粒度:加锁和解锁之间,包含的代码越多(实际执行的时间),就认为锁的粒度就越粗。
实际应用中,加锁之后的解锁是为了锁能够被其他线程使用,但是可能并没有其他线程来抢占这个锁,那么就没必要去解锁,,JVM就会把锁粗化,避免频繁释放锁,消耗资源。
注意:如果三个任务本身都需要加锁,那么粗化为一个锁就可以,但是如果只有两个任务需要加锁,粗化为一把锁就不合适了,能够并行的任务,变成串行了。
原子性
原子类
//private static int count=0;
//使用原子类 ,代替int
private static AtomicInteger count=new AtomicInteger(0);
public static void main(String[] args) {
Thread t1=new Thread(()->{
for(int i=0;i<20000;i++){
//count++
count.getAndIncrement();
}
});
Thread t2=new Thread(()->{
for(int i=0;i<20000;i++){
count.getAndIncrement();
}
});
t1.start();
t2.start();
try{
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count.get());
}
CAS
- compare and swap(比较并交换)
内存地址 寄存器的值 另一个寄存器的值
boolean CAS(address,expectValue,swapValue){
if(&address==expectedValue){//判断内存的值是否和寄存器的值一样
&address==swapValue;//相同,就把另一个寄存器的值交换给内存
return ture;//实质上是赋值
}
return false;
}
CAS操作严格意义上讲,它是CPU的一条指令,它的来源是:操作系统对CPU指令(CAS)进行封装,提供一些API,可以在C++重被调用,而JVM又是基于C++实现的,JVM也能够调用CAS这样的原子性操作。
使用这种原子性操作既保证了性能,又保证了线程安全。
- 基于CAS实现自旋锁
private Thread ower=null;//如果为null,锁是空闲
public void lock(){
//通过CAS看当前锁是否被某个线程持有
//如果锁被使用,则自旋等待
//如果锁空闲,那么ower设为当前需要加锁的线程
while(!CAS(this.ower,null,Thread.currentThread())){
}
}
public void unlock(){
this.ower=null;
}
CAS的缺陷 ABA问题
CAS能够线程安全,核心是先比较内存和寄存器是否“相等”
int oldBalance=balance;
CAS(balance,oldBalance,oldBalance-500);
<举个例子> 假设我们存款有1000,需要取出500,那么按照CAS方式来取款,内存和寄存器1比较相等,再把寄存器2 扣除500 的操作交换给内存,但是在这时,又有人给你转账500,内存此时变为1000,又和寄存器1的值相等,继续进行交换(赋值),内存最后的值还是500。你的存款痛失500!! |
ABA问题的核心在于判断中间是否有其他线程修改值,那么我们约定一个概念“版本号”:只要存在修改,版本号就+1
int oldVersion=version;
if(CAS(version,oldVersion,oldVersion+1)){
balance-=500;
}
完结
可以点一个免费的赞并收藏起来~
可以点点关注,避免找不到我~ ,我的主页:optimistic_chen
我们下期不见不散 ~ ~ ~