前言

在使用多线程并发编程的时,经常会遇到对共享变量修改操作。此时我们可以选择ConcurrentHashMap,ConcurrentLinkedQueue来进行安全地存储数据。但如果单单是涉及状态的修改,线程执行顺序问题,使用Atomic开头的原子组件或者ReentrantLock、CyclicBarrier之类的同步组件,会是更好的选择,下面将一一介绍它们的原理和用法

  • 原子组件的实现原理CAS
  • AtomicBoolean、AtomicIntegerArray等原子组件的用法、
  • 同步组件的实现原理
  • ReentrantLock、CyclicBarrier等同步组件的用法

关注公众号,一起交流,微信搜一搜: 潜行前行

原子组件的实现原理CAS

应用场景

  • 可用来实现变量、状态在多线程下的原子性操作
  • 可用于实现同步锁(ReentrantLock)

原子组件

  • 原子组件的原子性操作是靠使用cas来自旋操作volatile变量实现的
  • volatile的类型变量保证变量被修改时,其他线程都能看到最新的值
  • cas则保证value的修改操作是原子性的,不会被中断

基本类型原子类

AtomicBoolean //布尔类型
AtomicInteger //正整型数类型
AtomicLong //长整型类型
  • 使用示例
public static void main(String[] args) throws Exception {
AtomicBoolean atomicBoolean = new AtomicBoolean(false);
//异步线程修改atomicBoolean
CompletableFuture<Void> future = CompletableFuture.runAsync(() ->{
try {
Thread.sleep(1000); //保证异步线程是在主线程之后修改atomicBoolean为false
atomicBoolean.set(false);
}catch (Exception e){
throw new RuntimeException(e);
}
});
atomicBoolean.set(true);
future.join();
System.out.println("boolean value is:"+atomicBoolean.get());
}
---------------输出结果------------------
boolean value is:false

引用类原子类

AtomicReference
//加时间戳版本的引用类原子类
AtomicStampedReference
//相当于AtomicStampedReference,AtomicMarkableReference关心的是
//变量是否还是原来变量,中间被修改过也无所谓
AtomicMarkableReference
  • AtomicReference的源码如下,它内部定义了一个volatile V value,并借助VarHandle(具体子类是FieldInstanceReadWrite)实现原子操作,MethodHandles会帮忙计算value在类的偏移位置,最后在VarHandle调用Unsafe.public final native boolean compareAndSetReference(Object o, long offset, Object expected, Object x)方法原子修改对象的属性
public class AtomicReference<V> implements java.io.Serializable {
private static final long serialVersionUID = -1848883965231344442L;
private static final VarHandle VALUE;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
VALUE = l.findVarHandle(AtomicReference.class, "value", Object.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
private volatile V value;
....

ABA问题

  • 线程X准备将变量的值从A改为B,然而这期间线程Y将变量的值从A改为C,然后再改为A;最后线程X检测变量值是A,并置换为B。但实际上,A已经不再是原来的A了
  • 解决方法,是把变量定为唯一类型。值可以加上版本号,或者时间戳。如加上版本号,线程Y的修改变为A1->B2->A3,此时线程X再更新则可以判断出A1不等于A3
  • AtomicStampedReference的实现和AtomicReference差不多,不过它原子修改的变量是volatile Pair<V> pair;,Pair是其内部类。AtomicStampedReference可以用来解决ABA问题
public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
  • 如果我们不关心变量在中间过程是否被修改过,而只是关心当前变量是否还是原先的变量,则可以使用AtomicMarkableReference
  • AtomicStampedReference的使用示例
public class Main {
public static void main(String[] args) throws Exception {
Test old = new Test("hello"), newTest = new Test("world");
AtomicStampedReference<Test> reference = new AtomicStampedReference<>(old, 1);
reference.compareAndSet(old, newTest,1,2);
System.out.println("对象:"+reference.getReference().name+";版本号:"+reference.getStamp());
}
}
class Test{
Test(String name){ this.name = name; }
public String name;
}
---------------输出结果------------------
对象:world;版本号:2

数组原子类

AtomicIntegerArray	 //整型数组
AtomicLongArray  //长整型数组
AtomicReferenceArray //引用类型数组
  • 数组原子类内部会初始一个final的数组,它把整个数组当做一个对象,然后根据下标index计算法元素偏移量,再调用UNSAFE.compareAndSetReference进行原子操作。数组并没被volatile修饰,为了保证元素类型在不同线程的可见,获取元素使用到了UNSAFEpublic native Object getReferenceVolatile(Object o, long offset)方法来获取实时的元素值
  • 使用示例
//元素默认初始化为0
AtomicIntegerArray array = new AtomicIntegerArray(2);
// 下标为0的元素,期待值是0,更新值是1
array.compareAndSet(0,0,1);
System.out.println(array.get(0));
---------------输出结果------------------
1

属性原子更新类

AtomicIntegerFieldUpdater 
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater
  • 如果操作对象是某一类型的属性,可以使用AtomicIntegerFieldUpdater原子更新,不过类的属性需要定义成volatile修饰的变量,保证该属性在各个线程的可见性,否则会报错
  • 使用示例
public class Main {
public static void main(String[] args) {
AtomicReferenceFieldUpdater<Test,String> fieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Test.class,String.class,"name");
Test test = new Test("hello world");
fieldUpdater.compareAndSet(test,"hello world","siting");
System.out.println(fieldUpdater.get(test));
System.out.println(test.name);
}
}
class Test{
Test(String name){ this.name = name; }
public volatile String name;
}
---------------输出结果------------------
siting
siting

累加器

Striped64
LongAccumulator
LongAdder
//accumulatorFunction:运算规则,identity:初始值
public LongAccumulator(LongBinaryOperator accumulatorFunction,long identity)
  • LongAccumulator和LongAdder都继承于Striped64,Striped64的主要思想是和ConcurrentHashMap有点类似,分段计算,单个变量计算并发性能慢时,我们可以把数学运算分散在多个变量,而需要计算总值时,再一一累加起来
  • LongAdder相当于LongAccumulator一个特例实现
  • LongAccumulator的示例
public static void main(String[] args) throws Exception {
LongAccumulator accumulator = new LongAccumulator(Long::sum, 0);
for(int i=0;i<100000;i++){
CompletableFuture.runAsync(() -> accumulator.accumulate(1));
}
Thread.sleep(1000); //等待全部CompletableFuture线程执行完成,再获取
System.out.println(accumulator.get());
}
---------------输出结果------------------
100000

同步组件的实现原理

