Atomic简介

​ Atomic包是java.util.concurrent下的另一个专门为线程安全设计的Java包,包含多个原子操作类这个包里面提供了一组原子变量类。

​ 其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。可以对基本数据、数组中的基本数据、对类中的基本数据进行操作。原子变量类相当于一种泛化的volatile变量,能够支持原子的和有条件的读-改-写操作。

传统锁的问题

我们先来看一个例子:计数器(Counter),采用Java里比较方便的锁机制synchronized关键字,初步的代码如下:


class Counter { private int value; public synchronized int getValue() {
return value;
} public synchronized int increment() {
return ++value;
} public synchronized int decrement() {
return --value;
}
}

其实像这样的锁机制,满足基本的需求是没有问题的了,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁,这里会有些问题:首先,如果被阻塞的线程优先级很高很重要怎么办?其次,如果获得锁的线程一直不释放锁怎么办?(这种情况是非常糟糕的)。还有一种情况,如果有大量的线程来竞争资源,那CPU将会花费大量的时间和资源来处理这些竞争(事实上CPU的主要工作并非这些),同时,还有可能出现一些例如死锁之类的情况,最后,其实锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿过于笨重,因此,对于这种需求我们期待一种更合适、更高效的线程安全机制,于是CAS诞生了。

传送门:CAS

Atomic

java.util.concurrent.atomic中的类可以分成4组:

  • 标量类(Scalar):AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
  • 数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  • 更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
  • 复合变量类:AtomicMarkableReference,AtomicStampedReference

基础数据型

第一组AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference这四种基本类型用来处理布尔,整数,长整数,对象四种数据,其内部实现不是简单的使用synchronized,而是一个更为高效的方式CAS (compare and swap) + volatile和native方法,从而避免了synchronized的高开销,执行效率大为提升。我们来看个例子,与我们平时i++所对应的原子操作为:getAndIncrement()

package com.company.atomics;

/*
* 使用原子变量类定义一个计数器
* 该计数器在整个程序中都能使用,并且在所有的地方都可以使用这个计数器,
* 这个计数器可以设计为单例
* */ import java.util.concurrent.atomic.AtomicLong; public class Indicator {
//构造方法私有化
private Indicator(){}
//定义一个私有的本类静态的对象
private static final Indicator INSTANCE=new Indicator();
//提供一个公共静态方法返回唯一实例
public static Indicator getInstance(){
return INSTANCE;
}
//使用原子变量类保存请求总数,成功数,失败数
private final AtomicLong requestCount=new AtomicLong(0);
private final AtomicLong successCount=new AtomicLong(0);
private final AtomicLong failureCount=new AtomicLong(0); //有新的请求
public void requestProcessReceive(){
requestCount.incrementAndGet();
}
//处理成功
public void requestProcessSuccess(){
successCount.incrementAndGet();
}
//处理失败
public void requestProcessFailur(){
failureCount.incrementAndGet();
}
//查看总数,
public long getRequestCount(){
return requestCount.get();
}
//查看成功数
public long getSuccessCount(){
return successCount.get();
}
//查看失败数
public long getFailurCount(){
return failureCount.get();
}
}

结果测试:

package com.company.atomics;
import java.util.Random;
public class IndicatorTest {
public static void main(String[] args) {
//通过线程模拟请求
for (int i = 0; i < 10000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//每个线程就是一个请求,请求总数要+1
Indicator.getInstance().requestProcessReceive();
int num=new Random().nextInt();
if (num%2==0){ //处理成功
Indicator.getInstance().requestProcessSuccess();
}else{ //处理失败
Indicator.getInstance().requestProcessFailur();
}
}
}).start();
} try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} System.out.println(Indicator.getInstance().getRequestCount());//总数
System.out.println(Indicator.getInstance().getSuccessCount());//成功数
System.out.println(Indicator.getInstance().getFailurCount());//失败数 }
}

