专栏首页编程坑太多Java线程安全如何进行原子操作,一致性的最佳实践

Java线程安全如何进行原子操作,一致性的最佳实践

上次主要说了一个结论就是volatile,线程安全可见性的问题,大部分情况下可见性都不需要管理的,但是多线程编程的代码中,我们会使用到volatile关键字,通过volatile关键字解决可见性问题,一个线程对共享变量的修改,能够及时的被其他线程看到。只要加了volatile关键字,所有对变量的读取立刻进行同步。volatile关键字的用途:禁止缓存;相关的变量不做重排序。

(一)线程安全

  • ① 介绍

线程安全是多线程编程时的计算机程序代码中的一个概念。当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。

  • ② 竞态条件与临界区

多线程访问了相同的资源,向这些资源做了写操作时,对执行顺序有要求。

临界区

incr 方法内部就是临界区域,关键部分代码的多线程并发执行,对会执行结果产生影响,下面的代码就属于临界区。不见得就有一行代码,只要对多线程并发有影响的都叫临界区。

int i = 0;
i =i +1;
x = i

竞态条件

可能发生在临界区域内的特殊条件。触发线程安全的环境。上边的代码 x = i 就是竞态条件。

  • ③ 问题代码

多线程情况下,预期打印20000,但是打印了13914。

public class LockDemo {

     volatile int i = 0;


    public void add() {
        // TODO xx00
         i++;// 三个步骤
    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo ld = new LockDemo();

        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    ld.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(ld.i);
    }
}

(二)共享资源

  1. 如果一段代码是线程安全的,则它不包含竞态条件,只有当多线程更新共享资源时,才会发生竞态条件。
  2. 栈封闭时,不会在线程之间共享的变量,都是线程安全的。
  3. 局部对象引用对象不共享,但是引用了对象存储在共享堆中。如果方法内创建的对象,只是在方法中传递,并且不对其他线程可用,那么也是线程安全的。

判定规则

如果创建,使用和处理资源,永远不会逃脱单个线程的控制,该资源的使用线程安全的。

(三)不可变对象

  • ① 实例
public class Demo{
  private int value = 0;
  public Demo(int value){
    this.value = value;
}
  public int getValue(){
    return this.value
}
}

方法里面没有setValue的方法,这就是不可变的对象。

  • ② 定义

创建不可变的共享对象来保证对象在线程共享时不会被修改,从而实现线程安全。实例被创建,value变量就不能再被修改,这就是不可变性。

不可变是相对的,其实可以通过反射的方式进行破坏。

(四)原子操作定义

  • ① 介绍

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被分割而只执行其中的一部分(不可中断性)。

将整个操作视作一个整体,资源在该次操作中保持一致,这是原子性的核心特性。

  • ② 实例分析
public class Demo{
  public int i = 0;
   public void incr(){
    i++
  }
}

里面的i++ 底层运行分为三步:加载i,计算+1,赋值i ,在底层被拆分了。

在多线程需要原子性操作,对修改,读取,保持一致性。

(五)什么是CAS

  • ① 介绍

compare and swap的缩写,中文翻译成比较并交换。属于硬件同步原语,处理器提供了基本内功操作的原子性保证。CAS操作需要输入两个数值,一个旧值A(操作前的值)和一个新值B,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换新值,发生了变化则不交换。避免硬件底层出现并发的操作的可能。

JAVA中的sun,misc.Unsafe类,提供了compareAndSwpInt() 和 compareAndSwpLong() 等几个方法实现CAS。

  • ② 演示

