Java实现手写自旋锁的示例代码

目录
  • 前言
  • 自旋锁
    • 原子性
    • 自己动手写自旋锁
    • 自己动手写可重入自旋锁
  • 总结

前言

我们在写并发程序的时候,一个非常常见的需求就是保证在某一个时刻只有一个线程执行某段代码,像这种代码叫做临界区,而通常保证一个时刻只有一个线程执行临界区的代码的方法就是锁。在本篇文章当中我们将会仔细分析和学习自旋锁,所谓自旋锁就是通过while循环实现的,让拿到锁的线程进入临界区执行代码,让没有拿到锁的线程一直进行while死循环,这其实就是线程自己“旋”在while循环了,因而这种锁就叫做自旋锁。

自旋锁

原子性

在谈自旋锁之前就不得不谈原子性了。所谓原子性简单说来就是一个一个操作要么不做要么全做,全做的意思就是在操作的过程当中不能够被中断,比如说对变量data进行加一操作,有以下三个步骤:

  • 将data从内存加载到寄存器。
  • 将data这个值加一。
  • 将得到的结果写回内存。

原子性就表示一个线程在进行加一操作的时候,不能够被其他线程中断,只有这个线程执行完这三个过程的时候其他线程才能够操作数据data。

我们现在用代码体验一下,在Java当中我们可以使用AtomicInteger进行对整型数据的原子操作:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {

  public static void main(String[] args) throws InterruptedException {
    AtomicInteger data = new AtomicInteger();
    data.set(0); // 将数据初始化位0
    Thread t1 = new Thread(() -> {
      for (int i = 0; i < 100000; i++) {
        data.addAndGet(1); // 对数据 data 进行原子加1操作
      }
    });
    Thread t2 = new Thread(() -> {
      for (int i = 0; i < 100000; i++) {
        data.addAndGet(1);// 对数据 data 进行原子加1操作
      }
    });
    // 启动两个线程
    t1.start();
    t2.start();
    // 等待两个线程执行完成
    t1.join();
    t2.join();
    // 打印最终的结果
    System.out.println(data); // 200000
  }
}

从上面的代码分析可以知道,如果是一般的整型变量如果两个线程同时进行操作的时候,最终的结果是会小于200000。

我们现在来模拟一下一般的整型变量出现问题的过程:

主内存data的初始值等于0,两个线程得到的data初始值都等于0。

现在线程一将data加一,然后线程一将data的值同步回主内存,整个内存的数据变化如下:

现在线程二data加一,然后将data的值同步回主内存(将原来主内存的值覆盖掉了):

我们本来希望data的值在经过上面的变化之后变成2,但是线程二覆盖了我们的值,因此在多线程情况下,会使得我们最终的结果变小。

但是在上面的程序当中我们最终的输出结果是等于20000的,这是因为给data进行+1的操作是原子的不可分的,在操作的过程当中其他线程是不能对data进行操作的。这就是原子性带来的优势。

自己动手写自旋锁

AtomicInteger类

现在我们已经了解了原子性的作用了,我们现在来了解AtomicInteger类的另外一个原子性的操作——compareAndSet,这个操作叫做比较并交换(CAS),他具有原子性。

public static void main(String[] args) {
  AtomicInteger atomicInteger = new AtomicInteger();
  atomicInteger.set(0);
  atomicInteger.compareAndSet(0, 1);
}

compareAndSet函数的意义:首先会比较第一个参数(对应上面的代码就是0)和atomicInteger的值,如果相等则进行交换,也就是将atomicInteger的值设置为第二个参数(对应上面的代码就是1),如果这些操作成功,那么compareAndSet函数就返回true,如果操作失败则返回false,操作失败可能是因为第一个参数的值(期望值)和atomicInteger不相等,如果相等也可能因为在更改atomicInteger的值的时候失败(因为可能有多个线程在操作,因为原子性的存在,只能有一个线程操作成功)。

自旋锁实现原理

我们可以使用AtomicInteger类实现自旋锁,我们可以用0这个值表示未上锁,1这个值表示已经上锁了。

AtomicInteger类的初始值为0。

