为了实现线程安全,我们前面都是用锁的方式来保证原子性,那么有不加锁能不能实现线程安全呢?这要从乐观锁和悲观锁说起。 1. 悲观锁。所谓的悲观锁,就是对资源的访问,默认情况下是认为会存在资源抢占,所以每次都要加锁,只能有一个线程执行。
乐观锁。另外一种锁的策略,默认资源不存在竞争,多个线程可以同时操作,在最后进行更新数据的时候,查看该资源是否被其他线程修改过,没有则执行更新,有则放弃本次操作。CAS(Compare And Swap)比较替换,意思是 用一个期望值与当前值比较,如果相等,则用一个新的值替换当前变量值;如果不相等则放弃操作。
假设线程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---2000000Counter---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可以避免这个问题。
新闻热点
疑难解答