在了解JDK提供的线程原子性操作工具类之前,我们应该先知道什么是原子性:在多线程并发的条件下,对于变量的操作是线程安全的,不会受到其他线程的干扰。接下来我们就学习JDK中线程的原子性操作。

一、CAS原理

  说道原子性,不得不提的就是CAS原理:

  使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

  CAS既然和锁有关,那么它和Synchronized有什么区别呢:

  元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。

二、原子更新基本类型

  在我们实际开发中,线程原子性问题是比较常见的地方,最简单的例子:i ++;i ++是我们开发中一定会用到的一段代码,但是你知道吗,i ++在多线程环境中是不安全的,我们接下来通过一个例子来说明为什么i ++不安全:

  

public class Demo2 implements Runnable {

    //给线程方法传递一个参数
static int count=0; /**
*线程任务,将count的值遍历到10000再输出
*/
public void run() {
System.out.println("当前线程获取count的值为:"+count);
for (int i = 0; i < 10000; i++) {
count++;
}
} public static void main(String[] args) throws InterruptedException {
//创建多线程环境,这里创建了10个线程
Thread[] thread = new Thread[10];
//未创建的多线程中添加线程任务
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Demo2());
}
//启动每个线程任务
for (int i = 0; i < 10; i++) {
thread[i].start();
}
//join方法的作用是阻塞主线程,防止还没有计算完成,就开始输出count的值了
for (int i = 0; i < 10; i++) {
thread[i].join();
}
System.out.println("count计算的结果是:"+count);
}
}

  运行的结果是:

  

  这里的 count++就是我们说的i ++;可以很明确的看到,count的值除了重复获取外, 执行count++操作时,也是有问题的,这个问题的原因是:count在执行++操作时,因为是多线程环境,假设count的值在第一个线程获取为0了但是还没有完成++操作,就有第二个线程抢到了CPU资源,此时count的值还是0,所以就出现了这个数据重复获取,导致count++操作不准确,因此count++是线程不安全的。

  面对这样的问题,JDK为我们提供了原子基本类型来解决,一个有三个:

    AtomicBoolean:原子更新布尔类型

     AtomicInteger:原子更新整型

     AtomicLong:原子更新长整型

  这个三个类中的方法都一致,我们主要使用的是:

  get():直接返回值

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

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

  其他的方法可以参考AtomicInteger的API文档,因此我们改造上面的场景:

  

public class Demo2 implements Runnable {

    //给线程方法传递一个参数
static AtomicInteger count=new AtomicInteger(0); /**
*线程任务,将count的值遍历到10000再输出
*/
public void run() {
System.out.println("当前线程获取count的值为:"+count);
for (int i = 0; i < 10000; i++) {
count.incrementAndGet();
}
} public static void main(String[] args) throws InterruptedException {
//创建多线程环境,这里创建了10个线程
Thread[] thread = new Thread[10];
//未创建的多线程中添加线程任务
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Demo2());
}
//启动每个线程任务
for (int i = 0; i < 10; i++) {
thread[i].start();
}
//join方法的作用是阻塞主线程,防止还没有计算完成,就开始输出count的值了
for (int i = 0; i < 10; i++) {
thread[i].join();
}
System.out.println("count计算的结果是:"+count);
}
}

运行结果:

可以看到计算的结果是准确的,证明我们的count ++操作是线程安全的

三、原子更新数组类型

  atomic包中更新数组类型的方式与更新基本类型类似,一共提供了三种方式:

    AtomicIntegerArray:原子更新整型数组中的元素;

    AtomicLongArray:原子更新长整型数组中的元素;

    AtomicReferenceArray:原子更新引用类型数组中的元素

    三个类的用法都很相似,我们以AtomicIntegerArray类来作为介绍,一共常用的有3个方法:

  addAndGet(int i, int delta):以原子更新的方式将数组中索引为i的元素与输入值相加;

  getAndIncrement(int i):以原子更新的方式将数组中索引为i的元素自增加1;

  compareAndSet(int i, int expect, int update):将数组中索引为i的位置的元素进行更新(注意这个是非原子性操作)

  我们就用这个几个方法写一个实例:

  

public class Demo2 implements  Runnable{

