在多线程情景下,如果不会某一共享变量采取一些同步机制,很可能发生数据不安全现象,比如购买车票时,当多个人购买时,不加锁就会产生多人买同一张票的现象,显然这是不可取的。所以要有一种同步机制,在某一时刻只能有一个线程处理该共享变量。

同步器的加锁

我将自己实现的同步器成为RoadAQS.

主要变量如下:

//当前锁的状态,1表示加锁,0表示未加锁
private volatile int state = 0;
private final static Unsafe unsafe = UnsafeInstance.reflectUnsafe();
//state在内存中的偏移量
private final static long stateOffset;
//当前持有锁的线程
private Thread lockHoder;
//是一个线程安全的队列,记录等待获取锁的线程
private ConcurrentLinkedQueue<Thread> waiters = new ConcurrentLinkedQueue<>(); static {
try {
stateOffset = unsafe.objectFieldOffset(RoadAQS.class.getDeclaredField("state"));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}

整体思想:

刚一开始初始化时会利用反射获取一个Unsafe魔法类,然后获取变量state在内存中的偏移量,为后续的CAS操作做准备。然后开始尝试获取锁,当等待队列为空或者当前线程等于等待队列的第一个线程,然后CAS更新状态为1成功,说明获得锁成功,并将同步器的拥有者设置为当前线程。如果加锁失败,就将该线程放入到等待队列中,然后开始无限for循环。

进入循环背内部,再尝试一次获取锁,仍然失败后,开始调用LockSupport.park()将该线程进行阻塞,与Object.wait一个最大的区别就是park()、unpark()能够指定具体的线程进行唤醒,而object.notify只能随机唤醒一个。

阻塞后当其他线程执行完退出后,会调用LockSupport.unpark(t)对等待队列中的第一个线程进行唤醒,唤醒后会继续执行for循环内部的代码,再尝试获得锁。获得锁后,从等待队列中取出,并将同步器的拥有者改为该线程。

public void lock() {
if(acquire()){
return;
}
Thread current = Thread.currentThread();
waiters.add(current);
for(;;) {
if(current == waiters.peek() && acquire()) {
waiters.poll();
return;
}
LockSupport.park();
}
}
public boolean acquire() {
Thread t = Thread.currentThread();
if ((waiters.size() == 0 || t == waiters.peek()) && compareAndSwapInt(0, 1)) {
setLockHoder(t);
return true;
}
return false;
}

同步器的解锁

获取当前的锁状态,并尝试更新为0,成功后将同步器的拥有者设为null,然后获取等待队列的第一个队列,将该队列进行唤醒。

public void unlock() {
if (Thread.currentThread() != getLockHolder()) {
throw new RuntimeException("lockHolder is not current Thread");
}
int state = getState();
if (compareAndSwapInt(state, 0)) {
setLockHoder(null);
Thread t = waiters.peek();
if (t != null) {
LockSupport.unpark(t);
}
}
}

测试用例

public class RoadAQSTest {
public static void main(String[] args) {
Goods goods = new Goods();
for(int i=0; i<100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
goods.reduceCount();
}
}, "Thread-" + i + "------").start();
}
} private static class Goods{
private int count = 10;
private RoadAQS lock = new RoadAQS();
public void reduceCount() { lock.lock(); if (count > 0) {
System.out.println("线程" + lock.getLockHolder() + " 获取第 " + count + "件商品");
count--;
} else {
System.out.println("商品已卖完!");
}
lock.unlock();
}
}
}

测试结果:

