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