5-理解自旋锁,重入锁

重入锁

重入锁,也就是锁重入,什么意思呢?之前我们用到的synchronized就是一个重入锁。那么,什么是重入锁呢?

先说非重入锁,我们知道,当多个线程来访问一个方法的时候,比如说这个方法上已经加了一个synchronized,多个线程来进行访问的时候,那么,显然,当一个线程拿到我们的锁之后,那么,其他的线程就需要等待竞争锁,那么,当第一个线程执行完毕释放了这个锁之后,那么,其他的线程才能够再进来,那么这就是说锁是不能够让所有的线程都进来的,只能有一个线程进来,然后其他的线程都在外面等着。

那么,锁重入是什么意思呢?之所以能把其他的线程挡在外面,就是因为当一个线程拿到了这个锁之后,其他的线程就不再能够拿到这个锁了。两个方法,这两个方法都是使用同一个对象去锁的都有synchronized,比如说我们在同一个实例里面都用的synchronized,a、b两个方法加上了这把锁,那么,第一个线程在a方法中,调用另一个b方法,那么,也就是说第一个线程进来之后,它拿到了synchronized的这把锁,然后,接着,第一个a方法在它自己里面调用第二个b方法的时候,它就可以直接的来访问这个方法,而不是说,这个对象的锁已经被这个线程拿到了,它就就不能再拿去调用第二个。这里不会发生死锁现象。而是,也可以来进行访问这个方法的,这就是所谓的锁的重入。我们见到的synchronized、包括我们以后要讲的lock,等等,都是一个重入锁,这里说锁的重入和重入锁,那么,能够让线程重入的锁,就称之为重入锁,这个过程叫做锁的重入。

  • 我们来举个例子看一下什么情况下会出现锁的重入呢?为什么会引入锁重入这么一个概念呢?

为了演示锁的重入,我们给这两个方法都加上synchronized,我们刚才也说到了,synchronized就是一个重入锁,我们知道,把synchronized加到方法上,那么,它所锁的对象就是当前类的实例。然后,我们在a()方法中去调用b()方法。

按照我们当前的逻辑来判断,这两个synchronized锁的都是当前类的实例,那么,当第一个线程进来之后拿到了这把锁,拿到了当前Demo这个类的实例所加的这把锁。那么,在它没有释放的时候,那么当前Demo这个类的实例所加的这把锁已经被这个线程拿到了,那么,这个线程还能再进入到另外一个被当前Demo这个类的实例所加的这把锁的一个方法吗?显然是不会的,因为锁已经被拿到了,所以,这样如果一个线程进来访问的话,那么,很容易就产生了一个死锁,那么死锁的概念我们后面会说,但是并不是像我们所想的这样,它并不会出现死锁的问题,而是,这个b()方法是可以进行执行的,那么,我们来写一个例子来看一下

我们发现很快就执行出来结果了。这就是关于锁的重入。它并不会出现死锁的问题,而是,这个b()方法是可以进行执行的。

 

自旋锁

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成 busy-waiting。

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。

Java中有那么一些类,是以Atomic开头的。这一系列的类我们称之为原子操作类。

AtomicReference:提供了引用变量的读写原子性操作,如果使用普通的对象引用,在多线程情况下进行对象的更新可能会导致不一致性。如果使用原子性对象引用,在多线程情况下进行对象的更新可以确保一致性。

AtomicInteger:它相当于一个int变量,我们执行Int的 i++ 的时候并不是一个原子操作。而使用AtomicInteger的incrementAndGet却能保证原子操作。

compareAndSet方法:如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。这里需要注意的是这个方法的返回值实际上是是否成功修改,而与之前的值无关。

 

  • 如何实现自旋锁?简单的例子

实现一个锁工具类

public class SpinLock {
    private AtomicReference<Thread> cas = new AtomicReference<Thread>();
    public void lock() {
        Thread current = Thread.currentThread();
        // 利用CAS
        while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }
    }
    public void unlock() {
        Thread current = Thread.currentThread();
        cas.compareAndSet(current, null);
    }
}

ock()方法利用的CAS(原子操作),当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。

  • 自旋锁的优点

  1. 自旋锁不会使线程状态发生切换,一直处于就绪状态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
  • 自旋锁存在的缺点

  1. 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
  2. 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题
  • 自旋锁的其他变种(解决线程饥饿问题)

TicketLock主要解决的是公平性的问题。

思路:每当有线程获取锁的时候,就给该线程分配一个递增的id,我们称之为排队号,同时,锁对应一个服务号,每当有线程释放锁,服务号就会递增,此时如果服务号与某个线程排队号一致,那么该线程就获得锁,由于排队号是递增的,所以就保证了最先请求获取锁的线程可以最先获取到锁,就实现了公平性。

可以想象成银行办理业务排队,排队的每一个顾客都代表一个需要请求锁的线程,而银行服务窗口表示锁,每当有窗口服务完成就把自己的服务号加一,此时在排队的所有顾客中,只有自己的排队号与服务号一致的才可以得到服务。

实现代码:

public class TicketLockV2 {
    /**
     * 服务号
     */
    private AtomicInteger serviceNum = new AtomicInteger(1);
    /**
     * 排队号
     */
    private AtomicInteger ticketNum = new AtomicInteger();
    /**
     * 新增一个ThreadLocal,用于存储每个线程的排队号
     */
    private ThreadLocal<Integer> ticketNumHolder = new ThreadLocal<Integer>();
    public void lock() {
        int currentTicketNum = ticketNum.incrementAndGet();
        // 获取锁的时候,将当前线程的排队号保存起来
        ticketNumHolder.set(currentTicketNum);
        while (currentTicketNum != serviceNum.get()) {
            // Do nothing
        }
    }
    public void unlock() {
        // 释放锁,从ThreadLocal中获取当前线程的排队号
        Integer currentTickNum = ticketNumHolder.get();
        serviceNum.compareAndSet(currentTickNum, currentTickNum + 1);
    }
}

上面的实现方式是,线程获取锁之后,将每个线程的排队号放到了ThreadLocal中。等该线程释放锁的时候,从ThreadLocal中获取当前线程的排队号进行释放。

TicketLock存在的问题:

多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量serviceNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。

所以可以排队号和服务号的实现,可以采用其他方式,比如结合redis,使用redis的原子性操作,这里不详细说。

 

自旋锁与互斥锁

  • 自旋锁与互斥锁都是为了实现保护资源共享的机制。
  • 无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者。
  • 获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值