Java Concurrency(二)——J.U.C atomic包源码解读

java5之后的java.util.concurrent包(J.U.C)是世界级并发大师Doug Lea的作品,里面主要实现了

  1. atomic包里Integer/Long对应的原子类,主要基于CAS;
  2. 一些同步子,包括Lock,CountDownLatch,Semaphore,FutureTask等,这些都是基于AbstractQueuedSynchronizer类;
  3. 关于线程执行的Executors类等;
  4. 一些并发的集合类,比如ConcurrentHashMap,ConcurrentLinkedQueue,CopyOnWriteArrayList等。
    今天我们主要介绍atomic包下相关内容。

CAS

atomic包下的类主要基于现代主流 CPU 都支持的一种指令,Compare and Swap(CAS),这个指令能为多线程编程带来更好的性能。引用《Java Concurrency in Practice》里的一段描述:

在这里,CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。简单介绍一下这个指令的操作过程:首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。最后,CPU 会将旧的数值返回。这一系列的操作是原子的。它们虽然看似复杂,但却是 Java 5 并发机制优于原有锁机制的根本。简单来说,CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少”。

AtomicInteger

private volatile int value;

AtomicInteger里面只包含一个字段,用来记录当前值,定义为volatile是为了满足可见性

// setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
      try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
      } catch (Exception ex) { throw new Error(ex); }
    }

一开始定义了static变量Unsafe,AtomicInteger里面的方法都是对unsafe里面

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

方法的封装。
我们来看原子性的i++,

public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }

在一个无限循环里面,首先获取当前值,然后调用

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

unsafe.compareAndSwapInt(this, valueOffset, expect, update)的含义是把this对象里面valueOffset这个位置(即value值)跟expect比较,如果相等,则修改为update,返回true;如果不相等,说明在获取到current之后有其他线程修改过value的值,则重新来一遍,一直到修改成功为止。这里就可以看出,理论上来说,这个方法是有可能永远不能返回的,实际而言,当并发冲突很严重,反复compareAndSet(current, next)失败,有可能也需要花费很多时间。
AtomicInteger里面的其他方法,基本类似;其他类包括AtomicLong,AtomicReference等也是基本对Unsafe里面compareAndSet的一个封装。

Unsafe

前面可以看到Unsafe类在实现atomic的重要性。为什么有Unsafe这个class呢,基本原因是Java不允许代码直接操作内存,好处是更安全,一般不会出现内存泄露,因为有JVM的GC;坏处是有些底层调用执行不了。我的理解是,Unsafe就是这个java安全围城通向比如c++这个不安全外围的一道门,所以叫Unsafe嘛。Unsafe里面基本都是native,即通过JNI调用c/c++等代码。大部分是直接内存操作,以及后面会讲到的挂起唤醒线程等,包括park和unpark。
前面到

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

就不是java代码了,如果想看实现的话,需要下载OpenJDK源码,里面是c++代码调用汇编代码,blabla。我不建议大家再往下继续了,原因有几个,一是我们用java等高级语言的目的就是为了避免纠结复杂的底层细节,站在更高层的角度思考问题,而是java里面还有更多的问题等待你去解决呢,更多的知识可以学习!如果你说你已经把java完全掌握了,包括把jdk源码,tomcat、spring,xxxxx源码都看过了,实在没得看了,那我会说,多陪陪家人吧~除非你是JVM开发工程师,哦,那不好意思,大神,当我啥都没说。。。。为了完整性,我贴几个参考链接http://www.blogjava.net/mstar/archive/2013/04/24/398351.html, http://zl198751.iteye.com/blog/1848575.
那么如果获取Unsafe呢?Unsafe有一个static方法可以获取Unsafe实例,如下

public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass(2);
        if(var0.getClassLoader() != null) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

可是你如果在自己代码里使用,可以编译通过,但是运行时候报错。因为里面限制了调用getUnsafe()这个方法的类必须是启动类加载器Bootstrap Loader。所以如果想在自己代码里面调用Unsafe的话(强烈建议不要这样子做),可以用Java的反射来实现:

static class UnsafeSupport {
        private static Unsafe unsafe;

