为了实现线程安全,我们前面都是用锁的方式来保证原子性,那么有不加锁能不能实现线程安全呢?这要从乐观锁和悲观锁说起。
1. 悲观锁。所谓的悲观锁,就是对资源的访问,默认情况下是认为会存在资源抢占,所以每次都要加锁,只能有一个线程执行。
CAS(Compare And Swap)比较替换,意思是
用一个期望值与当前值比较,如果相等,则用一个新的值替换当前变量值;如果不相等则放弃操作。
CAS 的ABA问题
假设线程1读取的时候变量值为A,此时线程2改变了变量值为B,然后又改回A。当线程1对比的时候发现变量值还是为A,则认为变量没有被其他线程修改过(事实上已经修改了A->B->A)。解决办法为在变量前面加上版本号,那么A->B->A就会变为1A->2B->3A。
CAS操作必须是原子操作,也就是比较-交换这整个过程是一个原子操作,不可分割,这需要处理器提供对应的指令集来实现。JDK中提供了一个Unsafe类,该类中的compareAndSwapXXX方法负责调用本地方法来实现CAS的原子操作。
在前面Java并发编程系列(一)—-深入剖析volatile关键字中分析了,下面的increaseAndGet()是没有无法实现原子操作的。
package com.rancho945.concurrent;
public class Counter {
public int count = 0;
public int increaseAndGet() {
return count++;
}
}
如果要实现原子操作,那么我么就必须对其进行加锁。JDK提供了一些原子类,可以实现上述功能的原子操作,并且没有加锁,先以AtomicInteger为例子,与上面的Counter类进行对比
package com.rancho945.concurrent;
import java.util.concurrent.atomic.AtomicInteger;
public class Test {
public static void main(String[] args) {
final AtomicInteger integer = new AtomicInteger();
final Counter counter = new Counter();
for (int i = 0; i < 20; i++) {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
for (int j = 0; j < 100000; j++) {
integer.incrementAndGet();
counter.increaseAndGet();
}
}
}).start();
}
while(Thread.activeCount()>1){
Thread.yield();
}
System.out.println("AtomicInteger---"+integer.get());
System.out.println("Counter---"+counter.count);
}
}
执行结果
AtomicInteger---2000000
Counter---1389131
可以看到原子类的实现了线程安全,而我们自己没有加锁的却没有实现线程安全。
那么AtomicInteger是怎么实现线程安全的呢?我们看看AtomicInteger的源码
//这个是JDK提供的CAS操作工具类
private static final Unsafe unsafe = Unsafe.getUnsafe();
//这个用于标记变量的偏移量
private static final long valueOffset;
//这个是int值
private volatile int value;
在类加载的时候执行的代码块:
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
这里的意思是获取value成员变量在对象内存地址的偏移量,使用valueOffset标记value的位置。在CAS中会使用到。
然后看看incrementAndGet方法:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
看看get方法:
public final int get() {
return value;
}
没什么好说的,再看看compareAndSet()
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
这里我们看到,使用的是unsafe.compareAndSwapInt,改方法负责调用jni本地方法实现CAS原子操作,这里的第一个参数传入的是当前对象,第二个参数就是前面静态代码块获取到的偏移量,第三个是期望值,如果期望值和valueOffset偏移量地址里内容一致,这把valueOffset地址(注意这里是valueOffset偏移量地址里的值而不是valueOffset本身的值,因为valueOffset是final的)里的内容更新成update的值,返回true;否则不更新,返回false。
过程就是先获取value的值,再加1,然后进行CAS操作,如果在读取value值和加1的过程中value的值被其他线程改变了,那么CAS失败,一直循环到成功为止。至于其他getAndAdd之类的方法也都差不多,读着可以自行分析。
synchronized加锁解锁是一个相对比较耗时间的过程,在单线程或者比较低或者说一般的并发环境下,CAS性能要优于synchronized。但是在非常高的并发环境下,如果对同一个资源竞争很激烈,CAS失败的情况就会很多。比如原子类的原子操作方法的for(;;)循环重试次数增多,消耗的CPU时间片也会相应的增加。
值得注意的是高并发环境不意味着对同一个资源竞争激烈,比如有100个线程,竞争同一个资源的线程只有几个,所以不意味着线程多使用synchronized就一定有优势,在通常情况下CAS都要优于synchronized。
另外,synchronized加锁会使等待锁的其他线程挂起,如果持有锁的线程阻塞,那么其他线程只能干巴巴地等待,CAS可以避免这个问题。
- 程序中的乐观锁与悲观锁,以及动手实现乐观锁 http://www.cnblogs.com/qinggege/p/5284750.html
- Java并发编程之CAS http://ifeve.com/compare-and-swap/
- 聊聊并发(五)原子操作的实现原理 http://ifeve.com/atomic-operation/
- unsafe.objectFieldOffset是什么? http://hllvm.group.iteye.com/group/topic/37940
- 《Java并发编程实战》 Brian Goetz等著 童云兰等译