java并发编程线程安全——原子性1

一、线程安全性

线程安全性:

定义——当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

 

原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作。

可见性:一个线程对主内存的修改可以及时的被其他线程观察到。

有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。

 

二、原子性

原子性——Atomic包

1,什么是CAS?

他们都是通过CAS来满足原子性的,那么什么事CAS?

CAS就是compare and swap,它解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。

 

不过单就这个定义,有种知其然不知其所以然的感觉,为啥CAS可以满足原子性的要求?它这种方式的思路是什么?网上查的这一段讲得不错:

 

CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。

CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

我们看一个例子:

1. 在内存地址V当中,存储着值为10的变量。

2. 此时线程1想把变量的值增加1.对线程1来说,旧的预期值A=10,要修改的新值B=11.

3. 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

4. 线程1开始提交更新,首先进行A和地址V的实际值比较,发现A不等于V的实际值,提交失败。

5. 线程1 重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

6. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行比较,发现A和地址V的实际值是相等的。

7. 线程1进行交换,把地址V的值替换为B,也就是12.

从上面的变更可以看出来,cas是通过比较内存地址V和线程中旧的预期值A来判断内存值是否被修改过,如果修改过就重新计算旧的预期值A,直到内存值和预期值一致才做修改,这样就实现了互斥的效果!

从思想上来说,synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。

乐观锁和悲观锁来复习下。java由并发可能产生3种并发问题:脏读(dirty read)、不可重复度(unrepeatable read)、幻读(phantom read)。

由此有四个对应级别来处理这三种并发现象:

1.read uncommitted,读未提交.导致了(脏读 + 不可重复 + 幻读)

2.read committed ,读已提交,避免了脏读,导致(不可重复 + 幻读)

4.repeatable read,可以重复读,避免了(脏读 + 不可重复读 + 幻读 mysql)

8.serializable(悲观锁).

悲观锁,是不允许并发,一个一个线程执行;乐观锁是支持并发的,但是会感知数据被篡改了。

 

 

在java中除了上面提到的Atomic系列类,以及Lock系列类夺得底层实现,甚至在JAVA1.6以上版本,synchronized转变为重量级锁之前,也会采用CAS机制。

CAS的缺点:

1) CPU开销过大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

2) 不能保证代码块的原子性

CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

3) ABA问题

这是CAS机制最大的问题所在。(后面有介绍)

所谓ABA问题,就是一个变量从A变成了B,然后又变回来了A,普通CAS会误判通过检测。

 

附上讲CAS的博文:

https://blog.csdn.net/qq_32998153/article/details/79529704

2,java中实现原子性的类

 

有前面的铺垫,其实就比较容易搞懂了。

java实现原子性有个专门的包——Atomic包

一般就叫做:AtomicXXX。实现原理也就是CAS,底层有个Unsafe.compareAndSwapInt

比如:

AtomicInteger 这个类,它的增加方法为:incrementAndGet (increment是增加的意思)

实现起来,调用了unsafe.getAndAddInt(this, valueOffset, 1) + 1;

而getAndAddInt方法如下:

 

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

compareAndSwapInt是native方法,原子类实际上采用CAS实现的。var1是当前传过来的对象,var2是当前的值,比如我要实现2+1=3的操作,var2=2,var3=1,var5调用底层的方法,用来确定它是底层当前的值。compareAndSwapInt(var1, var2, var5, var5 + var4)实现的就是底层的值(var5)和当前的值(var2)是相同的话,就把它更新成var5+var4,如果不一样的话,就循环执行。

 

3,AtomicLong和LongAddr

特别要说明下AtomicLong和LongAddr
AtomicLong和之前的讲得AtomicInteger很类似,底层实质上都是通过CAS来实现的。而LongAddr则实现得更为巧妙。
先来看看两个类方法上有何不同?
老实讲,看源码还是有点费力~~
这篇博文讲得不错:
https://blog.csdn.net/yao123long/article/details/63683991

单从方法来看,两者均有对应的操作方法。对于LongAddr,我们从它的自增方法看起,有两段代码比较重要:
(1)LongAddr的add方法
    public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }

读这段代码,要先从Cell对象看起,先看(2)


(2)Cell对象
    @sun.misc.Contended static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }

        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        private static final long valueOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

Cell 这个对象,实际上就是封装了一个volatile的long变量(volatile就是表示某人或某物是不稳定的、易变的),针对单个cell,也是有一个cas方法,然后更改这个变量唯一的方式通过cas。我们可以猜测到LongAdder的高明之处可能在于将之前单个节点的并发分散到各个节点的,这样从而提高在高并发时候的效率。

说明下volatile的作用:volatile作为java中的关键词之一,用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。volatile会禁止指令重排。

总结:
  LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。 
  缺点是LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值