        static {
            Field field;
            try {
                // 由反编译Unsafe类获得的信息
                field = Unsafe.class.getDeclaredField("theUnsafe");
                field.setAccessible(true);
                // 获取静态属性,Unsafe在启动JVM时随rt.jar装载
                unsafe = (Unsafe) field.get(null);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        public static Unsafe getInstance() {
//            return Unsafe.getUnsafe();//没有用,只能native获取,否则会抛异常
            return unsafe;
        }
    }

获取到了Unsafe的实例之后,你照样可以自己实现Atomic类,再说一遍,强烈建议不要这样做!!!

CAS优点

Compare and Set 是一个非阻塞的算法,这是它的优势。因为使用的是CPU支持的指令,提供了比原有的并发机制更好的性能和伸缩性。可以认为一般情况下性能更好,并且也更容易使用(这才是关键啊)。

CAS缺点

ABA问题

CAS操作容易导致ABA问题,也就是在做a++之间,a可能被多个线程修改过了,只不过回到了最初的值,这时CAS会认为a的值没有变。a在外面逛了一圈回来,你能保证它没有做任何坏事,不能!!也许它讨闲,把b的值减了一下,把c的值加了一下等等,更有甚者如果a是一个对象,这个对象有可能是新创建出来的,a是一个引用呢情况又如何,所以这里面还是存在着很多问题的,解决ABA问题的方法有很多,可以考虑增加一个修改计数(版本号),只有修改计数不变的且a值不变的情况下才做a++,atomic包下有AtomicStampedReference类做这个事情,这和事务原子性处理有点类似!

循环时间长开销大

  1. 即使没有任何争用也会做一些无用功
  2. 如果冲突比较严重的话,可能导致多次修改失败,for循环时间很长,可能比同步还慢
    我在自己的电脑上用100个线程去修改一个共享变量,发现用AtomicInteger就比synchronized慢,但是都很快!所以还是那个建议,不要过早优化,不要纠结到底是1ms还是2ms,除非测试之后发现确实是性能瓶颈,然后再仔细看一下,是不是代码的使用有问题,要相信,能写到JDK里的代码,一般都不会有问题。一般不到一天几千万上亿的PV,应该是没啥问题的。而且JVM对synchronized做了很多优化,包括锁去除(Lock Elimination),轻量级锁,偏向锁等,所以写代码的时候首先还是主要考虑代码正确、清晰、可维护。

    只能保证一个共享变量的原子操作

    如果并发约束条件涉及到两个变量,就不能用两个原子变量来达到整体的原子性,还是得用同步。当然你也可以用一个变通的方法,定义一个class,里面包含约束条件涉及到的变量,然后用AtomicReference来实现原子性。

    总结

    atomic包下的类比如AtomicInteger实现原子性的方法主要是依靠现代主流 CPU 都支持的CAS指令,它是通过Unsafe类的native方法调用的。一般而言性能比用锁同步要好,但是都已经很好了,一般而言不会遇到性能问题,关键还是看它的语义是否满足使用要求,以及是否可以让代码更清新。

Refers

  1. http://my.oschina.net/lifany/blog/133513
  2. http://zl198751.iteye.com/blog/1848575
  3. http://blog.csdn.net/aesop_wubo/article/details/7537960
  4. http://my.oschina.net/u/177808/blog/166819
  5. http://www.blogjava.net/mstar/archive/2013/04/24/398351.html
  6. http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/
  7. http://zeroturnaround.com/rebellabs/dangerous-code-how-to-be-unsafe-with-java-classes-objects-in-memory/
  8. http://www.pwendell.com/2012/08/13/java-lock-free-deepdive.html
时间: 2024-10-02 00:24:38

Java Concurrency(二)——J.U.C atomic包源码解读的相关文章

Java学习-039-源码 jar 包的二次开发扩展实例(源码修改)

最近在使用已有的一些 jar 包时,发现有些 jar 包中的一些方法无法满足自己的一些需求,例如返回固定的格式,字符串处理等等,因而需要对原有 jar 文件中对应的 class 文件进行二次开发扩展,并重新打包文件,替换原有的 jar 文件,满足测试开发自身的需求. 下面以修改 eclipse 默认注释中的 ${date} 和 ${time} 对应的返回样式(如下图所示),进行实例说明. 整个二次开发的过程如下所示: 0.未修改之前,生成注释的日期.时间显示格式如下所示: 1.获取对应的 jar

《java.util.concurrent 包源码阅读》02 关于java.util.concurrent.atomic包

Aomic数据类型有四种类型:AomicBoolean, AomicInteger, AomicLong, 和AomicReferrence(针对Object的)以及它们的数组类型, 还有一个特殊的AomicStampedReferrence,它不是AomicReferrence的子类,而是利用AomicReferrence实现的一个储存引用和Integer组的扩展类 首先,所有原子操作都是依赖于sun.misc.Unsafe这个类,这个类底层是由C++实现的,利用指针来实现数据操作 关于CAS

《java.util.concurrent 包源码阅读》 结束语

<java.util.concurrent 包源码阅读>系列文章已经全部写完了.开始的几篇文章是根据自己的读书笔记整理出来的(当时只阅读了部分的源代码),后面的大部分都是一边读源代码代码,一边写文章. 由于水平有限,在阅读源代码的时候,分析得也比较浅显,也有很多地方自己也没有研究明白,文章有的地方显得语焉不详,只能请各位多多见谅了. 后面会继续写一些关于Java并发编程的文章,希望各位多多指教. 这里整理了一个简单的目录,包含了本系列所有文章的链接: <java.util.concurr

《java.util.concurrent 包源码阅读》04 ConcurrentMap

Java集合框架中的Map类型的数据结构是非线程安全,在多线程环境中使用时需要手动进行线程同步.因此在java.util.concurrent包中提供了一个线程安全版本的Map类型数据结构:ConcurrentMap.本篇文章主要关注ConcurrentMa接口以及它的Hash版本的实现ConcurrentHashMap. ConcurrentMap是Map接口的子接口 public interface ConcurrentMap<K, V> extends Map<K, V> 与

《java.util.concurrent 包源码阅读》13 线程池系列之ThreadPoolExecutor 第三部分

这一部分来说说线程池如何进行状态控制,即线程池的开启和关闭. 先来说说线程池的开启,这部分来看ThreadPoolExecutor构造方法: public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecut

《java.util.concurrent 包源码阅读》06 ArrayBlockingQueue

对于BlockingQueue的具体实现,主要关注的有两点:线程安全的实现和阻塞操作的实现.所以分析ArrayBlockingQueue也是基于这两点. 对于线程安全来说,所有的添加元素的方法和拿走元素的方法都会涉及到,我们通过分析offer方法和poll()方法就能看出线程安全是如何实现的. 首先来看offer方法 public boolean offer(E e) { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lo

《java.util.concurrent 包源码阅读》05 BlockingQueue

想必大家都很熟悉生产者-消费者队列,生产者负责添加元素到队列,如果队列已满则会进入阻塞状态直到有消费者拿走元素.相反,消费者负责从队列中拿走元素,如果队列为空则会进入阻塞状态直到有生产者添加元素到队列.BlockingQueue就是这么一个生产者-消费者队列. BlockingQueue是Queue的子接口 public interface BlockingQueue<E> extends Queue<E> BlockingQueue拿走元素时,如果队列为空,阻塞等待会有两种情况:

《java.util.concurrent 包源码阅读》14 线程池系列之ScheduledThreadPoolExecutor 第一部分

ScheduledThreadPoolExecutor是ThreadPoolExecutor的子类,同时实现了ScheduledExecutorService接口. public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService ScheduledThreadPoolExecutor的功能主要有两点:在固定的时间点执行(也可以认为是延迟执行),重复执行.

《java.util.concurrent 包源码阅读》10 线程池系列之AbstractExecutorService

AbstractExecutorService对ExecutorService的执行任务类型的方法提供了一个默认实现.这些方法包括submit,invokeAny和InvokeAll. 注意的是来自Executor接口的execute方法是未被实现,execute方法是整个体系的核心,所有的任务都是在这个方法里被真正执行的,因此该方法的不同实现会带来不同的执行策略.这个在后面分析ThreadPoolExecutor和ScheduledThreadPoolExecutor就能看出来. 首先来看su