Unsafe 是操作c和c++底层来完成的。

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class LockDemo {
    volatile int value = 0;

    static Unsafe unsafe; // 直接操作内存,修改对象,数组内存....强大的API
    private static long valueOffset;

    static {
        try {
            // 反射技术获取unsafe值
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);

            // 获取到 value 属性偏移量(用于定于value属性在内存中的具体地址)
            valueOffset = unsafe.objectFieldOffset(LockDemo.class
                    .getDeclaredField("value"));

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public void add() {
        // TODO xx00
        // i++;// JAVA 层面三个步骤
        // CAS + 循环 重试
        int current;
        do {
            // 操作耗时的话, 那么 线程就会占用大量的CPU执行时间
            current = unsafe.getIntVolatile(this, valueOffset);
        } while (!unsafe.compareAndSwapInt(this, valueOffset, current, current + 1));
        // 可能会失败
    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo ld = new LockDemo();

        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    ld.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(ld.value);
    }
}

上边的代码太高大上了,基本都看不懂吧,下面说一个简单的方式。

(六)J.U.C 包内的原子操作

  • ① 介绍

java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类,用于定义类似于线程的自定义子系统,包括线程池,异步 IO 和轻量级任务框架。还提供了设计用于多线程上下文中的 Collection 实现等。rt.jar中的其实原子性,jdk本身都考虑到了,定义了几种类型。

  • ② 封装类

JDK1.8新增的原子性

原有的 Atomic系列类通过CAS来保证并发时操作的原子性,但是高并发也就意味着CAS的失败次数会增多,失败次数的增多会引起更多线程的重试,最后导致AtomicLong的效率降低。新的四个类通过减少并发,将单一value的更新压力分担到多个value中去,降低单个value的“热度”以提高高并发情况下的吞吐量。

DoubleAccumulator DoubleAdder LongAccumulator LongAdder

  • ③ 实例分析
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @program: dispatch_system
 * @description: ${description}
 * @author: LiMing
 * @create: 2019-10-31 10:57
 **/
public class LockDemo {

    // volatile int i = 0;
    AtomicInteger i = new AtomicInteger(0);


    public void add() {
        // TODO xx00
        // i++;// 三个步骤
        i.incrementAndGet();
    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo ld = new LockDemo();

        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    ld.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(ld.i);
    }
}

  • ② LongAdder

就是尝试使用分段CAS以及自动分段迁移的方式来大幅度提升多线程高并发执行CAS操作的性能!

在LongAdder的底层实现中,首先有一个base值,刚开始多线程来不停的累加数值,都是对base进行累加的,比如刚开始累加成了base = 5。接着如果发现并发更新的线程数量过多,就会开始施行分段CAS的机制,也就是内部会搞一个Cell数组,每个数组是一个数值分段。这时,让大量的线程分别去对不同Cell内部的value值进行CAS累加操作,这样就把CAS计算压力分散到了不同的Cell分段数值中了!这样就可以大幅度的降低多线程并发更新同一个数值时出现的无限循环的问题,大幅度提升了多线程并发更新数值的性能和效率!而且内部实现了自动分段迁移的机制,也就是如果某个Cell的value执行CAS失败了,那么就会自动去找另外一个Cell分段内的value值进行CAS操作。这样也解决了线程空旋转、自旋不停等待执行CAS操作的问题,让一个线程过来执行CAS时可以尽快的完成这个操作。会把base值和所有Cell分段数值加起来返回给你。

计算的时候很快,取结果的是比较慢。这个思路就类似现在的互联网分而治之的思路,量比较大,就接很多小的管道,小管道里面慢慢的去处理,如果直接处理比较的大的比较慢,就让小管道慢慢处理。分而治之的思路。

  • ③ 示例
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

// 测试用例:同时运行2秒,检查谁的次数最多
public class LongAdderDemo {
    private long count = 0;

    // 同步代码块的方式
    public void testSync() throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                long starttime = System.currentTimeMillis();
                while (System.currentTimeMillis() - starttime < 2000) { // 运行两秒
                    synchronized (this) {
                        ++count;
                    }
                }
                long endtime = System.currentTimeMillis();
                System.out.println("SyncThread spend:" + (endtime - starttime) + "ms" + " v" + count);
            }).start();
        }
    }

    // Atomic方式
    private AtomicLong acount = new AtomicLong(0L);

    public void testAtomic() throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                long starttime = System.currentTimeMillis();
                while (System.currentTimeMillis() - starttime < 2000) { // 运行两秒
                    acount.incrementAndGet(); // acount++;
                }
                long endtime = System.currentTimeMillis();
                System.out.println("AtomicThread spend:" + (endtime - starttime) + "ms" + " v-" + acount.incrementAndGet());
            }).start();
        }
    }

    // LongAdder 方式
    private LongAdder lacount = new LongAdder();
    public void testLongAdder() throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                long starttime = System.currentTimeMillis();
                while (System.currentTimeMillis() - starttime < 2000) { // 运行两秒
                    lacount.increment();
                }
                long endtime = System.currentTimeMillis();
                System.out.println("LongAdderThread spend:" + (endtime - starttime) + "ms" + " v-" + lacount.sum());
            }).start();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        LongAdderDemo demo = new LongAdderDemo();
        demo.testSync();
        demo.testAtomic();
        demo.testLongAdder();
    }
}