在上锁时,我们可以使用代码atomicInteger.compareAndSet(0, 1)进行实现,我们在前面已经提到了只能够有一个线程完成这个操作,也就是说只能有一个线程调用这行代码然后返回true其余线程都返回false,这些返回false的线程不能够进入临界区,因此我们需要这些线程停在atomicInteger.compareAndSet(0, 1)这行代码不能够往下执行,我们可以使用while循环让这些线程一直停在这里while (!value.compareAndSet(0, 1));,只有返回true的线程才能够跳出循环,其余线程都会一直在这里循环,我们称这种行为叫做自旋,这种锁因而也被叫做自旋锁。

线程在出临界区的时候需要重新将锁的状态调整为未上锁的上状态,我们使用代码value.compareAndSet(1, 0);就可以实现,将锁的状态还原为未上锁的状态,这样其他的自旋的线程就可以拿到锁,然后进入临界区了。

自旋锁代码实现

import java.util.concurrent.atomic.AtomicInteger;

public class SpinLock {

  // 0 表示未上锁状态
  // 1 表示上锁状态
  protected AtomicInteger value;

  public SpinLock() {
    this.value = new AtomicInteger();
    // 设置 value 的初始值为0 表示未上锁的状态
    this.value.set(0);
  }

  public void lock() {
    // 进行自旋操作
    while (!value.compareAndSet(0, 1));
  }

  public void unlock() {
    // 将锁的状态设置为未上锁状态
    value.compareAndSet(1, 0);
  }

}

上面就是我们自己实现的自旋锁的代码,这看起来实在太简单了,但是它确实帮助我们实现了一个锁,而且能够在真实场景进行使用的,我们现在用代码对上面我们写的锁进行测试。

测试程序:

public class SpinLockTest {

  public static int data;
  public static SpinLock lock = new SpinLock();

  public static void add() {
    for (int i = 0; i < 100000; i++) {
      // 上锁 只能有一个线程执行 data++ 操作 其余线程都只能进行while循环
      lock.lock();
      data++;
      lock.unlock();
    }
  }

  public static void main(String[] args) throws InterruptedException {
    Thread[] threads = new Thread[100];
    // 设置100个线程
    for (int i = 0; i < 100; i ++) {
      threads[i] = new Thread(SpinLockTest::add);
    }
    // 启动一百个线程
    for (int i = 0; i < 100; i++) {
      threads[i].start();
    }
    // 等待这100个线程执行完成
    for (int i = 0; i < 100; i++) {
      threads[i].join();
    }
    System.out.println(data); // 10000000
  }
}

在上面的代码单中,我们使用100个线程,然后每个线程循环执行100000data++操作,上面的代码最后输出的结果是10000000,和我们期待的结果是相等的,这就说明我们实现的自旋锁是正确的。

自己动手写可重入自旋锁

可重入自旋锁

在上面实现的自旋锁当中已经可以满足一些我们的基本需求了,就是一个时刻只能够有一个线程执行临界区的代码。但是上面的的代码并不能够满足重入的需求,也就是说上面写的自旋锁并不是一个可重入的自旋锁,事实上在上面实现的自旋锁当中重入的话就会产生死锁。

我们通过一份代码来模拟上面重入产生死锁的情况:

public static void add(int state) throws InterruptedException {
  TimeUnit.SECONDS.sleep(1);
  if (state <= 3) {
    lock.lock();
    System.out.println(Thread.currentThread().getName() + "\t进入临界区 state = " + state);
    for (int i = 0; i < 10; i++)
      data++;
    add(state + 1); // 进行递归重入 重入之前锁状态已经是1了 因为这个线程进入了临界区
    lock.unlock();
  }
}

在上面的代码当中加入我们传入的参数state的值为1,那么在线程执行for循环之后再次递归调用add函数的话,那么state的值就变成了2。

if条件仍然满足,这个线程也需要重新获得锁,但是此时锁的状态是1,这个线程已经获得过一次锁了,但是自旋锁期待的锁的状态是0,因为只有这样他才能够再次获得锁,进入临界区,但是现在锁的状态是1,也就是说虽然这个线程获得过一次锁,但是它也会一直进行while循环而且永远都出不来了,这样就形成了死锁了。

可重入自旋锁思想

