天天看点

unsafe类在java中的应用举例:原子计数器的实现

前言

今天看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