票锁比较直白,可以称为号码锁,叫号锁。是一种自旋锁,之前讲过CLH锁(传送门)也是一种自旋锁。
TicketLock定义
ticket lock就像是平时使用美味不用等一样,自己先通过公众号排队取号,然后不断的会刷新(自旋)当前叫到了第几号,如果叫到自己的号,就说明可以获取锁去吃饭啦。
TicketLock锁流程
假设饭店只有一个位子
- 初始时位子是空的,叫号0
- 第一位顾客取号,取号0
- 匹配号成功,成功就餐
- 第二位顾客取号,取号1,当前叫号为0,不匹配,自旋
- 第一位顾客用餐结束,工作人员叫号1
- 第二位顾客取号1与叫号1匹配,进入就餐。
TicketLock实现
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author 会灰翔的灰机
* @date 2019/9/15
*/
public class TicketLock implements MyLock {
/**
* 叫号的号码(排队号)
*/
private final AtomicInteger SERVICE_NO = new AtomicInteger(-1);
/**
* 取号的号码(当前可以获取锁的号)
*/
private final AtomicInteger CURRENT_SERVICE_NO = new AtomicInteger(0);
private volatile Integer lockedNO = -1;
@Override
public void lock(){
// 1. 取号
int number = SERVICE_NO.incrementAndGet();
// 2. 当前叫号与取号不匹配进入自旋
while(number != CURRENT_SERVICE_NO.get()) {
System.out.println("spin:SERVICE_NO="+number+",currentServiceNo="+CURRENT_SERVICE_NO.get());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 3. 当前叫号与取号匹配获取锁成功
System.out.println("got lock...");
// 4. 记录当前获取到所资源的编号,解锁时需要匹配,不能随便释放其他人的锁
lockedNO = number;
}
@Override
public void unlock(){
// 4. 释放锁,递增叫号唤醒下一个等待锁的排号者
CURRENT_SERVICE_NO.compareAndSet(lockedNO, lockedNO+1);
System.out.println("release lock:SERVICE_NO="+SERVICE_NO.get()+",currentServiceNo="+CURRENT_SERVICE_NO.get());
}
public static void main(String[] args) {
TicketLock ticketLock = new TicketLock();
MyTask myTask = new MyTask(10000, ticketLock);
myTask.start();
MyTask myTask2 = new MyTask(1000, ticketLock);
myTask2.start();
}
}
TicketLock优点
- 公平性
- 无饥饿性
- 原子操作少,共两处原子操作,其实仅需要一次即可,第二次不需要读取,仅需要一个递增操作不需要读取数值(读取并递增的两个动作合并一个原子操作的开销)
TicketLock缺点
- 扩展性差。如果等待者很多,在释放锁时,递增(餐厅服务人员叫号:请100号客人用餐)锁号时,会将其他所有等待线程中的(叫号)锁号缓存cacheline置为无效invalid。会通过内存主线重新加载新的值到缓存cacheline。这样会造成“拥堵”。因为叫号更新了,我手里的号码需要跟最新的叫号号码比较是否相等,如果相等说明到我用餐了