Java中线程安全的加一(+1)操作的三种方式

1.锁分为乐观锁和悲观锁,悲观锁总是假设每次的临界区操作会产生冲突,如果多个线程同时需要访问临界区资源,就宁可牺牲性能让线程进行等待。而乐观锁,它会假设对资源的访问都是没有冲突的,所有的线程都可以在不停顿的状态下持续执行,如果遇到冲突,乐观锁采用的叫做比较交换(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就尝试当前操作直到没有冲突为止

2.锁的必要性:

引例:变量i = 1,线程A进行了i+1操作,线程B也进行了i+1操作,经过两次线程加法之后可能i等于2,并不一定是想象中的等于3。

1)直接进行并发操作

分析:下图时Java的内存模型,假设主内存中有i=1,假设线程A先执行,线程A从主内存中读取i = 1到本地内存A,并进行加一操作,在线程A将加一后的值从本地内存A写回到主内存A前,线程B从主内存读取了i的值到本地内存B,此时仍然为1,线程B对 本地内存B中的i = 1进行加一操作。然后线程A和线程B分别将本地内存中的2写回到主内存中,所以最后结果是 i 的值都为2。

2)为了解决该问题,首先想到变量 i 使用volatile修饰

如果将i定义为volatile,此时保证了 i 的可见性,即当线程A修改了变量i的之后,新值对其他线程是立即可见的。但是仍然会有问题。问题出在i+1这条语句不是原子操作,i+1包含了3个操作:从工作内存读取i的值,通过操作数栈进行加一,将值写回到工作内存。再分析一下上面的过程,假设主内存中有i = 1,假设线程A先执行,线程A从主内存中读取 i = 1到本地内存A,执行i+1操作的时候,先将i = 1取到操作数栈顶,此时线程二获取了CPU的执行权,线程B从主内存中取出 i = 1,并进行加一操作,假设加一操作成功了,此时本地内存B中的i的值为2,同时本地内存A中i的值也被刷新为2(可见性),然后线程A又获取CPU的执行权,继续进行i++的操作,但是注意刚刚的i=1被取到了操作数栈(操作数栈不会重新到本地内存A中取数据),所以继续加一后i的值为2,并把2写回本地内存A,最终结果也为2。所以只满足可见性还是不行,还需要满足原子性

3)解决方式一:通过synchronized关键字

使用synchronized可以保证可见行和原子性

Java内存模型中定义了lock和unlock两种操作:

lock(锁定):作用与主内存的变量,它把一个变量标识为一条线程独占的状态(可简单理解为:变量此时只能被一条线程使用)。

unlock(解锁):作用域主内存的变量,它把一个处于锁定状态的变量释放出来,释放出来后的变量才可以被其他线程锁定。

原子行的保证:lock和unlock间的变量是被线程独占的,Java中无法直接使用这两条指令,但是可以用字节码指令monitorenter和monitorexit来隐式的使用这两个操作,这两个字节码指令反映到Java代码中就是同步块——synchronized关键字,所以synchronized块间的对变量的操作具有原子性

可见性的保证:对一个变量执行unlock操作之前,必须先把此变量同步回主内存中

3)解决方式二:使用CAS操作来解决

首先介绍CAS基本原理:它包含三个参数CAS(V,E,N)。V表示要跟新的变量,E表示预期值,N表示新值。仅当V值等于E值是,才会将V的值设置N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。

将i定义为AtomicInteger类型: 

 static AtomicInteger i = new AtomicInteger(1);

此时下面方法就能保证线程安全,A线程和B线程都执行加一操作后结果为3.

i.incrementAndGet();        //当前值加一

原理分析:

AtomicInteger中保存了一个核心字段:

private volatile int value;

incrementAndGet源码:

public final int incrementAndGet() {
    for (;;) {
        int current = get();                    //第三句                   
        int next = current + 1;               
        if (compareAndSet(current, next))       //第五句
            return next;
    }
}
public final int get() {
        return value;
}

incrementAndGet()保证了原子性,上面说了,int next = current + 1包含了多步操作,首先从从工作内存取出current,对其加一,然后赋值给next(至少包含这三步,其实转成机器指令,有更多的操作),现在value的值为1,所以current = get() = 1,执行next = current + 1 = 2,此时对比期望值current的值(为1)和要更新的变量值(即value值),如果从第三句到第五句之间被别的线程改变了value的值,那么期望值不等于要更新的变量值,操作失败,继续执行for(;;),如果操作成功就将value值替换成next的值(进行了加一操作)。

对于compareAndSet函数的实现。

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

compareAndSwapInt()方法是一个native方法,第一个参数o为给定的对象,offset为对象内的偏移量(其实就是一个字段到对象头部的偏移量,通过这个偏移量可以快速定位到value值),expected表示期望值,如果要更新的值value等于期望值,那么就将x赋值给value。注意这里的比较和赋值都是使用的CAS原子指令(通过调用CPU底层指令)完成的,因为要保证这两步的原子行。

自己的理解:这里对比的时候要更新值并不是取得工作内存的值,而是从主存中取值,所以使用偏移量直接定位到对象中的字段地址处。

参考:

http://zl198751.iteye.com/blog/1848575

http://www.cnblogs.com/xrq730/p/4976007.html

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值