自旋锁的定义
自旋锁指的是当线程获取不到资源时,不是进入阻塞状态,而是让当前的线程不停地执行空循环,直到循环条件被其他线程改变,进入临界区。
- 具体实现如下:
public class SpinLock {
private AtomicReference<Thread> sign = new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null,current)){
}
}
public void unlock(){
Thread cur = Thread.currentThread();
sign.compareAndSet(cur,null);
}
}
在这里使用了CAS原子操作,lock函数将sign设置为当前线程,并且预测原来的值为空。unlock函数将sign设置为null,并且预测值为当前线程。
当有第二个线程调用lock操作时,由于sign值不为空,导致循环一直被执行,直到第一个线程调用unlock操作将sign设置为null,第二个线程才能进入临界区。
- 进行测试:
public static void main(String[] args){
SpinLock lock = new SpinLock();
for (int i = 0; i < 2; i ++){
new Thread("Thread-" + i){
@Override
public void run() {
AddClass ac = new AddClass(lock);
//进行100次累加
for (int j = 0; j < 100; j ++)
try {
ac.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
static class AddClass{
//设置为静态变量,表示为临界区
private static int i;
private SpinLock lock;
public AddClass(SpinLock lock){
this.lock = lock;
}
//无锁自增
public void add() throws InterruptedException {
i ++;
System.out.println(Thread.currentThread().getName()+": i = " + i);
//睡眠一段时间,保证线程1,2之间可以相互切换
Thread.sleep(200);
}
//有锁自增
public void addWithLock() throws InterruptedException {
lock.lock();
i ++;
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+": i = " + i);
lock.unlock();
}
}
输出结果:
add: Thread-1: i = 176
addWithlock: Thread-1: i = 200
可见,自旋锁可以保证临界区中的互斥访问。
自旋锁的评价
-优点:由于自旋锁不改变线程的状态,所以线程的运行会比较快。
- 缺点:当线程数比较多时,会有较多的线程占用CPU空跑,影响其他线程获取CPU资源,导致性能下降。
- 使用场景:锁竞争不激烈且占用锁的时间较短的场景,如jdk中的Atomic类。
自旋锁的其中种类
- TicketLock:主要解决访问顺序的问题
public class TicketSpinLock {
//类比票号
private AtomicInteger ticketNum = new AtomicInteger(0);
//类比显示器上的号码
private AtomicInteger owner = new AtomicInteger(0);
private static final ThreadLocal<Integer> mTicketLocal = new ThreadLocal<>();
public void lock(){
//注意这里获取到的数为加一前的数
int mTicket = ticketNum.getAndIncrement();
mTicketLocal.set(mTicket);
while (mTicket != owner.get()){
}
}
public void unlock(){
int mTicket = mTicketLocal.get();
owner.compareAndSet(mTicket,mTicket + 1);
}
}
上述过程类比我们去银行办业务,在去取票机上取票后再排队等候,每个人都是一个线程,而票号就是ticketNum,柜台显示器显示的号码就是owner,当票号和显示器上的号码一致时,才可以去办理业务。而mTicketLocal在这里的作用是保存票号。
- CLHLock:确保无饥饿性,提供先到先服务的公平性。
public class CLHLock {
//保存最后一个申请lock的线程的mNode域
AtomicReference<QNode> tail = new AtomicReference<>();
//指向其前驱
ThreadLocal<QNode> mPred;
//保存当前的状态
ThreadLocal<QNode> mNode;
public static class QNode{
public volatile boolean locked = false;
}
public CLHLock(){
mNode = new ThreadLocal<QNode>(){
@Override
protected QNode initialValue() {
return new QNode();
}
};
mPred = new ThreadLocal<QNode>();
}
public void lock(){
QNode qnode = mNode.get();
qnode.locked = true;
QNode pred = tail.getAndSet(qnode);
mPred.set(pred);
while (pred.locked){
}
}
public void unlock(){
QNode qNode = mNode.get();
qNode.locked = false;
mNode.set(mPred.get());
}
}
锁本身表示成QNode的虚拟链表。共享的tail保存申请的最后的一个申请lock的线程的mNode域,每个申请lock的线程通过一个本地变量pred指向它的前驱。另一个本地线程变量mNode的locked保存本身的状态(true:正在申请锁或已申请到锁;false:已释放锁)。
每个线程通过监视自身的前驱状态,自旋等待锁被释放。释放锁时,把自身的mNode的locked设为false,使后续线程获得锁,并回收前驱的QNode,等待下一次使用。
当一个线程申请锁时:
a. 首先会实例化一个QNode节点,并将其设置为自己的本地线程变量myNode;
b. 然后将myNode的locked域设置为true,表明该线程正在等待获取锁;
c. 接着通过AtomicReference的getAndSet()方法,将myNode设置为队列的最后一个节点,并返回其前驱节点;
d. 最后该线程一直在其前驱节点的locked域自旋,直到locked域变为false,即前驱节点释放了锁。注意,对于SMP架构,由于是在cache中自旋,所以是高效的;
当一个线程释放锁时:
a. 将myNode的locked域设置为false,使得后继线程停止自旋以便获取到锁;
b. 然后重用前驱节点,将前驱节点设置为myNode,以便下一次该线程获取锁时使用。之所以能重用,是因为此时其前驱线程不会再使用该前驱节点了。而该线程原来的myNode,可能被其后继线程或tail引用。对于java语言来说,重用实际上是没有必要的,因为如果不重用,则前驱节点是可以被jvm回收的,从而降低内存的使用。

- MCS锁
同CLH锁一样,每个申请锁的线程通过一个QNode对象表示,QNode中有一个volatile boolean类型的成员变量locked,locked为true,则表示对应的线程已经获取到了锁或者正在等待锁;locked为false,则表示对应的线程已经释放了锁。
锁则由多个QNode节点组成链表表示,与CLH锁不同的是,MCS中的锁不是虚拟链表,而是实际链表,每个QNode节点都有一个next域指向后继结点。
public class MCSLock {
public class QNode{
public volatile boolean locked = false;
public QNode next = null;
}
private AtomicReference<QNode> tail;
private ThreadLocal<QNode> myNode;
public MCSLock(){
tail = new AtomicReference<>(null);
myNode = new ThreadLocal<QNode>(){
@Override
protected QNode initialValue() {
return new QNode();
}
};
}
public void lock(){
QNode qnode = myNode.get();
QNode pred = tail.getAndSet(qnode);
if (pred != null){
qnode.locked = true;
pred.next = qnode;
}
while (qnode.locked){
}
}
public void unlock(){
QNode qnode = myNode.get();
if (qnode.next == null){
if (tail.compareAndSet(qnode,null))
return;
while (qnode.next == null){
}
}
qnode.next.locked = false;
qnode.next = null;
}
}
当一个线程申请锁时:
a. 首先会实例化一个QNode节点,并将其设置为自己的本地线程变量myNode;
b. 然后通过调用AtomicReference的getAndset()方法,将myNode设置为队列的最后一个节点,并返回其前驱节点。
c. 接着判断前面是否已经有其他线程加入到锁的等待队列中,即前驱节点是否为空。如果前驱节点为空,则说明自己是第一个加入到锁的等待队列中的线程,执行自旋,由于locked域初始值为false,所以能立即退出自旋,获取到锁;
d. 如果前驱节点非空,则首先将myNode的locked域设置为true,表明自己正在等待获取锁;同时将前驱结点的next域指向myNode;
e. 最后该线程一直在自己节点的locked域自旋,直到locked域变为false,即前驱节点释放了锁。
当一个线程释放锁时,会处于以下三种情况之一中:
a. 此刻没有其它线程正在申请锁,即当前节点的next域指向null,且tail指向当前节点:此种情况通过调用AtomicReference的compareAndSet()方法,将tail指向null;然后直接返回。
b. 此刻有其他线程正在申请锁,而且已更新tail域,但还未将前驱节点的next域指向它。即当前节点的next域指向null,且tail不是指向当前节点:此种情况则一直等待其他线程设置前驱节点的next域后,才将后继节点的locked域设置为false,以便后继节点退出自旋,从而获取到释放的锁。
c. 此刻有其他线程正在申请锁,而且已更新tail域,而且已将前驱节点的next域指向它。即当前节点的next域非空:此种情况则直接将后继节点的locked域设置为false,以便后继节点退出自旋,从而获取到释放的锁。

网友评论