美文网首页
8.线程安全之原子性

8.线程安全之原子性

作者: 强某某 | 来源:发表于2020-03-03 15:09 被阅读0次

1.竞态条件与临界区

public class Demo{
    public int i=0;
    public void incr(){
        i++;
    }
}

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

  • 临界区:incr方法内部就是临界区域,关键部分代码的多线程并发执行,会对执行结果产生影响。
  • 竞态条件:可能发生再临界区域内的特殊条件。多线程执行incr方法中的I++关键代码时,产生了竞态条件。
  1. 共享资源
  • 如果一段代码是线程安全的,则不它不包含竞态条件。只有当多个线程更新共享资源时,才会发生竞态条件。
  • 栈封闭时,不会再线程之间共享的变量,都是线程安全的。
  • 局部对象引用本身不共享,因为引用的对象存储再共享堆中。如果方法内创建的对象,只是再方法中传递,并且不对其他线程可用,那么也是线程安全的。
  • 判断规则:如果创建、使用和处理资源,永远不会逃脱单个线程的控制,该资源的使用是线程安全的
  1. 不可变对象
public class Demo{
    private int value=0;
    public Demo(int value){
        this.value=value;
    }
    public int getValue(){
        return this.value;
    }
}

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

  1. 示例demo
public class LockDemo {
    int i=0;
    public void add() {
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo lockDemo=new LockDemo();
        for (int i = 0; i < 2; i++) {
            new Thread(()->{
                for (int j = 0; j < 10000; j++) {
                    lockDemo.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(lockDemo.i);
    }
}

i++在多线程访问就产生了竞态条件,而且i++是三个步骤,读取,自增,赋值,即使用volatile修饰也只能保证可见性,但是多线程可能读取值相同,所以无法保证最终结果正确

  1. 原子操作定义
    原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可被打乱,也可以被切割而只执行其中的一部分(不可中断性)。将整个操作视作一个整体是原子性的核心特征。方式:CAS,锁,同步。
//同步小案例
public class LockDemo {
    int i = 0;

    public void add() {
        synchronized (this) {
            i++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo lockDemo = new LockDemo();
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    lockDemo.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(lockDemo.i);
    }
}
  1. CAS机制
    Compare and swap比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证。CAS操作需要输入两个数值,一个旧值A(期望操作前的值)和一个新值B,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才会交换成新值,发生变化了则不交换。java提供了compareAndSwapInt()和compareAndSwapLong()等几个方法实现CAS。


    1.png
//unsafe方式实现cas,但是这时只是说明原理,一般情况下不这么写,JUC提供有如下下一条代码
import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class LockDemo {
    //volatile也可没有
    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);
            valueOffset=unsafe.objectFieldOffset(LockDemo.class.getDeclaredField("value"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void add() {
        //CAS+循环重试,因为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 lockDemo = new LockDemo();
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    lockDemo.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(lockDemo.value);
    }
}
  1. JUC包内的原子操作封装类
  • AtomicInteger:原子更新整型

  • AtomicLong:原子更新长整型

  • AtomicBoolean:原子更新布尔类型

  • AtomicIntegerArray:原子更新整型数组里的元素

  • AtomicLongArray:原子更新长整型数组里的元素

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

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器

  • AtomicLongFieldUpdater:原子更新长整型的字段的更新器

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

  • AtomicReference:原子更新引用类型

  • AtomicStampedReference:原子更新带有版本号引用类型

  • AtomicmARKABLEReference:原子更新带有标记位引用类型

public class LockDemo {
     AtomicInteger value = new AtomicInteger(0);
    public void add() {
        //其实内部就是上面unsafe的实现,可跟踪代码查看
        value.incrementAndGet();
    }

    public static void main(String[] args) throws InterruptedException {
        LockDemo lockDemo = new LockDemo();
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    lockDemo.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(lockDemo.value);
    }
}
  1. 1.8更新
  • 更新器:DoubleAccumulator、LongAccumulator
  • 计数器:DoubleAdder、LongAdder
  • 计数器增强版,高并发下性能更好
    频繁更细但不太频繁读取的汇总统计信息时使用,分成多个操作单元,不同线程更新不同的单元
    只有需要汇总(sum()方法)的时候才计算所有单元的操作
2.png
public class LongAdderDemo {
    public  LongAdder longAdder=new LongAdder();
    public static void main(String[] args) {
        LongAdderDemo longAdderDemo=new LongAdderDemo();
        longAdderDemo.testLongAddr();
    }

    public void testLongAddr() {
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                long starttime=System.currentTimeMillis();
                while (System.currentTimeMillis() - starttime < 2000) {
                    longAdder.increment();
                }
                long endtime=System.currentTimeMillis();
                System.out.println(endtime-starttime+":"+longAdder.sum());
            }).start();
        }
    }
}

如果写其他同步,或者1.8前cas写法,会发现现在的方法效率更高

//增强计算版本,可以自定义计算
//此处就是取得最大值输出999
public class LongAdderDemo {
    public static void main(String[] args) throws InterruptedException {
        LongAccumulator accumulator=new LongAccumulator(new LongBinaryOperator() {
            @Override
            public long applyAsLong(long left, long right) {
                //返回最大值,这里就是自定义计算,根据自己需求可以修改
                return left>right?left:right;
            }
        },0);
        for (int i = 0; i < 1000; i++) {
            int finall=i;
            new Thread(()->{
                //此处实际是执行上面的自定义操作
                accumulator.accumulate(finall);
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(accumulator.longValue());//999
    }
}
  1. CAS的三个问题
  • 循环+CAS:自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的CPU资源消耗
  • 仅针对单个变量的操作,不能用于多个变量来实现原子操作
  • ABA问题:如果只是简单的数据加减等,ABA实际没有任何问题,但是如果针对复杂操作,如果通过判断变量出现ABA问题会导致很多问题;甚至有些是监听变化来触发操作,虽然可能多线程,但是实际上可能出现单CPU执行,如果单CPU则即使多线程同一时间也只有一个执行,如果比较巧合的话,刚好需要监听的线程因为ABA不知道实际被监听的已经变化了,则ABA问题也可能导致没法触发


    3.png

相关文章

  • 8.线程安全之原子性

    1.竞态条件与临界区 多个线程访问了相同的资源,向这些资源做了写操作时,对执行顺序有要求。 临界区:incr方法内...

  • 多线程之线程安全性

    多线程环境下使用非线程安全类会导致线程安全问题。线程安全问题表现为原子性,有序性,可见性 在讲述线程安全三大特性之...

  • 线程安全之原子性

    一:非原子性的原因 先举个栗子: 上述例子中,可以看到,for循环下建立3个线程分别调用value++操作,每个线...

  • ios 原子性和非原子性

    原子性和非原子行相对于线程的安全来讲 nonatomic:非原子属性,线程不安全的,效率高 atomic:原子属性...

  • iOS中属性关键词

    1.原子性与非原子性 atomic:原子性,只有一个线程可以同时访问实例。atomic 是线程安全的(因为会为se...

  • 原子属性

    非原子属性nonatomic 和原子属性atomic 原子属性atomic:就是为了保证这个属性的安全性(线程安全...

  • 分布式事务

    1、线程安全保证线程安全一般分成两种方式:锁和原子变量原子变量:原子变量能够保证原子性的操作,意思是某个任务在执行...

  • atomic不能足够安全(effective objective

    atomic使用了原子性,保证了线程安全,事实真的是这样吗?nonatomic的内存管理语义是非原子性的,非原子性...

  • Java-可见性、原子性、有序性

    关键字:Java内存模型(JMM)、线程安全、可见性、原子性、有序性 1.线程安全(JMM) 多线程执行某个操作的...

  • [Objective-C基础]- Objective-C 中,a

    atomic使用了原子性,保证了线程安全,事实真的是这样吗? nonatomic的内存管理语义是非原子性的,非原子...

网友评论

      本文标题:8.线程安全之原子性

      本文链接:https://www.haomeiwen.com/subject/drchlhtx.html