锁类型
悲观锁
事事皆总作最坏的打算,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁
等,都是在做操作之前先上锁。Java中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
乐观锁
事事皆总作最好的打算,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition
机制,其实都是提供的乐观锁。在Java中 java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
应用场景
乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
常见锁算法
乐观锁常见算法
CAS算法
cas 锁(compare and swap) 对比交换
- CAS算法涉及到三个操作数
- 需要读写的内存值V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试
- 问题
- ABA 问题:
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
JDK 1.5 以后的 AtomicStampedReference类就提供了此种能力,其中的 compareAndSet方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 - 循环时间长开销大:
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率 - 只能保证一个共享变量的原子操作:
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了 AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用 AtomicReference类把多个共享变量合并成一个共享变量来操作。
- ABA 问题:
版本号算法
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
##常见悲观锁
Java Synchronized
synchronized是Java中的关键字,是一种同步锁。可修饰实例方法,静态方法,代码块。
ADD
什么是自旋锁?
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。 即乐观锁的一种实现。
自旋锁存在的问题
- 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
- 无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
自旋锁的优点
- 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
- 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
自旋锁的变种
MCS锁
MCS自旋锁是一种基於单向链表的高性能、公平的自旋锁,申请加锁的线程只需要在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class MCSLockV2 {
/**
* MCS锁节点
*/
public static class MCSNodeV2 {
/**
* 后继节点
*/
volatile MCSNodeV2 next;
/**
* 默认状态为等待锁
*/
volatile boolean blocked = true;
}
/**
* 线程到节点的映射
*/
private ThreadLocal<MCSNodeV2> currentThreadNode = new ThreadLocal<>();
/**
* 指向最后一个申请锁的MCSNode
*/
volatile MCSNodeV2 queue;
/**
* 原子更新器
*/
private static final AtomicReferenceFieldUpdater UPDATER = AtomicReferenceFieldUpdater.newUpdater(
MCSLockV2.class,
MCSLockV2.MCSNodeV2.class,
"queue");
/**
* MCS获取锁操作
*/
public void lock() {
MCSNodeV2 cNode = currentThreadNode.get();
if (cNode == null) {
System.out.println(String.format("Thread %s 初始化 ", Thread.currentThread().getId()));
// 初始化节点对象
cNode = new MCSNodeV2();
currentThreadNode.set(cNode);
}
// 将当前申请锁的线程置为queue并返回旧值
MCSNodeV2 mcsNodeV2 = (MCSNodeV2) UPDATER.getAndSet(this, cNode); // step 1
if (mcsNodeV2 != null) {
// 形成链表结构(单向)
mcsNodeV2.next = cNode; // step 2
// 当前线程处于等待状态时自旋(MCSNode的blocked初始化为true)
// 等待前驱节点主动通知,即将blocked设置为false,表示当前线程可以获取到锁
while (cNode.blocked) {
}
} else {
// 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己为非阻塞 - 表示已经加锁成功
cNode.blocked = false;
}
}
/**
* MCS释放锁操作
*/
public void unlock() {
// 获取当前线程对应的节点
MCSNodeV2 cNode = currentThreadNode.get();
if (cNode == null || cNode.blocked) {
// 当前线程对应存在节点
// 并且
// 锁拥有者进行释放锁才有意义 - 当blocked未true时,表示此线程处于等待状态中,并没有获取到锁,因此没有权利释放锁
return;
}
if (cNode.next == null && !UPDATER.compareAndSet(this, cNode, null)) {
// 没有后继节点的情况,将queue置为空
// 如果CAS操作失败了表示突然有节点排在自己后面了,可能还不知道是谁,下面是等待后续者
// 这里之所以要忙等是因为上述的lock操作中step 1执行完后,step 2可能还没执行完
while (cNode.next == null) {
}
}
if (cNode.next != null) {
// 通知后继节点可以获取锁
cNode.next.blocked = false;
// 将当前节点从链表中断开,方便对当前节点进行GC
cNode.next = null; // for GC
}
// 清空当前线程对应的节点信息
currentThreadNode.remove();
}
/**
* 测试用例
*
* @param args
*/
public static void main(String[] args) {
final MCSLockV2 lock = new MCSLockV2();
for (int i = 1; i <= 10; i++) {
new Thread(generateTask(lock, String.valueOf(i))).start();
}
}
private static Runnable generateTask(final MCSLockV2 lock, final String taskId) {
return () -> {
lock.lock();
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(String.format("Thread %s Completed %s ", Thread.currentThread().getId(), taskId));
lock.unlock();
};
}
}
- MCS锁的节点对象需要有两个状态,next用来维护单向链表的结构,blocked用来表示节点的状态,true表示处于自旋中;false表示加锁成功
- MCS锁的节点状态blocked的改变是由其前驱节点触发改变的
- 加锁时会更新链表的末节点并完成链表结构的维护
- 释放锁的时候由于链表结构建立的时滞(getAndSet原子方法和链表建立整体而言并非原子性),可能存在多线程的干扰,需要使用忙等待保证链表结构就绪
CLH锁
同MCS自旋锁一样,CLH也是一种基於单向链表(隐式创建)的高性能、公平的自旋锁,申请加锁的线程只需要在其前驱节点的本地变量上自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class CLHLockV2 {
/**
* CLH锁节点状态 - 每个希望获取锁的线程都被封装为一个节点对象
*/
private static class CLHNodeV2 {
/**
* 默认状态为true - 即处于等待状态或者加锁成功(换言之,即此节点处于有效的一种状态)
*/
volatile boolean active = true;
}
/**
* 隐式链表最末等待节点
*/
private volatile CLHNodeV2 tail = null;
/**
* 线程对应CLH节点映射
*/
private ThreadLocal<CLHNodeV2> currentThreadCacheNode = new ThreadLocal<>();
/**
* 原子更新器
*/
private static final AtomicReferenceFieldUpdater<CLHLockV2, CLHNodeV2> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLockV2.class, CLHNodeV2.class, "tail");
/**
* CLH加锁
*/
public void lock() {
CLHNodeV2 cNode = currentThreadCacheNode.get();
if (cNode == null) {
cNode = new CLHNodeV2();
currentThreadCacheNode.set(cNode);
}
// 通过这个操作完成隐式链表的维护,后继节点只需要在前驱节点的locked状态上自旋
CLHNodeV2 clhNodeV2 = UPDATER.getAndSet(this, cNode);
if (clhNodeV2 != null) {
// 自旋等待前驱节点状态变更 - unlock中进行变更
while (clhNodeV2.active) {
//等待unlock 时被释放
}
/**
* 因为unlock 后 当前线程已经结束 ,所以显示的是 阻塞线程的的下一线程的ID
*
*/
System.out.println(String.format("释放锁%s成功", Thread.currentThread().getId()));
}
// 没有前驱节点表示可以直接获取到锁,由于默认获取锁状态为true,此时可以什么操作都不执行
// 能够执行到这里表示已经成功获取到了锁
}
/**
* CLH释放锁
*/
public void unlock() {
CLHNodeV2 cNode = currentThreadCacheNode.get();
// 只有持有锁的线程才能够释放
if (cNode == null || !cNode.active) {
return;
}
// 从映射关系中移除当前线程对应的节点
currentThreadCacheNode.remove();
// 尝试将tail从currentThread变更为null,因此当tail不为currentThread时表示还有线程在等待加锁
if (!UPDATER.compareAndSet(this, cNode, null)) {
// 不仅只有当前线程,还有后续节点线程的情况 - 将当前线程的锁状态置为false,因此其后继节点的lock自旋操作可以退出
cNode.active = false;
}
}
/**
* 用例
*
* @param args
*/
public static void main(String[] args) {
final CLHLockV2 lock = new CLHLockV2();
for (int i = 1; i <= 10; i++) {
new Thread(generateTask(lock, String.valueOf(i))).start();
}
}
private static Runnable generateTask(final CLHLockV2 lock, final String taskId) {
return () -> {
lock.lock();
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(String.format("Thread %s Completed %s ", Thread.currentThread().getId(), taskId));
lock.unlock();
};
}
}