前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于自旋锁的公平和非公平模式

关于自旋锁的公平和非公平模式

作者头像
我是攻城师
发布2018-08-16 14:35:28
4030
发布2018-08-16 14:35:28
举报
文章被收录于专栏:我是攻城师我是攻城师

自旋锁是并发编程实战里面一个关于锁优化的非常重要的一个概念,通常情况下会配合CAS原语来实现轻量级的同步操作。

自旋锁一般用于线程竞争不激烈,临界区代码非常少,并且执行操作非常快的场景下。本质上是为了减少线程调度的上下文切换时间。所以在访问临界区资源失败的情况下并不会立即进入BLOCK状态,而通常是会再循环一定的cpu周期或时间直到该线程可以获得锁条件,

自旋的目的就是为了减少上下文线程调度的切换时间,从而会空转几个cpu周期,如果在此时间内,在此获取了锁便可以直接运行,从而避免上下文频繁调度。

自旋锁的缺点:

(1)如果线程竞争激烈会导致一些自旋cpu周期过长,从而浪费了大量的cpu资源

(2)自旋锁本身是抢占式的加锁,从而可能导致有些线程会出现饥饿现象,也就是所谓的不公示特证。

非公司自旋锁的示例如下:

代码语言:javascript
复制
package concurrent.lock_compare;

import java.util.concurrent.atomic.AtomicReference;

/**
 * Created by Administrator on 2018/8/2.
 */
public class SpinLock {

    private AtomicReference<Thread> owner=new AtomicReference<>();


    private void lock() throws InterruptedException {


          Thread expectValue=null;
          Thread   updateValue;

        do {
             updateValue=Thread.currentThread();
            System.out.println(updateValue.getName()+" 自旋等待中.....  ");
            Thread.sleep(1000);

            //只有第一个执行的线程,才会加锁成功,其他一直处于自旋等待中
        }while (!owner.compareAndSet(expectValue,updateValue));

        System.out.println(updateValue.getName()+" 加锁成功...... 4秒后释放锁 ");

        // do work

        Thread.sleep(4000);
        System.out.println(updateValue.getName()+" 释放锁了。。。。。 ");
        unlock();

    }


    public void unlock(){


        Thread expectValue=Thread.currentThread();

        owner.compareAndSet(expectValue,null);


    }




    public static void main(String[] args) {

        SpinLock spinLock=new SpinLock();


        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                try {
                    spinLock.lock();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };


        Thread t1=new Thread(runnable);
        Thread t2=new Thread(runnable);
        Thread t3=new Thread(runnable);


        t1.start();
        t2.start();
        t3.start();


    }



}

输出结果:

代码语言:javascript
复制
Thread-0 自旋等待中.....  
Thread-2 自旋等待中.....  
Thread-1 自旋等待中.....  
Thread-2 自旋等待中.....  
Thread-1 自旋等待中.....  
Thread-0 加锁成功...... 4秒后释放锁 
Thread-2 自旋等待中.....  
Thread-1 自旋等待中.....  
Thread-2 自旋等待中.....  
Thread-1 自旋等待中.....  
Thread-1 自旋等待中.....  
Thread-2 自旋等待中.....  
Thread-0 释放锁了。。。。。 
Thread-1 加锁成功...... 4秒后释放锁 
Thread-2 自旋等待中.....  
Thread-2 自旋等待中.....  
Thread-2 自旋等待中.....  
Thread-2 自旋等待中.....  
Thread-1 释放锁了。。。。。 
Thread-2 加锁成功...... 4秒后释放锁 
Thread-2 释放锁了。。。。。

实现公平的自旋锁:

实现一个公平的自旋锁,其实也比较容易,我们只需要按照线程的程序,构建一个FIFO先进先出的阻塞队列,便可以完成这件事。

一个生活中的例子是:我们去银行办业务,到了之后通常会先取一个号,然后坐等柜台叫号或者自己主动去看大屏幕上的号是否到我们了,当柜台每次处理完一个号,下次叫的号都是上次叫的号的+1,所以取票的顺序就是我们公平处理的顺序,按照这个思路我们来看下实现公平自旋的代码:

代码语言:javascript
复制
package concurrent.lock_compare;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by qindongliang on 2018/8/2.
 */
public class TicketLock {

     //当前正在服务的号码
    private AtomicInteger serviceNum=new AtomicInteger();

    //正在排队的号码

    private AtomicInteger ticketNum=new AtomicInteger();


    private void lock() throws InterruptedException {

        int getTicketNum=ticketNum.getAndIncrement();


        do{

            System.out.println(Thread.currentThread().getName()+" 开始自旋等待..... ticketNum="+getTicketNum);

            Thread.sleep(1000);


        }while (getTicketNum!=serviceNum.get());


        System.out.println(Thread.currentThread().getName()+" 使用号"+getTicketNum+" 完毕。");
        unlock();

    }

    private void unlock(){

        //开始叫下一位的号
       int nextServiceNum= 1 + serviceNum.get();


        serviceNum.compareAndSet(serviceNum.get(),nextServiceNum);


    }



    public static void main(String[] args) {

        TicketLock ticketLock=new TicketLock();

        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                try {
                    ticketLock.lock();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };


        Thread t1=new Thread(runnable);
        Thread t2=new Thread(runnable);
        Thread t3=new Thread(runnable);
        t1.start();
        t2.start();
        t3.start();



    }
}

输出如下:

代码语言:javascript
复制
Thread-0 开始自旋等待..... ticketNum=0
Thread-2 开始自旋等待..... ticketNum=2
Thread-1 开始自旋等待..... ticketNum=1
Thread-1 开始自旋等待..... ticketNum=1
Thread-2 开始自旋等待..... ticketNum=2
Thread-0 使用号0 完毕。
Thread-2 开始自旋等待..... ticketNum=2
Thread-1 使用号1 完毕。
Thread-2 使用号2 完毕。

从上面的结果我们能看出,服务号从0开始,依次处理,只有上一个服务号处理完毕,才会进行下一个服务号办理。从而就实现了公平的自旋锁模式。

公平的自旋锁能够确保不会出现线程饥饿现象,但公平模式不一定就意味着效率很高,具体跟临界区的代码执行的时长有关,如果临界区是一块很大的逻辑,那么就会导致其它自旋线程耗费大量的cpu资源。

总结:

本文主要了介绍了Java里面自旋锁的公平模式和非公平的实现,并介绍了其相关的优缺点,自旋锁通常搭配CAS来一起工作,自旋锁的临界区代码不能太多,而且耗时要尽可能的短,否则一旦自旋的代价超过线程睡眠唤醒调度的代价,那么将会大大浪费cpu资源。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-08-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 我是攻城师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档