就感觉最近蛮水逆的,
希望水逆快快过去。
目录
1.线程常见锁策略
①什么是锁策略:
接下来讲解的锁策略不仅仅是局限于 Java . 任何和 "锁" 相关的话题, 都可能会涉及到以下内容。这些特性主要是给锁的实现者来参考的。
②乐观锁和悲观锁:
乐观锁:
预期锁冲突很低,做的更少,成本更低,更高效的操作
悲观锁:
预期锁冲突很高,做得更多,成本更高,更低效的操作
举个例子:
当我们快要进行期末考试时,就可以把大家分为两类人,乐观人和悲观人。乐观人觉得仅仅是一场考试而已,没有必要那么紧张,平时学好了,随便复习复习就行。而悲观人呢,就会觉得很紧张,一直不停地复习,从而做得更多,耗费的时间更多,因此对于其他事情就减少了平时的投入,显然这就是很低效的操作
③读写锁和普通的互斥锁:
读写锁:
是指分别对读和写单独进行加锁的操作。实际上有三个操作,加读锁,加写锁以及解锁。(注意!!!读锁和读锁之间是不会产生互斥关系的,只有读锁和写锁,写锁和写锁之间才会产生互斥关系)
普通的互斥锁:
只要两个或多个线程对同一对象进行加锁,就会产生互斥。它只有两个操作,加锁和解锁。
④重量级锁和轻量级锁:
重量级锁:
就是事情做多了,开销更大。如果锁是基于内核某些功能来实现的话,那么认为它是重量级锁。(操作系统中的锁会在内核中会做很多事情,比如让线程阻塞等待)
轻量级锁:
事情做少了,开销更小。如果锁是基于用户态来进行实现的话,那么认为它是轻量级锁。(用户态的代码更高效,更可控)
而在一般情况下,我们认为乐观锁是轻量级锁而悲观锁是重量级锁。
⑤挂起等待锁和自旋锁:
挂起等待锁:
往往是通过内核的一些机制来进行实现的,往往较重(是重量级锁的一种典型实现)
自旋锁:
往往是通过用户态代码来实现的,往往较轻(是轻量级锁的一种典型实现)如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝 试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁. 相比于挂起等待锁, 优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用.缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源.
⑥公平锁和非公平锁:
公平锁:
这里的公平锁,指的是多个线程在等待锁时,按照先来后到的顺序进行执行的
非公平锁:
指的是多个线程在等待锁时,不遵循先来后到的规则,他们获取到锁的概率是均等的
而对于操作系统而言,本身线程的调度就是随机的(机会相等的),操作系统提供的mutex锁就是非公平锁
⑦可重入锁和不可重入锁:
可重入锁:
一个线程针对同一把锁可以多次进行加锁
不可重入锁:
一个线程只能加一把锁,再继续加锁就会出现死锁的情况。
2.CAS
①解释CAS:
全称Compare and swap,意思:”比较并交换“。
②CAS具体操作:
我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
1. 比较 A 与 V 是否相等。(比较)2. 如果比较相等,将 B 写入 V。(交换)3. 返回操作是否成功。③对CAS伪代码的解释:下面写的代码不是原子的, 真实的 CAS 是一个原子的硬件指令完成的. 这个伪代码只是辅助理解 CAS 的工作流程.④CAS的意义:
为这种多线程的代码提供了安全的保障,提供了一种新的思路和方法。
2.1基于CAS实现的"原子类"
①基于CAS实现原子类的完整代码剖析:
Java标准库里提供了一组原子类,针对所常用多一些的
int, long, int array...
进行了封装,可以基于CAS
的方式进行修改,并且线程安全。标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的。典型的就是 AtomicInteger 类. 其中的getAndIncrement 相当于 i++ 操作。而更是因为CAS是基于原子类来实现的。因此它是线程安全的,并且相比于synchronized而言,CAS是一种更加高效的操作,
synchronized
会涉及到锁的竞争,两个线程要相互等待,CAS
不涉及到线程阻塞等待。下面我们就对之前用synchronized执行过的自增代码操作用CAS来进行操作实现:
import java.util.concurrent.atomic.AtomicInteger; public class demo1 { public static void main(String[] args) throws InterruptedException { AtomicInteger num=new AtomicInteger(); Thread t1=new Thread(()->{ for(int i=0;i<5000;i++){ //用getAndIncrement()来实现自增的操作 num.getAndIncrement(); } }); Thread t2=new Thread(()->{ for (int i=0;i<5000;i++){ num.getAndIncrement(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(num); } }
执行结果如下:
②利用伪代码进一步进行说明:
③利用画图来进一步阐述为什么上述自增操作是安全的:
2.2基于CAS实现的"自旋锁"
①自旋锁的伪代码及详解:
②进一步说明:
这里的自旋锁是轻量级的,这样的忙等,其实效率更高。
2.3CAS中的ABA问题(重点)
①什么是ABA?
我们知道CAS实质上是通过比较来对两个值进行交换的操作,所谓比较实际是对当前值和和旧值进行比较,但是这里就会存在一个问题,到底是一步到位的没有改变,还是说中间经过了变化,最后又变回来导致的结果没有改变。用下图就能够进一步清楚地为我们解释ABA问题。
就好比我要从成都到北京,我可以直接坐直达的列车,当然我可以先从成都到河北,再从河北到北京,最终我是到达了北京,但是你不知道我到底经历了什么而到达的北京。
②举个例子结合图解来进行分析说明:
滑稽老铁取钱去买东西
a. 基于非CAS问题:
在滑稽进行取款的时候,机器卡了一下,也就是滑稽多按了一次取款。这就相当于一次取钱操作执行了两次(两个线程并发地去执行了这个操作),而我们的目的是只取成功一次,即取走50,余额还剩50。
b.而基于CAS问题的方式:
来这里取款的话就会出现新的情况。现在的情况就是在滑稽取款的一瞬间,他朋友给他转了50,这个时候就会出现ABA问题,这两次的巧合导致了这个Bug,我们需要来解决这个问题
c.基于CAS但是非ABA问题:
所以按照上述分析,此处就是两次操作,实际上只有一次成功了,而上述的成功是因为没有引入ABA问题,下面我们就引入ABA问题进一步进行分析。
d.CAS基于ABA问题的分析:(我们期望取款一次,而在这里我们取了两次)
③ABA问题的解决方案:(通过添加版本号来进行解决)
给要修改的数据引入版本号. 在
CAS
比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当前版本号比之前读到的版本号大, 就认为操作失败。
3.synchronized中锁的优化机制
①synchronized锁的特点:
a.既是一个乐观锁,也是—个悲观锁(根据锁竞争的激烈程度,自适应)
b.不是读写锁只是一个普通互斥锁
c.既是一个轻量级锁,也是一个重量级锁(根据锁竞争的激烈程度,自适应)
d.轻量级锁的部分基于自旋锁来实现.重量级的部分基于挂起等待锁来实现
e.非公平锁(通过竞争来获得锁,而不是先来后到)
f.可重入锁
②典型的优化手段:
a.锁膨胀/锁升级(体现了synchronized能够自适应的特点)
在引入之前先介绍一个概念:什么是偏向锁?
偏向锁不是真的加锁, 而只是在锁的对象头中记录一个标记(记录该锁所属的线程). 如果没有其他线程参与竞争锁, 那么就不会真正执行加锁操作, 从而降低程序开销. 一旦真的涉及到其他的线程竞争, 再取消偏向锁状态, 进入轻量级锁状态.简而言之,偏向锁就是只是对锁做了一个标记,并没有真正地去锁上,它的好处就是没人竞争的时候避免了锁的开销。自旋锁和重量级锁前面都有讲述,就不再重复了
b.锁粗化/锁细化
这里的粗细指的是"锁的粒度"
(粒度表示加锁代码涉及到的代码范围,加锁代码的范围越大,认为锁的粒度越粗,加锁代码范围越小,锁的粒度越细)
那么锁的粒度究竟是粗好还是细好?其各有各的好处:
如果锁粒度比较细,多个线程之间的并发性就更高;
如果锁粒度比较粗,加锁解锁的开销就更小;编译器就会有一个优化:
如果某个地方的代码锁的粒度太细了,就会自动判定进行粗化;
如果两次加锁之间的间隔较大(中间隔的代码多),一般不会进行这种优化.如果加锁之间间隔比较小(中间隔的代码少),就很可能触发这个优化。c.锁消除
比如你在单线程里面使用了StringBuffer,Vector(这些都在标准库里面进行了加锁操作),就相当于是你在单线程里面进行了加锁。
用StringBuffer()来进行一个举例
代码:
StringBuffer sb = new StringBuffer(); sb.append("a"); sb.append("b"); sb.append("c"); sb.append("d");
然而对于单线程而言,这样的加锁显然是没有必要的,会白白浪费资源的开销。
所以,编译器+JVM 判断锁是否可消除。 如果可以, 就直接消。这里表示再有些地方,可能并不需要加锁,但是你不小心加上了锁,编译器发现加上这个锁好像没啥必要,编译器就会直接把锁给去掉了
感谢观看~