用Atomic实现锁
一直想写ReentrantLock,就得先介绍AbstractQueueSynchronizer,可是我觉得这样写,不过瘾,我把代码贴一遍,懂的人自己就能找到这些代码看,不懂的人还是不懂。直到昨天灵机一动,不如自己从简到难重新写一遍,带着读者跳几个坑,可能就好了。
java.util.concurrent.lock下的几个锁以及synchronized锁其实背后都要使用atomic操作,那我们不妨就使用atomic操作把锁实现一遍。
咱们先从最简单的开始。
互斥
并发模型中,最简单的问题,就是互斥。一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源,这个公共资源,我们通常会称之为关键区。如何保护这个关键区就是互斥的问题。
这个其实比较简单,我只需要用一个atomic变量,让它为 0,不管有多少线程过来,谁先抢到这个变量把它置为1,谁就相当于拿到了关键区的使用权,而其他没抢到的就不能进入关键区。来看这样一个例子:
public class Mutex {
public static void main(String[] args) {
TestMutex test = new TestMutex();
int THREAD_NUM = 10;
Thread[] threads = new Thread[THREAD_NUM];
// 创建10个线程,让它们不断地去增加test.sum的值
for (int i = 0; i < THREAD_NUM; i++) {
Thread t= new Thread() {
public void run() {
for (int j = 0; j < 10000; j++) {
test.add();
}
}
};
t.start();
threads[i] = t;
}
for (Thread t : threads) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(test.sum);
}
}
class TestMutex {
public int sum = 0;
public void add() {
if (sum < 30_000) {
try {
// 这里sleep一下,增加线程切换的概率
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
sum += 50;
}
}
}
运行这个程序,可以看到结果是随机的,大概率会是落在30300到30450这个区间。这个是典型的因为并发引起的。那么想改正它,我们就可以把add用一个atomic变量保护起来。一个线程只有获得了这个许可,才能继续执行 add 操作。如果没有许可,就直接放弃,修改过后的Mutex变成这样:
class TestMutex {
public int sum = 0;
AtomicInteger mutex = new AtomicInteger(0);
public void add() {
if (!mutex.compareAndSet(0, 1))
return;
if (sum < 30_000) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
sum += 50;
}
mutex.set(0);
}
}
经过这样的修改,运行的结果就一定会是30000了。
同步
在互斥的例子里,如果一个线程拿不到许可(即mutex变量),那就直接放弃什么都不做了。但是如果我们希望它继续完成加法操作呢?那么线程之间就必须有一定的手段相互可以知道有没有线程在关键区里,如果有,那么我就不再进入关键区了,如果没有,我就尝试进去。
举个例子,有一条仅容一人通过的小巷子,有两个人相对而行,那么当左端的人进入了这个巷子以后,右端的人就不能再进去了,他得先等待。必须等到左端的人到达右端,然后告诉右端等待的人可以进了,右端的人才可以进去。这个小巷子就不光是互斥的关键区了,还得有两个人相互通知的机制。
还是拿之前的课程里的例子来说吧:
class LockTest {
public int total = 0;
public void testTwoThreads() throws InterruptedException {
Thread t1 = new Thread() {
public void run() {
for (int i = 0; i < 5_000; i++) {
incTotal();
}
}
};
Thread t2 = new Thread() {
public void run() {
for (int i = 0; i < 5_000; i++) {
incTotal();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(total);
}
public void incTotal() {
total += 1;
}
}
这个例子,启动了两个线程,每个线程都执行5000次加法,但如果真正运行的话,大概率不会是10000。这就又是并发的问题了,具体的原因,咱们之前的课程里分析过,这里就不再重复了。如果我们使用Atomic保护关键区的思路来改写,应该怎么做呢?
自旋锁
今天介绍一种自旋锁的思想。举个实际的例子,去公司的卫生间蹲坑,一直没位置,这时候由于没有任何的通知机制,所以我只能每隔一会去看看有没有空位,有空位就赶紧抢,然后把门锁上,如果没有,就只能一直在门口等。
这就是最典型的自旋锁。它不需要任何的通知机制,一个线程去抢许可变量,抢到了就进关键区,抢不到就死循环一直抢。好,我们来实现一个自旋锁:
public class SpinLock implements Lock{
AtomicInteger state = new AtomicInteger(0);
public void lock() {
for(;;) {
if (state.get() == 1)
continue;
else if (state.compareAndSet(0, 1)) {
break;
}
}
}
public void unlock() {
state.set(0);
}
}
原理很简单,就是一直CAS抢锁,如果抢不到,就一直死循环,直到抢到了才退出这个循环。然后,我们使用这个工具改写一下incTotal:
private Lock lock = new SpinLock();
//.....
public void incTotal() {
lock.lock();
total += 1;
lock.unlock();
}
这次再运行,结果就是10_000了。我们使用一个Atomic变量把整个关键区保护起来了。
自旋锁实现起来非常简单,如果关键区的执行时间很短,往往自旋等待会是一种比较高效的做法,它可以避免线程的频繁切换和调度。但如果关键区的执行时间很长,那这种做法就会大量地浪费CPU资源。
针对关键区执行时间长的情况,该怎么办呢?下篇文章再说吧。
- Scrapy在Ubuntu下的安装与配置
- Selenium2+python自动化20-引入unittest框架
- HDU 1002 A + B Problem II(高精度加法(C++/Java))
- POJ 1018 Communication System
- POJ 1017 Packets
- Codeforces 725B Food on the Plane
- Codefoces 723B Text Document Analysis
- Codefoces 723A The New Year: Meeting Friends
- ECJTUACM16 Winter vacation training #1 题解&源码
- 信息学奥赛一本通算法(C++版)基础算法:高精度计算
- 看破欧拉函数的奥秘
- 线段树入门总结
- 从零基础学三分查找
- Codeforces Beta Round #1 A,B,C
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 大型项目技术栈第三讲 ztree的使用
- JavaWeb新手训练经典项目 & 半小时高效开发 & 海量知识点涵盖 = 从这里开始
- Java反射_笔记分享
- Java注解详细总结
- 文档驱动 —— 表单组件(六):基于AntDV的Form表单的封装,目标还是不写代码
- 这就是你日日夜夜想要的docker!!!---------Docker资源控制--Cgroup
- 2020-09-26:请问rust中的&和c++中的&有哪些区别?
- python在Keras中使用LSTM解决序列问题
- python使用MongoDB,Seaborn和Matplotlib文本分析和可视化API数据
- 用于NLP的Python:使用Keras进行深度学习文本生成
- 用Python的Numpy求解线性方程组
- python用于NLP的seq2seq模型实例:用Keras实现神经机器翻译
- 使用Python和Keras进行主成分分析、神经网络构建图像重建
- python使用Flask,Redis和Celery的异步任务
- 在R语言中进行缺失值填充:估算缺失值