自旋锁
自旋锁,也是java中的一种锁,但是这个锁的实现原理,与sychronized不一样的。自旋锁从字面意义理解,就是当前线程一直循环获取锁,不达目的不罢休。相当于:线程调用自旋锁,如果没有获取锁,那么就一直在空循环,知道获取锁。
自旋锁就是通过代码进行while循环判断,直到获取锁成功,才进行下一步操作。
自旋锁分类
自旋锁分为两大类:1 有序自旋锁,2 无序自旋锁,即获取锁的顺序是否按照先到先得,有序自旋锁会维护一个链路,记录请求线程的先后顺序。其中有序自旋锁中,比较出名的有两种:CLH锁(Craig, Landin, and Hagersten locks)、MCS锁,两者最大的区别:CLH是循环判断前节点是否释放锁(他旋),而MCS是判断本身节点能否获取锁(自旋)。
无序自旋锁代码
示例1
package lock.review.lock;
import java.util.concurrent.atomic.AtomicBoolean;
/**无序自旋锁,即所有的线程是无序的争抢锁,不存在先到先得**/
public class DisorderedSpinLock {
//利用boolean state表明当前锁的状态,false锁未被使用,true锁被使用。利用AtomicBoolean保证state更新的原子性,防止同一线程多次加锁,多次释放锁。
public AtomicBoolean state = new AtomicBoolean(false);//false 代表未锁未被使用,true 代表锁被使用
//记录持有锁的线程,只有持有线程才能释放
public volatile Thread lockThread;
//加锁
public void lock(){
//利用while进行自旋
while(!state.compareAndSet(false, true)){
//获取锁,记录持有锁的线程,修改锁的状态
lockThread = Thread.currentThread();
}
}
//释放锁
public void unlock(){
if(lockThread == Thread.currentThread()){
//判断释放锁的线程是否有持有线程一致
state.compareAndSet(true, false);
}
}
}
因为上述自旋锁是无序的,如果要实现公平锁,就需要隐式或者显式的实现线程的队列。
线程调用DisorderedSpinLock.lock方法,就在while(!state.compareAndSet(false, true))方法循环,直到获取锁。
有序自旋锁
ticketSpinLock
ticketSpinLock源码:类似于买票,每个线程有一个票号,锁中有个当前可以获得锁的票号,通过比对票号进行顺序获取锁。
package lock.review.lock;
import java.util.concurrent.atomic.AtomicInteger;
/**有序自旋锁:ticket,从名称中就能看出,通过票的号码来实现有序,
* 即:每个线程都有一张ticket,根据ticket编号判断线程是否可以获取锁**/
public class TicketSpinLock {
//利用int ticket作为票的顺序号,即每个线程都有一个票号
public AtomicInteger ticket = new AtomicInteger(0);//记录已发的票号的最大值。
public AtomicInteger currentNum = new AtomicInteger(0);//允许持有锁的票号
public int lock(){
//线程获取票号
int ticketNum = ticket.getAndIncrement();
System.out.println(Thread.currentThread().getName()+" "+ticketNum+" 当前:"+currentNum.get());
//判断当前线程的票号,是否等于允许持有锁票号,如果相等说明当前线程可以持有锁,否则while循环
while(!(currentNum.get() == ticketNum));
System.out.println(Thread.currentThread().getName()+" 获取锁 "+ticketNum);
//返回当前线程的票号,用来解锁
return ticketNum;
}
public void unlock(int num){
System.out.println(Thread.currentThread().getName()+" 释放锁,票号:"+num+",当前票号 "+currentNum.get()+",下一票号");
//判断解锁的票号与当前持有票号是否一致
if(num==currentNum.get()){
//当前线程释放锁,票号自动加1,可以让在while循环中持有票号+1的线程获取锁
currentNum.compareAndSet(num, num+1);
}
}
}
ticketSpinLock:通过票号这一资源,显式的控制多线程的执行,因此解锁时,需要提供票号才可以解锁。当然了上述代码还有缺陷,就是解锁需要提供票号,这样很容易进行票号伪造,非法获取锁。
进阶版:
public class TicketSpinLock {
//利用int ticket作为票的顺序号,即每个线程都有一个票号
private AtomicInteger ticket = new AtomicInteger(0);//记录已发的票号的最大值。
private AtomicInteger currentNum = new AtomicInteger(0);//允许持有锁的票号
private Map<Thread,Integer> threadNumMap = new HashMap<>();
public void lock(){
//线程获取票号
int ticketNum = ticket.getAndIncrement();
System.out.println(Thread.currentThread().getName()+" "+ticketNum+" 当前:"+currentNum.get());
//判断当前线程的票号,是否等于允许持有锁票号,如果相等说明当前线程可以持有锁,否则while循环
while(!(currentNum.get() == ticketNum));
System.out.println(Thread.currentThread().getName()+" 获取锁 "+ticketNum);
//返回当前线程的票号,用来解锁
threadNumMap.put(Thread.currentThread(),ticketNum);
}
public void unlock(){
//判断解锁的票号与当前持有票号是否一致
Integer num = threadNumMap.get(Thread.currentThread());
System.out.println(Thread.currentThread().getName()+" 释放锁,票号:"+num+",当前票号 "+currentNum.get()+",下一票号");
if(num != null && num == currentNum.get()){
//当前线程释放锁,票号自动加1,可以让在while循环中持有票号+1的线程获取锁
currentNum.compareAndSet(num, num+1);
}
}
}
---
锁本身记录不同线程的票号,这样可以解决伪造票号的问题,
当然上述还有一些局限性,票号是AtomicInteger,而AtomicInteger是有最大值的。
CLH锁
CLH锁:CLH(Craig, Landin, and Hagersten locks): 是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。通过隐式的链表来作为线程的有序序列,但是CLH锁最大的特点是前置自旋,即while的判断条件是前一个节点的状态,前一个节点释放锁之后,当前节点才能持有锁。
也是一种自旋锁,只不过是通过链表来表明线程的先后顺序,而不是像ticketSpinLock通过票号来标明先后顺序。
CLH锁源代码
package lock.review.lock;
import java.util.concurrent.atomic.AtomicReference;
public class CLHLock {
public AtomicReference<Node> tail;//尾节点,标明有序队列的队尾。通过替换队尾进行有序排序。
//记录当前线程的节点,。
public ThreadLocal<Node> currentThreadLocal = new ThreadLocal<Node>();
public CLHLock(){
tail = new AtomicReference<Node>(new Node(false));//初始化尾节点是已经释放锁的
}
//获取锁
public void lock(){
Node currentNode = new Node(true);//创建当前线程node,需要获取锁
currentThreadLocal.set(currentNode);//记录当前线程所属的节点
Node preNode = tail.getAndSet(currentNode);//获取当前线程的前一节点,并设置当前线程为尾节点,通过前一节点是否释放锁进行判断。
while(!preNode.lock);
//获取锁
}
//释放锁
public void unlock(){
//释放锁,修改当前线程节点的lock状态
currentThreadLocal.get().lock=false;//释放锁
currentThreadLocal.remove();//help gc
}
}
//节点,每个节点存储当前线程是否获取锁
class Node{
//表明当前线程的状态:true 获取锁,false 释放锁,volatile修饰,使lock的修改可以立刻可见
public volatile boolean lock;
public Node(boolean lock){
this.lock = lock;
}
}
MCS锁
MCS锁:显式的链表表示多线程的顺序,自旋的判断条件是在本节点,而不是CLH锁自旋的判断条件是前一节点,这是两者最主要的区别。
MSC实现方式
package lock.review.lock;
import java.util.concurrent.atomic.AtomicReference;
public class MSCLock {
public AtomicReference<MCSNode> tail;//尾指针
public ThreadLocal<MCSNode> currentThreadLocal = new ThreadLocal<MCSNode>();
public void lock(){
MCSNode node = new MCSNode(false, null);//当前节点的初始状态:不可以获取锁
MCSNode preNode = tail.getAndSet(node);
//判断tail是否为空
if(preNode != null){
//非第一个线程
preNode.nextNode = node;//设置前尾节点指向node的nextNode为当前节点,建立显式链表。
currentThreadLocal.set(node);
while(node.lock);//自旋于本身节点
//获取锁
}else{
//第一个线程进入:什么都不用做,因为是按照自旋本地变量,因此不需要处理直接获取锁
}
}
public void unlock(){
MCSNode node = currentThreadLocal.get();
/***
*释放版本一:
node.nextNode.lock=true;
node.nextNode=null;//help gc
currentThreadLocal.remove();//help gc
上述代码存在部分问题:1 如果node是最后一个节点,那么nextNode就null,此时node.nextNode.lock=true就会报空指针异常,
因此需要加if判断
if(node.nextNode != null){
node.nextNode.lock=true;
node.nextNode=null;//help gc
currentThreadLocal.remove();//help gc
}
上述代码虽然解决了空指针的问题,但是还存在一个问题,如果node的nextNode为空,
那么就无法把node.nextNode.lock=true传递个下一个节点
*/
/****
* 释放版本二:
* 循环到当前节点的下一节点不为空,然后通知下一节点去获取锁,
* 这样才能保证锁的传递性,虽然解决了空指针问题,同时保证了锁的传递,
* 但是这样有个bug,因为这会导致当前线程死循环的等待下一个线程,
* 这个肯定是不合理的。因此可以考虑tail为null作为中转。
while(node.nextNode == null){
node.nextNode.lock=true;
node.nextNode=null;//help gc
}
*/
/**释放版本三:
*版本二存在当前释放线程一直空等待下一节点的到来,
*因此需要寻找另外一个方式,解决锁的传递。因为lock的时候,会特意判断tail是否为空,
*如果tail为空,说明是第一个线程,因此当node没有nextNode时,我们可以设置tail为null,
*这样就保证了锁的传递性。
* **/
if(node.nextNode == null){
//当前线程是最后一个线程,为了让锁传递下去,而当前线程也不用一直循环等待,设置tail为null
if(tail.compareAndSet(node, null)){
/**
* 设置tail为null,这样在lock的时候直接判断tail是否为空,
* 就可以把下一个线程处理为第一个进入的线程,这样就解决了锁的传递
*/
node.nextNode=null;
return;
}else{
/**
* 设置tail失败,已经有线程进入了,又为了防止进入的线程,还未设置释放锁线程node的nextNode=当前进入线程的node,
* 因此做一个while循环等待,这个时间还是比较小的,还可以等待
*/
while(node.nextNode != null){
node.nextNode.lock=true;
node.nextNode=null;
}
}
}
}
}
//节点
class MCSNode{
public volatile boolean lock;//true 可以获取锁,false 不可以获取锁,volatile 保证修改的立刻可见
/**当前节点的下一节点,通过nextNode进行显示的链表表示多线程的顺序,
* 同时当线程释放锁时,需要通过nextNode获取下一节点,修改其lock状态
*/
public MCSNode nextNode;
public MCSNode(boolean lock,MCSNode preNode){
this.lock = lock;
this.nextNode = preNode;
}
}
上述锁都是自旋锁,当线程获取时间片后,如果没有获取锁,就是自动的while循环进行自旋,直到获取锁,下面说一下线程没有获取锁后,进入等待的状态的锁。
CLH锁与MCS锁的区别
锁 | while循环条件 | 实现有序的方式 | 使用场景 |
---|---|---|---|
CLH锁 | 前一节点是否释放锁 | 隐式链表 | SMP:多处理器结构,多个cpu使用共同的内存和IO |
MCS锁 | 本身节点能否获取锁 | 显式链表 | NUMA:非一致存储访问,将CPU分为CPU模块,独立的内存和IO |
synchronized与自旋锁的区别
自旋锁线程的状态并不是按照正常的运行->等待->待运行这种状态,自旋锁无论是否获取锁,都是运行->待运行,简单说:自旋锁是通过代码进行控制。