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

同步器的加锁

我将自己实现的同步器成为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. 练习:等待用户输入input()

    等待用户输入 执行下面的程序在按回车键后就会等待用户输入: 实例(Python 3.0+) #!/usr/bin/python3 input("\n\n按下 enter 键后退出." ...

  2. 微信小程序开发技巧总结(二) -- 文件的选取、移动、上传和下载

    微信小程序开发技巧总结(二) -- 文件的选取.移动.上传和下载 1.不同类型文件的选取 1.1 常用的图片 视频 对于大部分开发者来说,需要上传的文件形式主要为图片,微信为此提供了接口. wx.ch ...

  3. Gorm与数据库(单复数)表结构之间的映射

    Gorm连接MySQL: import ( _ "github.com/go-sql-driver/mysql" "github.com/jinzhu/gorm" ...

  4. 《C# GDI+ 破境之道》:第一境 GDI+基础 —— 第二节:画矩形

    有了上一节画线的基础,画矩形的各种边线就特别好理解了,所以,本节在矩形边线上,就不做过多的讲解了,关注一下画“随机矩形”的具体实现就好.与画线相比较,画矩形稍微复杂的一点就是在于它多了很多填充的样式. ...

  5. 14-SSM整合

    今日知识 1. Spring整合MyBatis 2. SSM普通整合 3. SSM整合(Spring和SpringMVC分离) 4. 纯JavaConfig的SSM Spring整合MyBatis 1 ...

  6. 05-Spring02-AOP

    今日知识 1. AOP 2. AspectJ 3. JdbcTemplate AOP 1. AOP :Aspect Oriented Programming,意为面向切面编程,通过预编译方式和运行期动 ...

  7. 终于解决 k8s 集群中部署 nodelocaldns 的问题

    自从开始在 kubernetes 集群中部署 nodelocaldns 以提高 dns 解析性能以来,一直被一个问题困扰,只要一部署 nodelocaldns ,在 coredns 中添加的 rewr ...

  8. 见异思迁:K8s 部署 Nginx Ingress Controller 之 kubernetes/ingress-nginx

    前天才发现,区区一个 nginx ingress controller 竟然2个不同的实现.一个叫 kubernetes/ingress-nginx ,是由 kubernetes 社区维护的,对应的容 ...

  9. oracle11g-R2数据库的逻辑备份(数据泵的导入导出)

    一.环境: server1迁移到server2 server1: 服务器号:201 系统:Windows server 2008 R2 x64 IP地址:192.168.2.201 oracle数据库 ...

  10. 自动化测试中,cookie的调用方法。

    以cookie为例 方法一: 将返回的cookie写到setUp()中,每次执行用例之前就会调用一次. 如: class AA(unittest.TestCase): def setUp(self): ...