java多线程:5.3 自旋锁

自旋锁

自旋锁,也是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与自旋锁的区别

自旋锁线程的状态并不是按照正常的运行->等待->待运行这种状态,自旋锁无论是否获取锁,都是运行->待运行,简单说:自旋锁是通过代码进行控制。

猜你喜欢

转载自blog.csdn.net/u010652576/article/details/84723574