针对上面这种情况我们需要实现一个可重入的自旋锁,我们的思想大致如下:

  • 在我们实现的自旋锁当中,我们可以增加两个变量,owner一个用于存当前拥有锁的线程,count一个记录当前线程进入锁的次数。
  • 如果线程获得锁,owner = Thread.currentThread()并且count = 1。
  • 当线程下次再想获取锁的时候,首先先看owner是不是指向自己,则一直进行循环操作,如果是则直接进行count++操作,然后就可以进入临界区了。
  • 我们在出临界区的时候,如果count大于一的话,说明这个线程重入了这把锁,因此不能够直接将锁设置为0也就是未上锁的状态,这种情况直接进行count--操作,如果count等于1的话,说明线程当前的状态不是重入状态(可能是重入之后递归返回了),因此在出临界区之前需要将锁的状态设置为0,也就是没上锁的状态,好让其他线程能够获取锁。

可重入锁代码实现

实现的可重入锁代码如下:

public class ReentrantSpinLock extends SpinLock {

  private Thread owner;
  private int count;

  @Override
  public void lock() {
    if (owner == null || owner != Thread.currentThread()) {
      while (!value.compareAndSet(0, 1));
      owner = Thread.currentThread();
      count = 1;
    }else {
      count++;
    }

  }

  @Override
  public void unlock() {
    if (count == 1) {
      count = 0;
      value.compareAndSet(1, 0);
    }else
      count--;
  }
}

下面我们通过一个递归程序去验证我们写的可重入的自旋锁是否能够成功工作。

测试程序:

import java.util.concurrent.TimeUnit;

public class ReentrantSpinLockTest {

  public static int data;
  public static ReentrantSpinLock lock = new ReentrantSpinLock();

  public static void add(int state) throws InterruptedException {
    TimeUnit.SECONDS.sleep(1);
    if (state <= 3) {
      lock.lock();
      System.out.println(Thread.currentThread().getName() + "\t进入临界区 state = " + state);
      for (int i = 0; i < 10; i++)
        data++;
      add(state + 1);
      lock.unlock();
    }
  }

  public static void main(String[] args) throws InterruptedException {
    Thread[] threads = new Thread[10];
    for (int i = 0; i < 10; i++) {
      threads[i] = new Thread(new Thread(() -> {
        try {
          ReentrantSpinLockTest.add(1);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }, String.valueOf(i)));
    }
    for (int i = 0; i < 10; i++) {
      threads[i].start();
    }
    for (int i = 0; i < 10; i++) {
      threads[i].join();
    }
    System.out.println(data);
  }
}

上面程序的输出:

Thread-3    进入临界区 state = 1
Thread-3    进入临界区 state = 2
Thread-3    进入临界区 state = 3
Thread-0    进入临界区 state = 1
Thread-0    进入临界区 state = 2
Thread-0    进入临界区 state = 3
Thread-9    进入临界区 state = 1
Thread-9    进入临界区 state = 2
Thread-9    进入临界区 state = 3
Thread-4    进入临界区 state = 1
Thread-4    进入临界区 state = 2
Thread-4    进入临界区 state = 3
Thread-7    进入临界区 state = 1
Thread-7    进入临界区 state = 2
Thread-7    进入临界区 state = 3
Thread-8    进入临界区 state = 1
Thread-8    进入临界区 state = 2
Thread-8    进入临界区 state = 3
Thread-5    进入临界区 state = 1
Thread-5    进入临界区 state = 2
Thread-5    进入临界区 state = 3
Thread-2    进入临界区 state = 1
Thread-2    进入临界区 state = 2
Thread-2    进入临界区 state = 3
Thread-6    进入临界区 state = 1
Thread-6    进入临界区 state = 2
Thread-6    进入临界区 state = 3
Thread-1    进入临界区 state = 1
Thread-1    进入临界区 state = 2
Thread-1    进入临界区 state = 3
300

从上面的输出结果我们就可以知道,当一个线程能够获取锁的时候他能够进行重入,而且最终输出的结果也是正确的,因此验证了我们写了可重入自旋锁是有效的!

总结

在本篇文章当中主要给大家介绍了自旋锁和可重入自旋锁的原理,并且实现了一遍,其实代码还是比较简单关键需要大家将这其中的逻辑理清楚:

所谓自旋锁就是通过while循环实现的,让拿到锁的线程进入临界区执行代码,让没有拿到锁的线程一直进行while死循环。

可重入的含义就是一个线程已经竞争到了一个锁,在竞争到这个锁之后又一次有重入临界区代码的需求,如果能够保证这个线程能够重新进入临界区,这就叫可重入。

我们在实现自旋锁的时候使用的是AtomicInteger类,并且我们使用0和1这两个数值用于表示无锁和锁被占用两个状态,在获取锁的时候使用while循环不断进行CAS操作,直到操作成功返回true,在释放锁的时候使用CAS将锁的状态从1变成0。

实现可重入锁最重要的一点就是需要记录是那个线程获得了锁,同时还需要记录获取了几次锁,因为我们在解锁的时候需要进行判断,之后count = 1的情况才能将锁的状态从1设置成0。

到此这篇关于Java实现手写自旋锁的示例代码的文章就介绍到这了,更多相关Java自旋锁内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 实例讲解Java 自旋锁

