前言
今天看java并发编程实践时,看到了线程安全这一块,讲到了java自带的一个原子计数器,AtomicInteger 。我就很好奇它是怎么实现线程安全的。我查询了一下源码:
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the updated value
*/
public final int addAndGet(int delta) {
for (;;) {
int current = get();
int next = current + delta;
if (compareAndSet(current, next))
return next;
}
}
这个代码很简单,但是有一个方法 compareAndSet(current,next),这是什么东东?恩,再往下找~
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return true if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
unsafe是什么东东,当我再次点击查询源码时,出现source not found。原来这是sun包下的。我就bing了一下 unsafe.compareAndSwapInt。找到了一篇好文:Java Magic. Part 4: sun.misc.Unsafe 。看完之后,感觉发现了新大陆。以下讲的所有都是参考这个文章。
unsafe类简介
java是一个安全的编程语言,它帮助程序猿避免犯一些愚蠢的错误,这些错误一般都是基于内存管理。但是,如果你就是想故意的犯一些这样的错误,使用Unsafe类就对了。
Unsafe初始化
在使用前,我们需要创建Unsafe类的一个实例。创建Unsafe实例不能像创建普通类实例new一下就行,因为Unsafe类的构造方法是私有化的。它虽然有静态的方法getUnsafe(),但是调用Unsafe.getUnsafe()这个方法时,有可能出现安全异常 SecurityException.
比较简单的创建Unsafe类实例的方法
Unsafe类包含一个实例theUnsafe,它是private私有化的。我们可以通过反射的机制把它偷出来:
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
并发
Unsafe.compareAndSwap方法是原子的并且可以用来实现高性能不使用锁的数据结构。
举例,在多线程中使用共享对象来增加值。
首先我们定义一个简单的接口:Counter
interface Counter {
void increment();
long getCounter();
}
然后我们定义一个工作线程CounterClient来使用Counter:
class CounterClient implements Runnable {
private Counter c;
private int num;
public CounterClient(Counter c, int num) {
this.c = c;
this.num = num;
}
@Override
public void run() {
for (int i = 0; i < num; i++) {
c.increment();
}
}
}
下面是测试代码:
int NUM_OF_THREADS = 1000;
int NUM_OF_INCREMENTS = 100000;
ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);
Counter counter = ... // creating instance of specific counter
long before = System.currentTimeMillis();
for (int i = 0; i < NUM_OF_THREADS; i++) {
service.submit(new CounterClient(counter, NUM_OF_INCREMENTS));
}
service.shutdown();
service.awaitTermination(1, TimeUnit.MINUTES);
long after = System.currentTimeMillis();
System.out.println("Counter result: " + c.getCounter());
System.out.println("Time passed in ms:" + (after - before));
首先实现一个非同步的计数器:
class StupidCounter implements Counter {
private long counter = 0;
@Override
public void increment() {
counter++;
}
@Override
public long getCounter() {
return counter;
}
}
输出:
Counter result: 99542945
Time passed in ms: 679
程序运行时间非常短,但是没有线程管理,所以结果有问题。第二种尝试,使用简单的java方式的同步:
class SyncCounter implements Counter {
private long counter = 0;
@Override
public synchronized void increment() {
counter++;
}
@Override
public long getCounter() {
return counter;
}
}
输出:
Counter result: 100000000
Time passed in ms: 10136
激进的同步做法总是有效的,但是消耗时间非常长。我们接着试一下使用读写锁:
class LockCounter implements Counter {
private long counter = 0;
private WriteLock lock = new ReentrantReadWriteLock().writeLock();
@Override
public void increment() {
lock.lock();
counter++;
lock.unlock();
}
@Override
public long getCounter() {
return counter;
}
}
输出:
Counter result: 100000000
Time passed in ms: 8065
结果依然正确,耗时也比较短。使用原子类怎么样?
class AtomicCounter implements Counter {
AtomicLong counter = new AtomicLong(0);
@Override
public void increment() {
counter.incrementAndGet();
}
@Override
public long getCounter() {
return counter.get();
}
}
输出:
Counter result: 100000000
Time passed in ms: 6552
AtomicCounter甚至更好。
最后,重头戏来了,我们试一下使用Unsafe原生的compareAndSwapLong来实现计数器。
public class CASCounter implements Counter{
private volatile long counter = 0;
private Unsafe unsafe;
private long offset;
public CASCounter() throws Exception {
unsafe = getUnsafe();
offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
}
public Unsafe getUnsafe() {
Field f;
try {
f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
@Override
public void increment() {
long before = counter;
while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
before = counter;
}
}
@Override
public long getCounter() {
return counter;
}
}
输出:
Counter result: 100000000
Time passed in ms: 6454
和原子实现差不多。是不是原子方式就是用Unsafe实现的?YES
事实上这个例子很简单,但是展示出Unsafe一些能力。
就像之前说的,CAS原生可以用作实现不使用锁的数据结构。实现原理很简单:
- 拥有一些状态
- 创建一个副本
- 修改它
- 执行CAS
- 如果失败重复执行
老实说,在实际应用中,你遇到的困难超乎你的想象。比如很多类型ABA Problem。
如果你真的感兴趣,可以参考一下lock-free HashMap的精彩实现。
note: 在counter变量前增加volatile关键字为了避免死循环。
总结
即使,Unsafe很有意思,但千万不要使用。
另外,我之前说想看一下 Unsafe原生的compareAndSwapLong 源码,我查完知道了这个方法使用c写的... -_-||
个人感受
并发程序开发的确非常考验技术,比如怎么避免使用锁,锁会增加程序的运行时间,如果使用不好,会出现死锁,导致程序卡死。我实际工作中就遇到过死锁的问题:
我写的是一个用spring实现一个简单的调度程序,其中有一个调度的功能是从远程服务器拉文件到本地服务器,定时器频率是5分钟一次。部署上线后,总是隔三差五的程序假死:查询程序进程id存在,但是查看业务日志,就是没有打印。找了好几天,才定位到问题:
1 由于网络环境不稳定,从服务器拉文件这个动作可能会消耗很长时间。
2 代码中控制拉文件的一些参数:连接时间,读取时间,超时时间没有起作用。
3 拉文件这个方法加锁了。
这样问题就明晰了:
当定时器启动一个线程执行拉文件操作时,耗时非常长,过了五分钟之后,定时器又启动了另一个线程,因为拉文件操作加锁了,这个线程只能等待...随着时间的流逝,定时器起的线程全部在等待第一个线程释放锁,无法再新建线程执行其他的调度任务,所以程序假死。
解决方案很简单:
修复了代码中拉文件的参数,保证五分钟内 要么拉完文件,要么失败,中断网络连接,释放锁。
转载于:https://my.oschina.net/huaxiaoqiang/blog/2051483