Java线程安全-原子性

什么是原子性

如果把一个事务可看作是一个程序,它要么完整的被执行,要么完全不执行。这种特性就叫原子性

问题1

public class Counter {
    volatile int i = 0;

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

public class Demo1_CounterTest {

    public static void main(String[] args) throws InterruptedException {
        final Counter ct = new Counter ();
        // 开启10个线程对Counter对象中的i进行累加
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        ct.add();
                    }
                    System.out.println("done...");
                }
            }).start();
        }

        Thread.sleep(6000L);
        System.out.println(ct.i);
        // 无法达到预期输出结果 100000 
    }
}

以上代码虽然对实现了变量 i 的可见性,但是并没有实现对 i 的原子操作。其他线程都能读到 i ,但读取过后 i 又发生改变,就会导致读取的 i 是一个失效的值,从而引发原子性问题

原子操作

  • 原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中一部分。
  • 将整个操作视作为一个整体,资源在该次操作中保持一致,这是原子性的核心特征

解决原子性问题

  • Atomic 原子类
import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    // 使用原子类
    AtomicInteger i = new AtomicInteger(0);

    public void add() {
        i.incrementAndGet();
    }
}
  • synchronized 同步关键字
public class Counter {
    volatile int i = 0;
    // 加入同步关键字,即可实现线程安全同步锁,一个线程执行完了才能执行下一个线程
    public synchronized void add() {
        i++;
    }
}
  • Lock 锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    volatile int i = 0;
    // 加入锁,在同一时间只会有一个线程抢到锁
    Lock lock = new ReentrantLock();

    public void add() {
        lock.lock();
        try {
          i++;
        }finally {
          lock.unlock();
        }
    }
}

CAS(Compare and swap)

  • Compare and swap 比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证

  • CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先对旧值进行比较,若没有发生变化,才交换成新的值,发生了变化则不交换

  • Java中的sun.misc.Unsafe类,提供了compareAndSwaplnt() 和 compareAndSwapLong() 等几个方法实现CAS

cas操作原理,两个线程同时修改

CAS操作会用原来的值进行比较,比较后再设置新的值。如果发现比较的值不对,则本次操作失败。进入自旋(重新再执行)

模拟CAS操作
public class Counter {
    volatile int i = 0;

    private static Unsafe unsafe = null;
    private static long valueOffset;

    static {
        //unsafe = Unsafe.getUnsafe();  // JDK不提供,通过反射来实现
        try{
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            
            //获取i字段的offset
            Field iField = Counter.class.getDeclaredField("i");
            valueOffset = unsafe.objectFieldOffset(iField);

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }  
    }

    public void add() {
        while(true) {
            // 拿到i字段的当前值
            int current = unsafe.getIntVolatile(this, valueOffset);
            // 用CAS操作将i加1
            if (unsafe.compareAndSwapInt(this, valueOffset, current, current+1)) {
                break;
            }
        }
    }
}

AtomicInteger内部实现的就是CAS原子性操作


J.U.C包内的原子操作封装类

类名 解释
AtomicBoolean 原子更新布尔类型
AtomicInteger 原子更新整型
AtomicLong 原子更新长整型
AtomicIntegerArray 原子更新整型数组里的元素
AtomicLongArray 原子更新长整型数组里的元素
AtomicReferenceArray 原子更新引用类型数组里的元素
AtomicIntegerFieldUpdater 原子更新整型字段的更新器
AtomicLongFieldUpdater 原子更新长整型字段的更新器
AtomicReferenceFieldUpdater 原子更新引用类型的字段
AtomicReference 原子更新引用类型
AtomicStampedReference 原子更新带有版本号的引用类型
AtomicMarkableReference 原子更新带有标记位的引用类型
1.8更新:计数器增强版,高并发下性能更好 原理:分成多个单元,不同线程更新不同的单元,只有需要汇总的时候才计算所有单元的操作
DoubleAccumulator 更新器
LongAccumulator 更新器
DoubleAdder 计数器
LongAdder 计数器