常用方法

  • 构造函数(两个构造函数)

    • 默认的构造函数:初始化的数据分别是false,0,0,null
    • 带参构造函数:参数为初始化的数据
  • set( )和get( )方法:可以原子地设定和获取atomic的数据。类似于volatile,保证数据会在主存中设置或读取

  • void set()和void lazySet():set设置为给定值,直接修改原始值;lazySet延时设置变量值,这个等价于set()方法,但是由于字段是volatile类型的,因此次字段的修改会比普通字段(非volatile字段)有稍微的性能延时(尽管可以忽略),所以如果不是想立即读取设置的新值,允许在“后台”修改值,那么此方法就很有用。

  • getAndSet( )方法

    • 原子的将变量设定为新数据,同时返回先前的旧数据

    • 其本质是get( )操作,然后做set( )操作。尽管这2个操作都是atomic,但是他们合并在一起的时候,就不是atomic。在Java的源程序的级别上,如果不依赖synchronized的机制来完成这个工作,是不可能的。只有依靠native方法才可以。

      public final int getAndSet(int newValue) {
      for (;;) {
      int current = get();
      if (compareAndSet(current, newValue))
      return current;
      }
      }
  • compareAndSet( ) 和weakCompareAndSet( )方法

    这两个方法都是conditional modifier方法。接收2个参数,一个是期望数据(expected),一个是新数据(new),如果atomic里面的数据和期望数据一 致,则将新数据设定给atomic的数据,返回true,表明成功;否则就不设定,并返回false。JSR规范中说:以原子方式读取和有条件地写入变量但不 创建任何 happen-before 排序,因此不提供与除 weakCompareAndSet 目标外任何变量以前或后续读取或写入操作有关的任何保证。大意就是说调用weakCompareAndSet时并不能保证不存在happen- before的发生(也就是可能存在指令重排序导致此操作失败)。但是从Java源码来看,其实此方法并没有实现JSR规范的要求,最后效果和 compareAndSet是等效的,都调用了unsafe.compareAndSwapInt()完成操作。

    public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    public final boolean weakCompareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
  • 对于 AtomicInteger、AtomicLong还提供了一些特别的方法。

    getAndIncrement( ):以原子方式将当前值加 1,相当于线程安全的i++操作。

    incrementAndGet( ):以原子方式将当前值加 1, 相当于线程安全的++i操作。

    getAndDecrement( ):以原子方式将当前值减 1, 相当于线程安全的i--操作。

    decrementAndGet ( ):以原子方式将当前值减 1,相当于线程安全的--i操作。

    addAndGet( ): 以原子方式将给定值与当前值相加, 实际上就是等于线程安全的i =i+delta操作。

    getAndAdd( ):以原子方式将给定值与当前值相加, 相当于线程安全的t=i;i+=delta;return t;操作。

    以实现一些加法,减法原子操作。(注意 --i、++i不是原子操作,其中包含有3个操作步骤:第一步,读取i;第二步,加1或减1;第三步:写回内存)

数组型

AtomicIntegerArray,AtomicLongArray还有AtomicReferenceArray类进一步扩展了原子操作,对这些类型的数组提供了支持。这些类在为其数组元素提供 volatile 访问语义方面也引人注目,这对于普通数组来说是不受支持的。

他们内部并不是像AtomicInteger一样维持一个valatile变量,而是全部由native方法实现,如下

AtomicIntegerArray的实现片断:

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final int base = unsafe.arrayBaseOffset(int[].class);
private static final int scale = unsafe.arrayIndexScale(int[].class);
private final int[] array;
public final int get(int i) {
return unsafe.getIntVolatile(array, rawIndex(i));
}
public final void set(int i, int newValue) {
unsafe.putIntVolatile(array, rawIndex(i), newValue);
}

基本使用示例:

public class AtomicIntegerArrayTest {
public static void main(String[] args) {
//创建一个指定长度的原子数组
AtomicIntegerArray atomicIntegerArray=new AtomicIntegerArray(10);
System.out.println(atomicIntegerArray);//[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
//返回指定位置的元素
System.out.println(atomicIntegerArray.get(0));//0
System.out.println(atomicIntegerArray.get(1));//0
//设置指定位置的元素
atomicIntegerArray.set(0,10);
//先获取指定位置的值在设置
System.out.println(atomicIntegerArray.getAndSet(1,11));//0
//把数组元素加上某个值
System.out.println(atomicIntegerArray.addAndGet(0,22));//32
System.out.println(atomicIntegerArray.getAndAdd(1,33));//11
System.out.println(atomicIntegerArray);//[32, 44, 0, 0, 0, 0, 0, 0, 0, 0] //CAS操作,如果0位置的值是32,就将其值修改为222
System.out.println(atomicIntegerArray.compareAndSet(0,32,222));//返回ture
//先自增在返回
System.out.println(atomicIntegerArray.incrementAndGet(0));//223
//先返回再自减
System.out.println(atomicIntegerArray.getAndIncrement(1));//44
System.out.println(atomicIntegerArray);//[223, 45, 0, 0, 0, 0, 0, 0, 0, 0]
//先自减再返回
System.out.println(atomicIntegerArray.decrementAndGet(2));//-1
//先返回再自减
System.out.println(atomicIntegerArray.getAndDecrement(3));//0
System.out.println(atomicIntegerArray);//[223, 45, -1, -1, 0, 0, 0, 0, 0, 0] }
}
import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicIntegerArrayTest01 {
//定义一个原子数组
static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10); public static void main(String[] args) {
//定义线程数组
Thread[] threads=new Thread[10];
//给线程数组赋值
for (int i = 0; i < threads.length; i++) {
threads[i]=new addThread();
}
//开启子线程
for (Thread thread:threads
) {
thread.start();
} //在主线程中查看自增完后原子数组中各个元素的值,主线程中需要在所有子线程都执行完后查看
//把所有的子线程合并到当前主线程中
for (Thread thread:threads
) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(atomicIntegerArray)
} //定义一个线程类,在线程类中修改原子数组
static class addThread extends Thread{
@Override
public void run() {
//把数组元素的每个元素自增1000次
for (int i = 0; i < 1000; i++) {
for (int j = 0; j < atomicIntegerArray.length() ; j++) {
atomicIntegerArray.getAndIncrement(i%atomicIntegerArray.length());
}
}
}
}
}

