一,ReentrantLock
1.ReentrantLock是什么?
2.公平锁与非公平锁
- 公平锁:先到先得原则。如果锁被线程释放之后,先申请的线程先得到锁。公平锁性能较差一些,因为公平锁为了保证事件上的绝对顺序,上下文切换频繁
- 非公平锁:相对公平锁来说,如果锁被线程释放之后,后续所有申请的线程都有可能获得到锁,不会按照某一个顺序来,是随机的。非公平锁性能更高,但是可能导致某些线程永远获取不到锁
3.ReentrantLock的使用
lock(): 加锁 , 如果获取不到锁就死等 .trylock( 超时时间 ): 加锁 , 如果获取不到锁 , 等待一定的时间之后就放弃加锁 .unlock(): 解锁
4.ReentrantLock与Synchronized区别
- Sychronized依赖于JVM,而ReentrantLock依赖于API:Synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的
- Sychronized使用时不需要手动释放锁,ReentrantLock使用时需要手动释放锁,使用起来更加的灵活,但是也容易遗漏unlock
- Sychronized在申请锁失败时,会死等知道获取到锁;ReentrantLock可以通过trylock的方式等待一段时间之后就放弃等待
- Synchronized是非公平锁;ReentrantLock默认是非公平锁,可以通过构造方法传入一个true开启公平锁模式
-
更强大的唤醒机制 . synchronized 是通过 Object 的 wait / notify 实现等待 - 唤醒 . 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待 - 唤醒 , 可以更精确控制唤醒某个指定的线程.
-
ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
5.可中断锁和不可中断锁有什么区别
- 可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。
ReentrantLock
就属于是可中断锁。 - 不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。
synchronized
就属于是不可中断锁
6.如何选择使用哪个锁?
- 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
- 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
- 如果需要使用公平锁, 使用 ReentrantLock.
二,Atomic原子类
1.什么是原子类
Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
所以,所谓原子类就是具有原子操作特征的类
根据操作的数据类型,可以将JUC包中的原子类分为四类:
- 基本类型
AtomicInteger
:整型原子类AtomicLong
:长整型原子类AtomicBoolean
:布尔型原子类
- 数组类型
AtomicIntegerArray
:整型数组原子类AtomicLongArray
:长整型数组原子类AtomicReferenceArray
:引用类型数组原子类
- 引用类型
AtomicReference
:引用类型原子类AtomicMarkableReference
:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来。AtomicStampedReference
:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
- 对象的属性修改类型
AtomicIntegerFieldUpdater
:原子更新整型字段的更新器AtomicLongFieldUpdater
:原子更新长整型字段的更新器AtomicReferenceFieldUpdater
:原子更新引用类型里的字段
2.基本类型原子类
以AtomicInteger为例:常用方法有
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
3.基本数据类型优势
通过一个简单例子带大家看一下基本数据类型原子类的优势
1、多线程环境不使用原子类保证线程安全(基本数据类型)
class Test {
private volatile int count = 0;
//若要线程安全执行执行count++,需要加锁
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
2、多线程环境使用原子类保证线程安全(基本数据类型)
class Test2 {
private AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
public int getCount() {
return count.get();
}
}
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
三,CSA
1.什么是CAS?
CAS即compare and swap(比较与交换),它的主要思想很简单:就是用一个预期值和一个要更新变量的值进行比较,两值相等才会将新的值写入,否则不会操作成功。
CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。实际上Java的CAS利用的是unsafe这个类提供的CAS操作;unsafe的CAS依赖于JVM针对于不同的操作系统实现Atomic::cmpxchg;Atomic::cmpxchg的实现使用了汇编语言的CAS操作,并使用CPU提供的lock机制保证其原子性。简而言之,有了硬件层面的支持,软件层面才可以做到
2.CAS操作
CAS涉及三个操作数:
- V:要更新的变量
- E:预期值
- N:拟写入的新值
当且仅当V的值等于E的值时,CAS通过原子方式用新值N来更新V的值,如果不等,说明已经有其他线程更新了V,则当前线程放弃更新。
3.CAS的ABA问题
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。
对于这个问题,我们解决的方法是:给要修改的值,引入版本号,在CAS比较当前值和预期值相同的同时也需要比较版本号是否符合预期
- CAS 操作在读取旧值的同时, 也要读取版本号.
- 真正修改的时候,
- 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
- 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了)