Java多线程之Atomic:原子变量与原子类
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)对于AtomicIntegerFieldUpdater 和AtomicLongFieldUpdater 只能修改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:原子变量与原子类的更多相关文章
- JAVA多线程之volatile 与 synchronized 的比较
一,volatile关键字的可见性 要想理解volatile关键字,得先了解下JAVA的内存模型,Java内存模型的抽象示意图如下: 从图中可以看出: ①每个线程都有一个自己的本地内存空间--线程栈空 ...
- Java多线程之Runnable与Thread
Java多线程之Thread与Runnable 一.Thread VS Runnable 在java中可有两种方式实现多线程,一种是继承Thread类,一种是实现Runnable接口:Thread类和 ...
- Java多线程之ConcurrentSkipListMap深入分析(转)
Java多线程之ConcurrentSkipListMap深入分析 一.前言 concurrentHashMap与ConcurrentSkipListMap性能测试 在4线程1.6万数据的条件下, ...
- JAVA多线程之wait/notify
本文主要学习JAVA多线程中的 wait()方法 与 notify()/notifyAll()方法的用法. ①wait() 与 notify/notifyAll 方法必须在同步代码块中使用 ②wait ...
- java多线程之yield,join,wait,sleep的区别
Java多线程之yield,join,wait,sleep的区别 Java多线程中,经常会遇到yield,join,wait和sleep方法.容易混淆他们的功能及作用.自己仔细研究了下,他们主要的区别 ...
- JAVA多线程之UncaughtExceptionHandler——处理非正常的线程中止
JAVA多线程之UncaughtExceptionHandler——处理非正常的线程中止 背景 当单线程的程序发生一个未捕获的异常时我们可以采用try....catch进行异常的捕获,但是在多线程环境 ...
- java多线程之wait和notify协作,生产者和消费者
这篇直接贴代码了 package cn.javaBase.study_thread1; class Source { public static int num = 0; //假设这是馒头的数量 } ...
- Java多线程之volatile详解
本文目录 从多线程交替打印A和B开始 Java 内存模型中的可见性.原子性和有序性 Volatile原理 volatile的特性 volatile happens-before规则 volatile ...
- java多线程之CAS原理
前言 在Java并发包中有这样一个包,java.util.concurrent.atomic,该包是对Java部分数据类型的原子封装,在原有数据类型的基础上,提供了原子性的操作方法,保证了线程安全.下 ...
随机推荐
- layui左右移动tab标签模版
{% load staticfiles %} <!DOCTYPE html> <html> <head> <meta charset="utf-8& ...
- jquery中请求格式
$.ajax({ url:"/ceshi/", type:"get", cache:false, dataType:"json", data ...
- 使用AC自动机解决文章匹配多个候选词问题
解决的问题 KMP算法用于单个字符串匹配,AC自动机用于文章中匹配多个候选词. 流程 第一步,先将候选词先建立前缀树. 第二步,以宽度优先遍历的方式把前缀树的每个节点设置fail指针, 头节点的fai ...
- Python3入门系列之-----看完这一篇文章我终于学会了类
前言 类顾名思义,就是一类事物.或者叫做实例,它用来描述具有共同特征的一类事物.我们在Python中声明类的关键词是class,类还有功能和属性,属性就是这类事物的特征,而功能就是它能做什么,也是就是 ...
- 简单介绍session,cookie,token以及区别
Cookie简介 ①.是由服务器发给客户端的特殊信息,以文本的形式存放在客户端 ②.客户端再次请求的时候,会把Cookie回发给服务器 ③.服务器接收到后,会解析Cookie生成与客户端相对应的内容 ...
- 【Docker】(9)---每天5分钟玩转 Docker 容器技术之镜像
镜像是 Docker 容器的基石,容器是镜像的运行实例,有了镜像才能启动容器.为什么我们要讨论镜像的内部结构? 如果只是使用镜像,当然不需要了解,直接通过 docker 命令下载和运行就可以了. 但如 ...
- NOI2018屠龙勇士(扩展CRT + splay(multiset))
QWQ 一到假期就颓废 哎 今年新鲜出炉的NOI题,QwQ同步赛的时候写的,后来交了一发洛谷,竟然过了 首先 根据题目,我们很容易得到,假设对应每一条龙的剑的攻击力是\(atk\)的话 \[a_i-x ...
- SpringBoot入门08-整合Mabatis
整合所需的依赖 注解方式和映射文件方式的mybatis都可以被整合进springboot 创建springboot的web项目后,在pom加入spring-mybatis和mysql-jdbc和thy ...
- ReentrantLock可重入锁、公平锁非公平锁区别与实现原理
ReentrantLock是lock接口的一个实现类,里面实现了可重入锁和公平锁非公平锁 ReentrantLock公平锁和不公平锁实现原理 公平锁会获取锁时会判断阻塞队列里是否有线程再等待,若有获取 ...
- Java(30)集合五Set
作者:季沐测试笔记 原文地址:https://www.cnblogs.com/testero/p/15228440.html 博客主页:https://www.cnblogs.com/testero ...