    //给线程方法传递一个参数
static int[] arr = new int[]{0, 0}; //将创建的数组传递给AtomicIntegerArray,进行原子性操作
static AtomicIntegerArray count=new AtomicIntegerArray(arr); /**
*线程任务,将count第一个元素的值赋值让其等于i的值,将第二个元素的值自增
*/
public void run() {
for (int i = 0; i < 10000; i++) {
//以原子更新的方式将数组中索引为i的元素与输入值相加
count.getAndSet(0, i);
//以原子更新的方式将数组中索引为i的元素自增加1
count.getAndIncrement(1);
}
} public static void main(String[] args) throws InterruptedException {
//创建多线程环境,这里创建了10个线程
Thread[] thread = new Thread[10];
//未创建的多线程中添加线程任务
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Demo2());
}
//启动每个线程任务
for (int i = 0; i < 10; i++) {
thread[i].start();
}
//join方法的作用是阻塞主线程,防止还没有计算完成,就开始输出count的值了
for (int i = 0; i < 10; i++) {
thread[i].join();
}
System.out.println("计算完成后count数组第一个元素的值为:"+count.get(0));
System.out.println("计算完成后count数组第二个元素的值为:"+count.get(1));
} }

运行的结果:

可以看到,我们在线程任务中进行的自增和赋值操作都是原子性的

四、原子更新类 

  一共有三种方式

  AtomicReference:原子更新引用类型

  AtomicReferenceFieldUpdater:原子更新引用类型里的字段

  AtomicMarkableReference:原子更新带有标记位的引用类型

  主要使用的是get和set方法,我们来看一个实例:

public class Demo2 implements  Runnable{

    //创建一个原子更新引用,引用一个Bean对象
static AtomicReference<Bean> count=new AtomicReference(); /**
*线程任务,对bean对象的字段赋值
*/
public void run() {
for (int i = 0; i < 10000; i++) {
//以原子更新的方式将数组中索引为i的元素与输入值相加
Bean bean=new Bean();
bean.setName("一共有"+i+"次执行");
bean.setNum(i);
count.set(bean);
}
} public static void main(String[] args) throws InterruptedException {
//创建多线程环境,这里创建了10个线程
Thread[] thread = new Thread[10];
//未创建的多线程中添加线程任务
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Demo2());
}
//启动每个线程任务
for (int i = 0; i < 10; i++) {
thread[i].start();
}
//join方法的作用是阻塞主线程,防止还没有计算完成,就开始输出count的值了
for (int i = 0; i < 10; i++) {
thread[i].join();
}
System.out.println(count.get().getName());
System.out.println("赋值的结果是"+count.get().num);
} }

其中bean对象的代码是:

  

public class Bean {

    String name;

    int num;

    public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public int getNum() {
return num;
} public void setNum(int num) {
this.num = num;
} }

运行的结果

五、原子更新字段

  AtomicReference:原子更新引用类型;

  AtomicReferenceFieldUpdater:原子更新引用类型里的字段;

  AtomicMarkableReference:原子更新带有标记位的引用类型;

public class Demo2 implements  Runnable{

    static Bean bean=new Bean();
//创建一个原子更新字段对象,更新bean中的num属性
static AtomicIntegerFieldUpdater<Bean> count=AtomicIntegerFieldUpdater.newUpdater(Bean.class,"num"); /**
*线程任务,对bean对象的字段赋值
*/
public void run() {
for (int i = 0; i < 10000; i++) {
//以原子更新的方式将数组中索引为i的元素与输入值相加 bean.setNum(i);
count.getAndIncrement(bean);
}
} public static void main(String[] args) throws InterruptedException {
//创建多线程环境,这里创建了10个线程
Thread[] thread = new Thread[10];
//未创建的多线程中添加线程任务
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Demo2());
}
//启动每个线程任务
for (int i = 0; i < 10; i++) {
thread[i].start();
}
//join方法的作用是阻塞主线程,防止还没有计算完成,就开始输出count的值了
for (int i = 0; i < 10; i++) {
thread[i].join();
}
System.out.println("赋值的结果是"+count.get(bean));
} }

  

