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. javascript 对象池

    * 一个对象池的简单应用 tool tip tootip.html <html> <head> <meta charset="UTF-8"> & ...

  2. openldap 双主模式部署

    规划两台机器 系统版本centos7.5 master1上部署ldap: 一.安装启动openldap软件 yum -y install openldap compat-openldap openld ...

  3. P4357-[CQOI2016]K远点对【K-Dtree】

    正题 题目链接:https://www.luogu.com.cn/problem/P4357 题目大意 平面上给出\(n\)个点,求第\(k\)远的点对距离. 解题思路 \(\text{K-Dtree ...

  4. P4707-重返现世【dp,数学期望,扩展min-max容斥】

    正题 题目链接:https://www.luogu.com.cn/problem/P4707 题目大意 \(n\)个物品,每次生成一种物品,第\(i\)个被生成的概率是\(\frac{p_i}{m}\ ...

  5. P4491-[HAOI2018]染色【多项式,二项式反演】

    正题 题目链接:https://www.luogu.com.cn/problem/P4491 题目大意 给\(n\)个物品染上\(m\)种颜色,若恰好有\(k\)个颜色的物品个数为\(S\)那么就会产 ...

  6. selenium--多窗口

    多窗口/句柄 有些页面的链接打开后,会重新打开一个窗口,对于这种情况,想在新页面上操作,就得先切换窗口了.获取窗口的唯一标识用句柄表示,所以只需要切换句柄,我们就能在多个页面上灵活自如的操作了. 1. ...

  7. 轻量级 Java 基础开发框架,Solon & Solon Cloud 1.5.40 发布

    Solon 已有120个生态扩展插件,此次版本以细节打磨为主: 增加 mybatisplus-solon-plugin 插件 //至此,Solon 已完成国内外主流的5个ORM框架插件适配 插件 so ...

  8. 阿里云研究员叔同:Serverless 正当时!

    作者 | 叔同 导读:Serverless 将开发人员从繁重的手动资源管理和性能优化中解放出来,就像数十年前汇编语言演变到高级语言的过程一样,云计算生产力再一次发生变革.Serverless 的核心价 ...

  9. ThreadLocal概念以及使用场景

    ThreadLocal概念以及使用场景 根据自身的知识深度,这里只限于自己使用和学习的知识点整理,原理的解释还需要再沉淀. 该文章从项目开发中举例,希望能帮助到各位,不了解ThreadLocal的朋友 ...

  10. Oil Deposits 新年特辑篇

    链接:E - Oil Deposits 题目: The GeoSurvComp geologic survey company is responsible for detecting undergr ...