输出结果:

//10个线程,每个线程自增1000次
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

字段更新器

AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater基于反射的实用工具,可以对指定类的指定 volatile 字段进行原子更新。API非常简单,但是也是有一些约束:

(1)字段必须是volatile类型的

(2)字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。也就是说 调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。

(3)只能是实例变量,不能是类变量,也就是说不能加static关键字。

(4)只能是可修改变量,不能使final变量,因为final的语义就是不可修改。实际上final的语义和volatile是有冲突的,这两个关键字不能同时存在。

(5)对于AtomicIntegerFieldUpdaterAtomicLongFieldUpdater 只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater

netty5.0中类ChannelOutboundBuffer统计发送的字节总数,由于使用volatile变量已经不能满足,所以使用AtomicIntegerFieldUpdater 来实现的

示例:

//定义一个User实体类
public class User {
int id; volatile int age; public User(int id, int age) {
this.id = id;
this.age = age;
} @Override
public String toString() {
return "User{" +
"id=" + id +
", age=" + age +
'}';
}
} //定义一个线程类,实现字段自增
public class AtomicUpdater extends Thread{
private User user; private AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age"); public AtomicUpdater(User user) {
this.user = user;
} @Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(updater.getAndIncrement(user));
}
}
} //测试类
public class Test {
public static void main(String[] args) {
//初始化User对象
User user = new User(1, 10);
//开启10个线程
for (int i = 0; i < 10; i++) {
new AtomicUpdater(user).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(user);
}
}

输出结果:

//一个线程自增10次,10个线程自增100次
User{id=1, age=110}

引用型

原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类

AtomicReference

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceTest {
static AtomicReference<String> atomicReference=new AtomicReference<>("abc"); public static void main(String[] args) {
//创建十个线程修改字符串
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
if (atomicReference.compareAndSet("abc","def")){
System.out.println(Thread.currentThread().getName()+"把字符串修改为了def");
}
}
}).start();
}
//再创建100个线程
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
if (atomicReference.compareAndSet("def","abc")){
System.out.println(Thread.currentThread().getName()+"把字符串还原为了abc");
}
}
}).start();
} try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.get());
} }

AtomicStampedReference

由于atomicReference是采用了CAS,所以可能会产生ABA问题。利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在ABA问题了。这就是AtomicStampedReference的解决方案。

public class AtomicStampReferenceTest {

    //定义AtomicStampedReference引用操作"abc"字符串,指定初始版本号为0
private static AtomicStampedReference<String> stampedReference=new AtomicStampedReference<>("abc",0); public static void main(String[] args) {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
stampedReference.compareAndSet("abc","def",
stampedReference.getStamp(),
stampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+"----"+stampedReference.getReference()); stampedReference.compareAndSet("def","abc",
stampedReference.getStamp(),
stampedReference.getStamp()+1);
}
});
Thread t2=new Thread(new Runnable() {
//如果在此处获取stamp,则可能无法获取当前的版本号,在线程睡眠的那一秒中,版本号可能发生了改变
//int stamp=stampedReference.getStamp();//获取版本号
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
int stamp=stampedReference.getStamp();//获取版本号
System.out.println(stampedReference.compareAndSet("abc","ggg",stamp,stamp+1));
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
} System.out.println(stampedReference.getReference()); }
}

AtomicMarkableReference

原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)