  • java的多数同步组件会在内部维护一个状态值,和原子组件一样,修改状态值时一般也是通过cas来实现。而状态修改的维护工作被Doug Lea抽象出AbstractQueuedSynchronizer(AQS)来实现
  • AQS的原理可以看下之前写的一篇文章:详解锁原理,synchronized、volatile+cas底层实现

同步组件

ReentrantLock、ReentrantReadWriteLock

  • ReentrantLock、ReentrantReadWriteLock都是基于AQS(AbstractQueuedSynchronizer)实现的。因为它们有公平锁和非公平锁的区分,因此没直接继承AQS,而是使用内部类去继承,公平锁和非公平锁各自实现AQS,ReentrantLock、ReentrantReadWriteLock再借助内部类来实现同步
  • ReentrantLock的使用示例
ReentrantLock lock = new ReentrantLock();
if(lock.tryLock()){
//业务逻辑
lock.unlock();
}
  • ReentrantReadWriteLock的使用示例
public static void main(String[] args) throws Exception {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
if(lock.readLock().tryLock()){ //读锁
//业务逻辑
lock.readLock().unlock();
}
if(lock.writeLock().tryLock()){ //写锁
//业务逻辑
lock.writeLock().unlock();
}
}

Semaphore实现原理和使用场景

  • Semaphore和ReentrantLock一样,也有公平和非公平竞争锁的策略,一样也是通过内部类继承AQS来实现同步
  • 通俗解释:假设有一口井,最多有三个人的位置打水。每有一个人打水,则需要占用一个位置。当三个位置全部占满时,第四个人需要打水,则要等待前三个人中一个离开打水位,才能继续获取打水的位置
  • 使用示例
public static void main(String[] args) throws Exception {
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 3; i++)
CompletableFuture.runAsync(() -> {
try {
System.out.println(Thread.currentThread().toString() + " start ");
if(semaphore.tryAcquire(1)){
Thread.sleep(1000);
semaphore.release(1);
System.out.println(Thread.currentThread().toString() + " 无阻塞结束 ");
}else {
System.out.println(Thread.currentThread().toString() + " 被阻塞结束 ");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
});
//保证CompletableFuture 线程被执行,主线程再结束
Thread.sleep(2000);
}
---------------输出结果------------------
Thread[ForkJoinPool.commonPool-worker-19,5,main] start
Thread[ForkJoinPool.commonPool-worker-5,5,main] start
Thread[ForkJoinPool.commonPool-worker-23,5,main] start
Thread[ForkJoinPool.commonPool-worker-23,5,main] 被阻塞结束
Thread[ForkJoinPool.commonPool-worker-5,5,main] 无阻塞结束
Thread[ForkJoinPool.commonPool-worker-19,5,main] 无阻塞结束
  • 可以看出三个线程,因为信号量设定为2,第三个线程是无法获取信息成功的,会打印阻塞结束

CountDownLatch实现原理和使用场景

  • CountDownLatch也是靠AQS实现的同步操作
  • 通俗解释:玩游戏时,假如主线任务需要靠完成五个小任务,主线任务才能继续进行时。此时可以用CountDownLatch,主线任务阻塞等待,每完成一小任务,就done一次计数,直到五个小任务全部被执行才能触发主线
  • 使用示例
public static void main(String[] args) throws Exception {
CountDownLatch count = new CountDownLatch(2);
for (int i = 0; i < 2; i++)
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(1000);
System.out.println(" CompletableFuture over ");
count.countDown();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
//等待CompletableFuture线程的完成
count.await();
System.out.println(" main over ");
}
---------------输出结果------------------
CompletableFuture over
CompletableFuture over
main over

CyclicBarrier实现原理和使用场景

  • CyclicBarrier则是靠ReentrantLock lockCondition trip属性来实现同步
  • 通俗解释:CyclicBarrier需要阻塞全部线程到await状态,然后全部线程再全部被唤醒执行。想象有一个栏杆拦住五只羊,需要当五只羊一起站在栏杆时,栏杆才会被拉起,此时所有的羊都可以飞跑出羊圈
  • 使用示例
public static void main(String[] args) throws Exception {
CyclicBarrier barrier = new CyclicBarrier(2);
CompletableFuture.runAsync(()->{
try {
System.out.println("CompletableFuture run start-"+ Clock.systemUTC().millis());
barrier.await(); //需要等待main线程也执行到await状态才能继续执行
System.out.println("CompletableFuture run over-"+ Clock.systemUTC().millis());
}catch (Exception e){
throw new RuntimeException(e);
}
});
Thread.sleep(1000);
//和CompletableFuture线程相互等待
barrier.await();
System.out.println("main run over!");
}
---------------输出结果------------------
CompletableFuture run start-1609822588881
main run over!
CompletableFuture run over-1609822589880

StampedLock

  • StampedLock不是借助AQS,而是自己内部维护多个状态值,并配合cas实现的
  • StampedLock具有三种模式:写模式、读模式、乐观读模式
  • StampedLock的读写锁可以相互转换
//获取读锁,自旋获取,返回一个戳值
public long readLock()
//尝试加读锁,不成功返回0
public long tryReadLock()
//解锁
public void unlockRead(long stamp)
//获取写锁,自旋获取,返回一个戳值
public long writeLock()
//尝试加写锁,不成功返回0
public long tryWriteLock()
//解锁
public void unlockWrite(long stamp)
//尝试乐观读读取一个时间戳,并配合validate方法校验时间戳的有效性
public long tryOptimisticRead()
//验证stamp是否有效
public boolean validate(long stamp)
  • 使用示例
public static void main(String[] args) throws Exception {
StampedLock stampedLock = new StampedLock();
long stamp = stampedLock.tryOptimisticRead();
//判断版本号是否生效
if (!stampedLock.validate(stamp)) {
//获取读锁,会空转
stamp = stampedLock.readLock();
long writeStamp = stampedLock.tryConvertToWriteLock(stamp);
if (writeStamp != 0) { //成功转为写锁
//fixme 业务操作
stampedLock.unlockWrite(writeStamp);
} else {
stampedLock.unlockRead(stamp);
//尝试获取写读
stamp = stampedLock.tryWriteLock();
if (stamp != 0) {
//fixme 业务操作
stampedLock.unlockWrite(writeStamp);
}
}
}
}

欢迎指正文中错误

参考文章

基础篇:JAVA原子组件和同步组件的更多相关文章

  1. java基础篇---文件上传(commons-FileUpload组件)

    上一篇讲解了smartupload组件上传,那么这一篇我们讲解commons-FileUpload组件上传 FileUpload是Apache组织(www.apache.org)提供的免费的上传组件, ...

  2. layui数据表格使用(一:基础篇,数据展示、分页组件、表格内嵌表单和图片)

    表格展示神器之一:layui表格 前言:在写后台管理系统中使用最多的就是表格数据展示了,使用表格组件能提高大量的开发效率,目前主流的数据表格组件有bootstrap table.layui table ...

  3. 谈论Java原子变量和同步的效率 -- 颠覆你的生活

    我们认为,由于思维定式原子变量总是比同步运行的速度更快,我想是这样也已经,直到实现了ID在第一次测试过程生成器不具有在这样一个迷迷糊糊的东西. 测试代码: import java.util.Array ...

  4. 基础篇-java开发

    开局必知 1.变量 在java中,以{}为作用域,所以就存在成员变量和局部变量之说 由于java是强类型语言,所以在申明变量的时候,必须指定类型 java里,一个变量有声明过程和初始化过程(也就是赋值 ...

  5. java学习笔记(基础篇)--java关键字与数据类型

    java关键字与数据类型 Java语言的关键字是程序代码中的特殊字符.包括: . 类和接口的声明--class, extends, implements, interface . 包引入和包声明--i ...

  6. java学习笔记(基础篇)—java数组

    一:什么是数组,什么时候使用数组? 数组是用来保存一组数据类型相同的元素的有序集合,数组中的每个数据称为元素.有序集合可以按照顺序或者下标取数组中的元素. 在Java中,数组也是Java对象.数组中的 ...

  7. Redis基础篇(六)数据同步:主从复制

    Redis具有高可靠性,体现在两方面: 一是数据尽量少丢失,通过前面介绍的持久化方式AOF和RDB,在宕机时可以恢复数据. 二是服务尽量少中断,通过副本冗余来实现. 今天我们学习的就是通过主从复制实现 ...

  8. Java学习 (四)基础篇 Java基础语法

    注释&标识符&关键字 注释 注释并不会被执行,其主要目的用于解释当前代码 书写注释是一个非常好的习惯,大厂要求之一 public class hello { public static ...

  9. android基础篇------------java基础(12)(多线程操作)

    <一>基本概念理解 1.什么是进程? 进程就是在某种程度上相互隔离,独立运行的程序.一般来说,系统都是支持多进程操作的,这所谓的多进程就是让系统好像同时运行多个程序. 2.什么是线程呢? ...

随机推荐

  1. Python模块是否支持自定义属性使用双下划线开头和结尾?

    我们知道在Python中,变量名类似__xxx__的,也就是以双下划线开头并且以双下划线结尾的变量和方法,是特殊变量,特殊变量是可以直接访问的,不是私有变量,所以,一般实例变量和类变量以及方法不能用_ ...

  2. 第15.23节 PyQt(Python+Qt)入门学习:Model/View架构中QListView视图配套Model的开发使用

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 一.概述 QListView理论上可以和所有QAbstractItemModel派生的类如QStri ...

  3. 第九章、Qt Designer可视化设计界面布局组件介绍

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 一.引言 在Qt Designer中,在左边部件栏的提供了界面布局相关部件,如图: 可以看到共包含有 ...

  4. LeetCode初级算法之数组:66 加一

    加一 题目地址:https://leetcode-cn.com/problems/plus-one/ 给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一.最高位数字存放在数组的首位, 数 ...

  5. clion2020最新安装破解版教程 内含激活码、破解补丁、无限重置时间插件

    clion是一款开发C.C++等相关程序的利器,到目前为止已经更新到2020.3版本啦!还没有升级的小伙伴们赶紧升级啦,本文教大家如何安装clion2020.3版本并且破解,此方法亲测100%可以永久 ...

  6. CF1327F AND Segments

    链接 Description 要求构造满足下列条件的长度为 \(n\) 的序列 \(a\) 的个数: 每个数值域在 \([0, 2 ^ k)\) \(m\) 个限制条件 \(l, r, x\),需要满 ...

  7. CSP-S2020 浙江 游记

    2020.10.9 今天是 \(2020\) 年 \(10\) 月 \(9\) 日,距离初赛还有两天(算两天吗,完整的应该只有一天多了). 原本对于比赛还是没什么感觉的,每天做做题,水水文章,感觉时间 ...

  8. 一起看下MySQL的崩溃恢复到底是怎么回事

    目录 回顾 思考一个问题 checkponit机制 Checkpoint的种类及触发条件 LSN 推荐阅读 本文稍微有点晦涩.但是看过之后你就能Get到MySQL的崩溃恢复到底是怎么做的! 文章公号 ...

  9. STL——容器(List)list 数据的存取

    list.front(); //返回第一个元素 list.back(); //返回最后一个元素 1 #include <iostream> 2 #include <list> ...

  10. KafkaMirrorMaker 的不足以及一些改进

    背景 某系统使用 Kafka 存储实时的行情数据,为了保证数据的实时性,需要在多地机房维护多个 Kafka 集群,并将行情数据同步到这些集群上. 一个常用的方案就是官方提供的 KafkaMirrorM ...