LongAccumulator示例

import java.util.concurrent.atomic.LongAccumulator;

public class Demo_LongAccumulator {
    public static void main(String[] args) throws InterruptedException {  
        /*
          LongAccumulator 可以帮我们做到自定义的累加计算
          第一个参数为lambda方法,y表示状态值,x表示每次传入的值
          第二个参数为状态值,(这里传入0,则y一开始等于0)
        */
        LongAccumulator accumulator = new LongAccumulator(
                (y,x)->{
                    System.out.println("x:" + x + ",y:" + y);
                    // 自定义累加逻辑 (这里为:x + y)
                    return  x + y; 
                },
                0L);

        for (int i = 0; i < 3; i++) {
            // 把1传入到 LongAccumulator 中
            accumulator.accumulate(1);
        }

        System.out.println("result=" + accumulator.get());
    }
}

LongAdder示例

import java.util.concurrent.atomic.LongAdder;

public class Demo_LongAdder {
    public static void main(String[] args) throws InterruptedException {  
        /*
          LongAdder类与AtomicLong类的区别在于
          高并发时前者将对单一变量的CAS操作
          分散为对数组cells中多个元素的CAS操作,取值时进行求和;        
          而在并发较低时仅对base变量进行CAS操作,与AtomicLong类原理相同 
        */
        LongAdder lacount = new LongAdder();
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                long starttime = System.currentTimeMillis();
                while (System.currentTimeMillis() - starttime < 2000) { // 运行两秒
                    //increment()方法就是对add(long x)的封装
                    lacount.increment();
                }
                long endtime = System.currentTimeMillis();
            }).start();
        }

        Thread.sleep(3000);  
        // 返回的是base和cells数组中所有元素的和,这里的base像是一个初始值的作用
        System.out.println(lacount.sum());
    }
}


CAS的三个问题

  1. 循环+CAS,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果长时间不成功,会带来很大的CPU资源消耗
  2. 仅针对单个变量的操作,不能用于多个变量来实现原子操作
  3. ABA问题。

ABA问题

ABA问题
  1. 线程1、线程2同时读取到i=0后
  2. 线程1、线程2都要执行CAS操作
  3. 假设线程2操作稍后于线程1、则线程1执行成功,线程2执行失败
  4. 但紧接着线程1又执行了CAS(1,0),将i的值改回0,此时线程2执行,引发ABA问题

AtomicStampedReference 加入版本号的数据比较更新

// 存储在栈里面元素 -- 对象
public class Node {
    public final String value;
    public Node next;

    public Node(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "value=" + value;
    }
}
import java.util.concurrent.atomic.AtomicStampedReference;
import java.util.concurrent.locks.LockSupport;

public class ConcurrentStack {
    // top cas无锁修改
    //AtomicReference<Node> top = new AtomicReference<Node>();
    AtomicStampedReference<Node> top =
            new AtomicStampedReference<>(null, 0);

    public void push(Node node) { // 入栈
        Node oldTop;
        int v;
        do {
            v = top.getStamp();
            oldTop = top.getReference();
            node.next = oldTop;
        }
        while (!top.compareAndSet(oldTop, node, v, v+1)); // CAS 替换栈顶
    }


    // 出栈 -- 取出栈顶 ,为了演示ABA效果, 增加一个CAS操作的延时
    public Node pop(int time) {

        Node newTop;
        Node oldTop;
        int v;

        do {
            v = top.getStamp();
            oldTop = top.getReference();
            if (oldTop == null) {   //如果没有值,就返回null
                return null;
            }
            newTop = oldTop.next;
            if (time != 0) {    //模拟延时
                LockSupport.parkNanos(1000 * 1000 * time); // 休眠指定的时间
            }
        }
        while (!top.compareAndSet(oldTop, newTop, v, v+1));     //将下一个节点设置为top
        return oldTop;      //将旧的Top作为值返回
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容