Java多线程之Atomic:原子变量与原子类的更多相关文章

  1. JAVA多线程之volatile 与 synchronized 的比较

    一,volatile关键字的可见性 要想理解volatile关键字,得先了解下JAVA的内存模型,Java内存模型的抽象示意图如下: 从图中可以看出: ①每个线程都有一个自己的本地内存空间--线程栈空 ...

  2. Java多线程之Runnable与Thread

    Java多线程之Thread与Runnable 一.Thread VS Runnable 在java中可有两种方式实现多线程,一种是继承Thread类,一种是实现Runnable接口:Thread类和 ...

  3. Java多线程之ConcurrentSkipListMap深入分析(转)

    Java多线程之ConcurrentSkipListMap深入分析   一.前言 concurrentHashMap与ConcurrentSkipListMap性能测试 在4线程1.6万数据的条件下, ...

  4. JAVA多线程之wait/notify

    本文主要学习JAVA多线程中的 wait()方法 与 notify()/notifyAll()方法的用法. ①wait() 与 notify/notifyAll 方法必须在同步代码块中使用 ②wait ...

  5. java多线程之yield,join,wait,sleep的区别

    Java多线程之yield,join,wait,sleep的区别 Java多线程中,经常会遇到yield,join,wait和sleep方法.容易混淆他们的功能及作用.自己仔细研究了下,他们主要的区别 ...

  6. JAVA多线程之UncaughtExceptionHandler——处理非正常的线程中止

    JAVA多线程之UncaughtExceptionHandler——处理非正常的线程中止 背景 当单线程的程序发生一个未捕获的异常时我们可以采用try....catch进行异常的捕获,但是在多线程环境 ...

  7. java多线程之wait和notify协作,生产者和消费者

    这篇直接贴代码了 package cn.javaBase.study_thread1; class Source { public static int num = 0; //假设这是馒头的数量 } ...

  8. Java多线程之volatile详解

    本文目录 从多线程交替打印A和B开始 Java 内存模型中的可见性.原子性和有序性 Volatile原理 volatile的特性 volatile happens-before规则 volatile ...

  9. java多线程之CAS原理

    前言 在Java并发包中有这样一个包,java.util.concurrent.atomic,该包是对Java部分数据类型的原子封装,在原有数据类型的基础上,提供了原子性的操作方法,保证了线程安全.下 ...

随机推荐

  1. 鸿蒙内核源码分析(自旋锁篇) | 当立贞节牌坊的好同志 | 百篇博客分析OpenHarmony源码 | v26.02

    百篇博客系列篇.本篇为: v26.xx 鸿蒙内核源码分析(自旋锁篇) | 当立贞节牌坊的好同志 | 51.c.h .o 进程通讯相关篇为: v26.xx 鸿蒙内核源码分析(自旋锁篇) | 当立贞节牌坊 ...

  2. 踩坑经验总结之go web开源库第一次编译构建

    前言:记录一个go新手第一次构建复杂开源库的经历.go虽然是新手,但是编程上还是有多年的经验,除了c/c++,用过IDEA能进行简单的java编程.甚至scala编程.所以最开始还是有点信心的.所以也 ...

  3. 前端规范之Git提交规范(Commitizen)

    代码规范是软件开发领域经久不衰的话题,几乎所有工程师在开发过程中都会遇到或思考过这一问题.而随着前端应用的大型化和复杂化,越来越多的前端团队也开始重视代码规范.同样,前段时间,笔者所在的团队也开展了一 ...

  4. logback日志入门超级详细讲解

    基本信息 日志:就是能够准确无误地把系统在运行状态中所发生的情况描述出来(连接超时.用户操作.异常抛出等等): 日志框架:就是集成能够将日志信息统一规范后输出的工具包. Logback优势 Logba ...

  5. 10.3 Nginx

    Nginx介绍 engine X,2002年开发,分为社区版和商业版(nginx plus) 2019年 f5 Networks 6.7亿美元收购nginx Nginx 免费 开源 高性能 http ...

  6. Windows Terminal 美化教程

    Windows Terminal 美化教程 1.安装Windows Terminal 在微软商店搜索Windows Terminal下载即可 2.安装相应的插件 使用管理员权限打开Windows Te ...

  7. Powerful Number 学习笔记

    定义 对于一个正整数 \(n\) ,若完全分解之后不存在指数 \(=1\) ,则称 \(n\) 为 \(\text{Powerful Number}\) . 可以发现的是,在 \([1,n]\) 中, ...

  8. 题解 CF1103E Radix sum

    题目传送门 题目大意 给出一个\(n\)个数的序列\(a_{1,2,..,n}\),可以选\(n\)次,每次可以选与上次选的相同的数,问对于\(\forall p\in[0,n-1]\)满足选出来的数 ...

  9. 题解 GT考试

    题目传送门 题目大意 给出\(n,m,k\),以及一个长度为\(m\)的数字串\(s_{1,2,...,m}\),求有多少个长度为\(n\)的数字串\(X\)满足\(s\)不出现在其中的个数模\(k\ ...

  10. 洛谷2387 NOI2014魔法森林(LCT维护最小生成树)

    本题是运用LCT来维护一个最小生成树. 是一个经典的套路 题目中求的是一个\(max(a_i)+max(b_i)\)尽可能小的路径. 那么这种的一个套路就是,先按照一维来排序,然后用LCT维护另一维 ...