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

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

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

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

自旋锁的缺点:

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

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

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

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();


    }



}

输出结果:

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,所以取票的顺序就是我们公平处理的顺序,按照这个思路我们来看下实现公平自旋的代码:

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();



    }
}

输出如下:

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资源。

原文发布于微信公众号 - 我是攻城师(woshigcs)

原文发表时间:2018-08-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏用户2442861的专栏

Python日志输出——logging模块

http://blog.csdn.net/chosen0ne/article/details/7319306

18110
来自专栏C/C++基础

CMake简介及使用实例

CMake是一个跨平台的建构系统的工具,可以用简单的语句来描述所有平台的安装(编译过程)。他能够输出各种各样的构建文档makefile或者project文件,描...

20920
来自专栏Linyb极客之路

Spring Cloud开发注意事项

如果provider中需要引入其他feign client的接口,需在 provider的启动类添加注解 @EnableFeignClients(basePac...

46130
来自专栏JavaEE

Thymeleaf的使用前言:一、thymeleaf简介:二、thymeleaf标准方言:三、thymeleaf与springboot集成案例:总结:

最近听说thymeleaf好像也挺流行的,还说是spring官方推荐使用,那thymeleaf究竟是什么呢?spring为什么推荐用它呢?怎么用呢?本文将为你揭...

15020
来自专栏IT杂记

Slf4j+Logback配置文件变量使用小记

项目中须要根据不同的模块,产生出不同的日志文件名,使用的是同一logback.xml配置文件,这里简单调研,说明两种实现方式,以及两种实现方式的区别。 测试准备...

33480
来自专栏柠檬先生

SpringMVC——笔记

使用 @RequestMapping 映射请求 Spring MVC 使用@RequestMapping 注解为控制器指定可以处理那些URL请求。   在控制器...

24550
来自专栏斑斓

Spray中的Authentication和JMeter测试

Spray Authentication 在Spray中,如果需要对REST API添加认证,可以使用Spray提供的Authenticate功能。本质上,Au...

38790
来自专栏帅小子的日常

使用redis做缓存

83370
来自专栏aoho求索

Spring Cloud 覆写远端的配置属性

覆写远端的配置属性 应用的配置源通常都是远端的Config Server服务器,默认情况下,本地的配置优先级低于远端配置仓库。如果想实现本地应用的系统变量和c...

41090
来自专栏pangguoming

Spring Boot Maven Plugin打包异常及三种解决方法:Unable to find main class

68320

扫码关注云+社区

领取腾讯云代金券