目录
一、乐观锁&悲观锁
1.1、悲观锁
1.2、乐观锁
二、重量级锁&轻量级锁
2.1、轻量级锁
2.2、重量级锁
三、自旋锁&挂机等待锁
3.1、自旋锁
3.2、挂起等待锁
四、读写锁&普通互斥锁
4.1、读写锁
4.2、互斥锁
五、公平锁&非公平锁
六、可重入锁&不可重入锁
6.1、可重入锁
6.2、不可重入锁
七、对比synchronized
八、CAS
8.1、什么是CAS
8.2、CAS应用
8.2.1、实现原子类
8.3、CAS的ABA问题
一、乐观锁&悲观锁
1.1、悲观锁
在获取锁的时候预期这个锁的竞争十分激烈,那么就必须先加锁再执行任务,阻塞其它想获取锁的任务。
1.2、乐观锁
在获取锁的时候预期这个锁的竞争不太激烈,那么就可以先不加锁,或者少加锁(有真实的竞争的时候再加锁)
举个栗子:
坤坤要在西安开演唱会,小黑子练习时长两年半好不容易抢到了一张咯咯的演唱会门票去看咯咯的演唱会,就在开场10分钟后,小黑子觉得肚子不舒服想要去厕所方便一下到了厕所这个点没几个人来上厕所,小黑子就没有给厕所门上锁,当他听见有脚步声才把门插上。
二、重量级锁&轻量级锁
锁的核心特性”原子性“,这样的机制追根溯源是CPU这样的硬件设备提供的。
- CPU提供了"原子操作指令".
- 操作系统基于CPU的原子指令,实现了mutex互斥锁.
- JVM基于操作系统提供的互斥锁,实现了synchronized和ReentrantLock等关键字和类.
- 注意:synchronized并不仅仅是对mutex进行封装,在synchronized内部还做了其它很多工作。
2.1、轻量级锁
轻量级锁的加锁过程比较简单,用到的资源比较少,典型的就是用户态的一些加锁操作(在java代码层面就可以完成加锁).
- 少量的内核态用户态切换.
- 不太容易引发线程调度.
2.2、重量级锁
重量级锁的加锁过程比较复杂,用到的资源也比较多,典型的就是内核态的一些加锁操作.
- 大量的内核态用户态切换.
- 很容易引发线程调度.
乐观锁是能不加就不加锁,导致消耗的资源也就少了,所以乐观锁也是轻量级锁。
悲观锁是能加就加锁,导致消耗的资源也就多了,所以悲观锁也是重量级锁。
三、自旋锁&挂机等待锁
3.1、自旋锁
自旋锁伪代码:
while (抢锁(lock) == 失败){}
如果获取锁失败,立即尝试获取锁,无限循环,直到获取锁为止,第一次获取锁失败,第二次的尝试会在极短的时间内到来。所以一旦锁资源被释放,就能够第一时间获取到锁。
自旋锁是一种典型的轻量级锁实现方式.
- 优点:没有放弃CPU,不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁。
- 缺点:如果锁被其它线程持有的时间比较久,那么就会持续消耗CPU资源.(而挂起等待锁是不需要消耗CPU的)
synchronized中的轻量级锁策略大概就是通过自旋锁的方式实现的.
3.2、挂起等待锁
不主动访问锁资源,而是让系统调度去竞争锁资源。
- 通过阻塞与就绪状态的切换来获取锁资源.
- 锁一旦被释放,没有办法立刻知道.
- 是通过系统内核来处理的.
挂起等待锁是一种典型的重量级锁实现方式.
四、读写锁&普通互斥锁
4.1、读写锁
多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方之间以及和读者之间都需要进行互斥。如果两种场景下都使用同一个锁,那么就会产生极大的性能损耗,所以产生了读写锁。
读写锁:
在读的时候加读锁(共享锁),多个锁可以共存,同时加多个读锁是互不影响的。
在写的时候加写锁(排他锁),只有一个写锁在执行任务时,和别的锁是冲突的。
- 写锁和写锁不能共存.
- 写锁和读锁不能共存.
- 读锁和读锁可以共存.
读写锁就是把读操作和写操作区分对待,java库中提供了ReentrantReadWriteLock 类, 实现了读写
锁.
- ReentrantReadWriteLock.readLock类表示一个读锁,这个对象提供了lock()/unlock方法进行加锁。
- ReentrantReadWriteLock.writeLock类表示一个写锁,这个对象也提供了lock()/unlock方法进行加锁。
读写锁特别适合于"频繁读,不频繁写"的场景中.
synchronized不是读写锁
4.2、互斥锁
有竞争关系,只能一个线程释放锁之后,别的线程再来抢。
五、公平锁&非公平锁
5.1、公平锁
公平锁讲究的守规矩,遵循”先来后到“的原则,先排队的线程先获取到锁,后排队的线程后获取到锁。
5.2、非公平锁
大家都去抢锁,谁抢到就是谁的。
注意:
- 操作系统内部的线程调度就可以视为随机调度,如果不作任何额外的限制,锁就是非公平锁,如果想要实现公平锁,就需要依赖额外的数据结构,来记录线程顺序。
- 公平锁和非公平锁没有好坏之分,关键还要看适用场景。
synchronized是公平锁
六、可重入锁&不可重入锁
6.1、可重入锁
可重入锁可以对一把锁连续加锁多次,而不造成死锁。加锁多次那么解锁也要多次解锁,否则其它线程就无法获取到锁。
java中只要以Reentrant开头的都是可重入锁,而且JDK提供的所有的现成的lock实现类,包括synchronized都是可重入的
6.2、不可重入锁
不可重入锁就是对一把锁连续加锁多次,造成死锁。linux系统提供的mutex是不可重入锁。
七、对比synchronized
八、CAS
8.1、什么是CAS
CAS:全称Compare and swap,意思就是”比较并交换“,一个CAS涉及到以下操作。
假设内存中的源数据V,旧的预期值是A,需要修改的新值为B。
1、比较A与V是否相等(比较)。
2、如果比较相等,将B写入A(交换)。
3、返回操作是否成功。
举个栗子:
场景:
泡了一杯茶,泡上了之后,有事需要出去一趟。
十分钟回来之后,看了一下茶,如果茶还是满杯和走之前是一样的,那么就代表没有人喝过,就可以继续喝。
如果这个茶剩半杯了,就代表有人喝过了,那么就不能喝了,再重新泡一杯。
执行CAS操作重要的是确定有没有其它线程修改过第一次获取的值,如果确定没有被修改过,那么当前线程去修改是通过一条CPU指令去完成的,那么修改的过程就是线程安全的。
8.2、CAS应用
8.2.1、实现原子类
标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.
典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();
假设两个线程同时调用 getAndIncrement
1) 两个线程都读取 value 的值到 oldValue 中. (oldValue 是一个局部变量, 在栈上. 每个线程有自己的栈)
2) 线程1 先执行 CAS 操作. 由于 oldValue 和 value 的值相同, 直接进行对 value 赋值.
注意:
- CAS 是直接读写内存的, 而不是操作寄存器.
- CAS 的读内存, 比较, 写内存操作是一条硬件指令, 是原子的.
3) 线程2 再执行 CAS 操作, 第一次 CAS 的时候发现 oldValue 和 value 不相等, 不能进行赋值. 因此需要进入循环,在循环里重新读取 value 的值赋给 oldValue
4) 线程2 接下来第二次执行 CAS, 此时 oldValue 和 value 相同, 于是直接执行赋值操作.
5) 线程1 和 线程2 返回各自的 oldValue 的值即可.
通过形如上述代码就可以实现一个原子类. 不需要使用重量级锁, 就可以高效的完成多线程的自增操作.
示例代码:
import java.util.concurrent.atomic.AtomicInteger;
//CAS原子类
public class Exe_01 {
public static void main(String[] args) throws InterruptedException {
//基于CAS的原子类
AtomicInteger atomicInteger=new AtomicInteger();
Thread t1=new Thread(() ->{
for (int i = 0; i < 50000; i++) {
//通过调用并获取自增的方法实现自增操作。
atomicInteger.getAndIncrement();
}
});
Thread t2=new Thread(() ->{
for (int i = 0; i < 50000; i++) {
//通过调用并获取自增的方法实现自增操作。
atomicInteger.getAndIncrement();
}
});
//启动线程
t1.start();
t2.start();
//等待两个线程结束
t1.join();
t2.join();
//打印执行结果
System.out.println("执行结果——》"+atomicInteger.get());
}
}
8.3、CAS的ABA问题
刚才的场景:
1、泡好茶出门了,十分钟又回来了,按照之前的逻辑,检查杯子中的水满着没有,如果满着就代表没有人动过,可以继续喝。
2、但是存在一个问题,中途有人喝了一半之后,又续上了,回来一看还是这杯水,这杯水跟走之前那杯水完全不一样了。
前面看到的结果(A)和后面看到的结果(A)是一样的,但是中间发生过改变(B),只不过这个值跟取值之前是一样的。
如何解决这个问题?
这个值加一个属性,记录一下修改的次数(版本号),这个值只增不减,只要这个值做了修改就+1.