(七)CAS三大问题

  • ① ABA问题

因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A、变成了B、又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但实际上却变化了。

  • ② 循环开销时间长

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果jvm能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:

1.它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。 2.它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率。

  • ③ 只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个方法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a合并一下ij=2a,然后用CAS来操作ij。从java1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

PS:代码都是最终的结果,这里面涉及的思路很多,JDK已经到13了里面的工具越来越多。本次主要引用了原子性,数据变化,保证数据的一致性,这是个本质,希望各位老铁参与评论,大家多交流。

本文分享自微信公众号 - 编程坑太多(idig88),作者:诸葛阿明

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-07-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Lambda表达式概述

    IT故事会
  • JAVA之线程中止(三)

    PS:上边介绍了三种线程中止的方式,stop(不要用),interrupt(通过抛出异常,方便开发者始终),volatile(标志位,首先业务逻辑可以通过变量才...

    IT故事会
  • JAVA线程之ThreadLocal与栈封闭(六)

    PS:这次说了线程封闭的概念,其实很容易理解只要知道在ThreadLocal是JVM内部维护了一个Map就可以了。栈封闭没有纤细概述,跟局部变量是一个概念。

    IT故事会
  • 非阻塞同步机制和CAS

    我们知道在java 5之前同步是通过Synchronized关键字来实现的,在java 5之后,java.util.concurrent包里面添加了很多性能更加...

    程序那些事
  • SpringBoot RabbitMQ实现消息可靠投递

    因为收不到该条消息的ACK。所以一直处于发送中。开启任务调度再次进行投递(投递次数+1,且更新下次投递时间)

    喜欢天文的pony站长
  • SpringBoot RabbitMQ实现消息可靠投递

    因为收不到该条消息的ACK。所以一直处于发送中。开启任务调度再次进行投递(投递次数+1,且更新下次投递时间)

    喜欢天文的pony站长
  • 第三阶段-Java常见对象:【第八章 System类】

    System.gc() 可用于垃圾回收.当使用System.gc() 回收某个对象所占用的内存之前,通过要求程序调用适当的方法来清理资源,在没有明确指定资源清理...

    BWH_Steven
  • 机器学习第4天:线性回归及梯度下降

    线性回归属于监督学习,因此方法和监督学习应该是一样的,先给定一个训练集,根据这个训练集学习出一个线性函数,然后测试这个函数训练的好不好(即此函数是否足够拟合训练...

    明天依旧可好
  • 那些年我们用Java写过的小游戏 --- 快速击键系统

    训练技能点 面向对象设计的思想 使用类图理解类的关系 类的封装 构造方法的使用 this、static关键字的使用 需求概述 根据输入速率和正确率将玩家分为不同...

    房上的猫
  • Java 线程的 wait 和 notify 的神坑

    也许我们只知道wait和notify是实现线程通信的,同时要使用synchronized包住,其实在开发中知道这个是远远不够的。接下来看看两个常见的问题。

    芋道源码

扫码关注云+社区

领取腾讯云代金券