动手实现一个同步器(AQS)的更多相关文章

  1. Java 中队列同步器 AQS(AbstractQueuedSynchronizer)实现原理

    前言 在 Java 中通过锁来控制多个线程对共享资源的访问,使用 Java 编程语言开发的朋友都知道,可以通过 synchronized 关键字来实现锁的功能,它可以隐式的获取锁,也就是说我们使用该关 ...

  2. 基于AQS自己实现一个同步器

    前面说了这个多,我们可以自己尝试实现一个同步器,我们可以简单的参考一下ReentrantLock这个类的实现方式,我们就简单的实现一个不可重入的独占锁吧! 一.简单分析ReentrantLock的结构 ...

  3. 死磕 java同步系列之自己动手写一个锁Lock

    问题 (1)自己动手写一个锁需要哪些知识? (2)自己动手写一个锁到底有多简单? (3)自己能不能写出来一个完美的锁? 简介 本篇文章的目标一是自己动手写一个锁,这个锁的功能很简单,能进行正常的加锁. ...

  4. JAVA并发-同步器AQS

    什么是AQS aqs全称为AbstractQueuedSynchronizer,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLo ...

  5. Java 显示锁 之 队列同步器AQS(六)

    1.简述 锁时用来控制多个线程访问共享资源的方式,一般情况下,一个锁能够防止多个线程同时访问共享资源.但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁. 在Java 5.0之前,在协调对共享对 ...

  6. 并发——抽象队列同步器AQS的实现原理

    一.前言   这段时间在研究Java并发相关的内容,一段时间下来算是小有收获了.ReentrantLock是Java并发中的重要部分,所以也是我的首要研究对象,在学习它的过程中,我发现它是基于抽象队列 ...

  7. 《动手实现一个网页加载进度loading》

    loading随处可见,比如一个app经常会有下拉刷新,上拉加载的功能,在刷新和加载的过程中为了让用户感知到 load 的过程,我们会使用一些过渡动画来表达.最常见的比如"转圈圈" ...

  8. C#中自己动手创建一个Web Server(非Socket实现)

    目录 介绍 Web Server在Web架构系统中的作用 Web Server与Web网站程序的交互 HTTPListener与Socket两种方式的差异 附带Demo源码概述 Demo效果截图 总结 ...

  9. 自己动手实现一个简单的JSON解析器

    1. 背景 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着诸多优点.比如易读性更好,占用空间更少等.在 ...

随机推荐

  1. 关于求最长子串,使得最大减最小小于k的问题-以POJ4003为例

    问题 给出一个长度为\(n\)的序列\(a[i]\),有\(m\)次询问, 每次给你一个\(k\),让你求一个最长子串\([l,r]\),使得\(max_l^r\{a_i\}-min_l^r\{a_i ...

  2. 编写 Django 应用单元测试

    作者:HelloGitHub-追梦人物 文中所涉及的示例代码,已同步更新到 HelloGitHub-Team 仓库 我们博客功能越来越来完善了,但这也带来了一个问题,我们不敢轻易地修改已有功能的代码了 ...

  3. 《Head first设计模式》学习笔记

    1. 单例模式 2. 工厂模式 3. 抽象工厂 4. 策略模式 5. 观察者模式 6. 装饰者模式 7. 命令模式 8. 适配器模式 9. 外观模式 10. 模版方法模式 11. 迭代器模式 设计模式 ...

  4. 云服务器centos系统安装python

    1.查看python的版本 $ cd /usr/bin/$ ls python* $ ls -al python* //查看依赖关系 2.如果版本不合适可以卸载python再重新安装 # rpm -q ...

  5. Serverless + Egg.js 后台管理系统实战

    本文将介绍如何基于 Egg.js 和 Serverless 实现一个后台管理系统 作为一名前端开发者,在选择 Nodejs 后端服务框架时,第一时间会想到 Egg.js,不得不说 Egg.js 是一个 ...

  6. linux入门系列15--文件传输之vsftp服务

    前面的系列文章基本讲完了linux管理相关的基础知识,从本篇开始讲解centos7中服务程序的部署和配置,以便为外部提供各种服务. 日常工作和娱乐中,我们所需的各种资源都离不开网络以及各种服务,我们通 ...

  7. 剖析Java OutOfMemoryError异常

    剖析Java OutOfMemoryError异常 在JVM中,除了程序计数器外,虚拟机内存中的其他几个运行时区域都有发生OutOfMemoryError异常的可能,本篇就来深入剖析一下各个区域出现O ...

  8. git系列之---工作中项目的常用git操作

    0.本地git的安装 官网下载 1.git 配置 git config user.name  查看 用户名 git config user.email   查看 邮箱 git config --glo ...

  9. 掌握这四大MySQL知识点,吊打面试官

    作为一名后端开发,MySQL的使用必不可少,合理的使用索引和索引调优是后端开发者必须掌握的技能之一. 在日常数据库的问题当中,不合理的使用索引占大部分. MySQL是大家工作上最常用的关系型数据库之一 ...

  10. hadoop完全分布式部署

    1.我们先看看一台节点的hdfs的信息:(已经安装了hadoop的虚拟机:安装hadoophttps://www.cnblogs.com/lyx666/p/12335360.html) start-d ...