    一直以来不是怎么清楚自旋锁,最近有点时间,好好的学习了一下: 所谓的自旋锁在我的理解就是多个线程在尝试获取锁的时候,其中一个线程获取锁之后,其他的线程都处在一直尝试获取锁的状态,不会阻塞!!!那么什么叫做一直尝试获取锁呢?就是一个循环,比较经典的是AtomicInteger中的一个updateAndGet方法,下图所示(当然也可以直接看unsafe类中的getAndAddInt等类似方法): 我们可以看出在while循环中使用CAS去尝试更新一个变量,如果更新失败,就会一直在这个循环中一直在尝试

  • 一篇文章轻松搞懂Java中的自旋锁

    前言 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) .这些已经写好提供的锁为我们开发提供了便利. 在之前的文章<一文彻底搞懂面试中常问的各种"锁" >中介绍了Java中的各种"锁",可能对于不是很了解这些概念的同学来说会觉得有点绕,所以我决定拆分出来,逐步详细的介绍一下这些锁的来龙去脉,那么这篇文章就先来会一会"自旋锁". 正文 出现原因 在我们的

  • 深入理解java自旋锁

    简单回顾一下CAS算法 CAS算法 即compare and swap(比较与交换),是一种有名的无锁算法.无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization).CAS算法涉及到三个操作数 需要读写的内存值 V 进行比较的值 A 拟写入的新值 B 当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作).一般情

  • Java 自旋锁(spinlock)相关知识总结

    一.前言 谈到『自旋锁』,可能大家会说,这有啥好讲的,不就是等待资源的线程"原地打转"嘛.嗯,字面理解的意思很到位,但能深入具体点吗?自旋锁的设计真就这么简单? 本文或者说本系列的目的,都是让大家不要停留在表面,而是深入分析,做到: 灵活使用 掌握原理 优缺点 二.锁的优化:自旋锁 当多个线程想同时访问同一个资源时,就存在资源冲突,这时,大家最直接想到的就是加锁来互斥访问,加锁会有这么几个问题: 等待资源的线程进入睡眠,发生用户态向内核态的切换,有一定的性能开销: 占用资源的线程很快就

  • 什么是Java自旋锁

    目录 1.自旋锁 2.工作流程 3.缺点 4.实现原理 5.自适应自旋 前言: 阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间.如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长. 1.自旋锁 在有些场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失. 如果机器有多个CPU核心,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间

  • Java锁之自旋锁详解

    锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) .这些已经写好提供的锁为我们开发提供了便利,但是锁的具体性质以及类型却很少被提及.本系列文章将分析JAVA下常见的锁名称以及特性,为大家答疑解惑. 1.自旋锁 自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区.如下 复制代码 代码如下: public class SpinLock { private AtomicRe

  • Java实现手写自旋锁的示例代码

    目录 前言 自旋锁 原子性 自己动手写自旋锁 自己动手写可重入自旋锁 总结 前言 我们在写并发程序的时候,一个非常常见的需求就是保证在某一个时刻只有一个线程执行某段代码,像这种代码叫做临界区,而通常保证一个时刻只有一个线程执行临界区的代码的方法就是锁.在本篇文章当中我们将会仔细分析和学习自旋锁,所谓自旋锁就是通过while循环实现的,让拿到锁的线程进入临界区执行代码,让没有拿到锁的线程一直进行while死循环,这其实就是线程自己“旋”在while循环了,因而这种锁就叫做自旋锁. 自旋锁 原子性

  • Java实现手写线程池的示例代码

    目录 前言 线程池给我们提供的功能 工具介绍 Worker设计 线程池设计 总结 前言 在我们的日常的编程当中,并发是始终离不开的主题,而在并发多线程当中,线程池又是一个不可规避的问题.多线程可以提高我们并发程序的效率,可以让我们不去频繁的申请和释放线程,这是一个很大的花销,而在线程池当中就不需要去频繁的申请线程,他的主要原理是申请完线程之后并不中断,而是不断的去队列当中领取任务,然后执行,反复这样的操作.在本篇文章当中我们主要是介绍线程池的原理,因此我们会自己写一个非常非常简单的线程池,主要帮

  • 利用Java手写阻塞队列的示例代码

    目录 前言 需求分析 阻塞队列实现原理 线程阻塞和唤醒 数组循环使用 代码实现 成员变量定义 构造函数 put函数 offer函数 add函数 take函数 重写toString函数 完整代码 总结 前言 在我们平时编程的时候一个很重要的工具就是容器,在本篇文章当中主要给大家介绍阻塞队列的原理,并且在了解原理之后自己动手实现一个低配版的阻塞队列. 需求分析 在前面的两篇文章ArrayDeque(JDK双端队列)源码深度剖析和深入剖析(JDK)ArrayQueue源码当中我们仔细介绍了队列的原理,

  • java 百度手写文字识别接口配置代码

    代码如下所示: package org.fh.util; import org.json.JSONObject; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.List; import java.util.Map; /** * 说明:获取文字识别token类 * 作者:

  • 超详细PyTorch实现手写数字识别器的示例代码

    前言 深度学习中有很多玩具数据,mnist就是其中一个,一个人能否入门深度学习往往就是以能否玩转mnist数据来判断的,在前面很多基础介绍后我们就可以来实现一个简单的手写数字识别的网络了 数据的处理 我们使用pytorch自带的包进行数据的预处理 import torch import torchvision import torchvision.transforms as transforms import numpy as np import matplotlib.pyplot as plt

  • java基于mongodb实现分布式锁的示例代码

    目录 原理 实现 使用 原理 通过线程安全findAndModify 实现锁 实现 定义锁存储对象: /** * mongodb 分布式锁 */ @Data @NoArgsConstructor @AllArgsConstructor @Document(collection = "distributed-lock-doc") public class LockDocument { @Id private String id; private long expireAt; privat

  • PyTorch实现手写数字识别的示例代码

    目录 加载手写数字的数据 数据加载器(分批加载) 建立模型 模型训练 测试集抽取数据,查看预测结果 计算模型精度 自己手写数字进行预测 加载手写数字的数据 组成训练集和测试集,这里已经下载好了,所以download为False import torchvision # 是否支持gpu运算 # device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # print(device) # print(torch.cud

  • C语言手写集合List的示例代码

    目录 前沿 定义结构 创建List 扩容 创建数据节点 给集合添加值 删除集合内指定的值 删除集合内指定下标的值 打印集合 迭代器 查询指定元素的下标(第一个) 末尾查询指定元素下标(第一个) 判断数组是否有序 二分查询 修改集合指定元素的值 快速排序 集合去重 集合复制 集合合并 集合差集 集合补集 集合并集 集合交集 销毁集合 前沿 数组长度是固定的,那么在很多时候我们并不知道到底有多少数据需要存储,这时候我么就需要一个可变长度的数组来进行存储,在C语言中需要我们自己进行定义,我们称为集合

  • C语言实现手写红黑树的示例代码

    目录 前沿 红黑树代码 测试 前沿 写C的红黑树前建议先看我博客这篇文章Java-红黑树 主要看原理 红黑树代码 #ifndef STUDY_RBTREE_H #define STUDY_RBTREE_H #include "charkvlinked.h" typedef int boolean;//定义一个布尔类型 #define TRUE 1 #define FALSE 0 enum COL{RED=0,BLACK=1}; typedef struct rBNode { char

  • java基于jedisLock—redis分布式锁实现示例代码

    分布式锁是啥? 单机锁的概念:我们正常跑的单机项目(也就是在tomcat下跑一个项目不配置集群)想要在高并发的时候加锁很容易就可以搞定,java提供了很多的机制例如:synchronized.volatile.ReentrantLock等锁的机制. 为啥需要分布式锁:当我们的项目比较庞大的时候,单机版的项目已经不能满足吞吐量的需求了,需要对项目做负载均衡,有可能还需要对项目进行解耦拆分成不同的服务,那么肯定是做成分布式的项目,分布式的项目因为是不同的程序控制,所以使用java提供的锁并不能完全保

随机推荐