java并发学习--第四章 JDK提供的线程原子性操作工具类的更多相关文章

  1. java并发学习--第七章 JDK提供的线程工具类

    一.ThreadLocal ThreadLocal类用于隔离多线程中使用的对象,为ThreadLocal类中传递的泛型就是要隔离的对象,简单的来说:如果我们在主线程创建了一个对象,并且需要给下面的多线 ...

  2. Java并发程序设计(四)JDK并发包之同步控制

    JDK并发包之同步控制 一.重入锁 重入锁使用java.util.concurrent.locks.ReentrantLock来实现.示例代码如下: public class TryReentrant ...

  3. java并发学习第五章--线程中的锁

    一.公平锁与非公平锁 线程所谓的公平,就是指的是线程是否按照锁的申请顺序来获取锁,如果是遵守顺序来获取,这就是个公平锁,反之为非公平锁. 非公平锁的优点在于吞吐量大,但是由于其不是遵循申请锁的顺序来获 ...

  4. java并发学习--第六章 线程之间的通信

    一.等待通知机制wait()与notify() 在线程中除了线程同步机制外,还有一个最重要的机制就是线程之间的协调任务.比如说最常见的生产者与消费者模式,很明显如果要实现这个模式,我们需要创建两个线程 ...

  5. java并发学习--第三章 线程安全问题

    线程的安全问题一直是我们在开发过程中重要关注的地方,出现线程安全问题的必须满足两个条件:存在着两个或者两个以上的线程:多个线程共享着共同的一个资源, 而且操作资源的代码有多句.接下来我们来根据JDK自 ...

  6. “全栈2019”Java多线程第二十四章:等待唤醒机制详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  7. 《Java程序设计》第四章-认识对象

    20145221<Java程序设计>第四章-认识对象 总结 教材学习内容总结 类与对象 定义:对象是Java语言中重要的组成部分,之前学过的C语言是面向过程的,而Java主要是面向对象的. ...

  8. “全栈2019”Java多线程第十四章:线程与堆栈详解

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...

  9. “全栈2019”Java异常第十四章:将异常输出到文本文件中

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java异 ...

随机推荐

  1. Spark Streaming的优化之路—从Receiver到Direct模式

    作者:个推数据研发工程师 学长     1 业务背景   随着大数据的快速发展,业务场景越来越复杂,离线式的批处理框架MapReduce已经不能满足业务,大量的场景需要实时的数据处理结果来进行分析.决 ...

  2. Otto

    导入依赖:implementation 'com.squareup:otto:1.3.8'1定义一个类继承Bus,并且设置单列模式注册和声明订阅者发送事件,最后解除注册与EventBus相同Event ...

  3. ORACLE权限管理—创建只读账号

    创建只读用户:grant connect to user; grant create session to user; 1.创建角色 CREATE ROLE SELECT_ROLE 2.给角色分配权限 ...

  4. fedora23安装firefox中的flash插件-最终解决问题是: 要给libflashplayer.so以777权限, 开始给的755权限没有实现!

    下载的flash插件是一个rpm包. ===================================== rpm查看文件属于哪个包? 要看这个rpm包安装过还是没有安装过? (如果不用-p就是 ...

  5. python进行数据库迁移的时候显示(TypeError: __init__() missing 1 required positional argument: 'on_delete')

    进行数据库迁移的时候,显示  TypeError: __init__() missing 1 required positional argument: 'on_delete' 图示: 出现原因: 在 ...

  6. DataFrame 结构

    概念 DataFrame 是表格型的数据结构 ,DataFrame 本质上可以看做是由series 组成的字典, 它既有行索引,也有列索引.  它并不是列表,也不是字典,.

  7. myeclipse的修改背景颜色和字体

  8. Ecshop二次开发必备基础

    EcShop二次开发学习方法 近年来,随着互联网的发展,电子商务也跟着一起成长,B2B,C2C,B2C的电子商务模式也不断的成熟.这时催生出了众多电子商务相关的PHP开源产品.B2C方面有Ecshop ...

  9. xmake新增智能代码扫描编译模式

    最近给xmake增加了一个新特性,对于一份工程源码,可以不用编写makefile,也不用编写各种make相关的工程描述文件(例如:xmake.lua,makefile.am, cmakelist.tx ...

  10. 配置OSPF认证

      按照上图拓扑配置路由器的IP 配置完后测试直连网段连通性 搭建OSPF网络 注意是多区域的配置,R2是ABR 连着area0和area1 并且每个路由器的环回接口IP也要加进去  此时密码以明文方 ...