Java 并发编程(高级篇,狠狠收藏!)

302 阅读13分钟

一、前言

Java 并发编程实践中的话:

编写正确的程序并不容易,而编写正常的并发程序就更难了。相比于顺序执行的情况,多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作的顺序是不可预期的。

并发编程相比 Java 中其他知识点学习起来门槛相对较高,学习起来比较费劲,从而导致很多人望而却步;

而无论是职场面试和高并发高流量的系统的实现却还都离不开并发编程,从而导致能够真正掌握并发编程的人才成为市场比较迫切需求的。

本场 Chat 作为 Java 并发编程之美系列的高级篇之二,主要讲解内容如下:

  • rt.jar 中 Unsafe 类主要函数讲解, Unsafe 类提供了硬件级别的原子操作,可以安全的直接操作内存变量,其在 JUC 源码中被广泛的使用,了解其原理为研究 JUC 源码奠定了基础。

  • rt.jar 中 LockSupport 类主要函数讲解,LockSupport 是个工具类,主要作用是挂起和唤醒线程,是创建锁和其它同步类的基础,了解其原理为研究 JUC 中锁的实现奠定基础。

  • 讲解 JDK8 新增原子操作类 LongAdder 实现原理,并讲解 AtomicLong 的缺点是什么,LongAdder 是如何解决 AtomicLong 的缺点的,LongAdder 和 LongAccumulator 是什么关系?

  • JUC 并发包中并发组件 CopyOnWriteArrayList 的实现原理,CopyOnWriteArrayList 是如何通过写时拷贝实现并发安全的 List?

二、 Unsafe 类探究

JDK 的 rt.jar 包中的 Unsafe 类提供了硬件级别的原子操作,Unsafe 里面的方法都是 native 方法,通过使用 JNI 的方式来访问本地 C++ 实现库。下面我们看下 Unsafe 提供的几个主要方法以及编程时候如何使用 Unsafe 类做一些事情。

2.1 主要方法介绍

  • long objectFieldOffset(Field field) 方法

作用:返回指定的变量在所属类的内存偏移地址,偏移地址仅仅在该 Unsafe 函数中访问指定字段时候使用。如下代码使用 unsafe 获取AtomicLong 中变量 value 在 AtomicLong 对象中的内存偏移。

static {        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
}
  • int arrayBaseOffset(Class arrayClass) 方法
    获取数组中第一个元素的地址

  • int arrayIndexScale(Class arrayClass) 方法
    获取数组中单个元素占用的字节数

  • boolean compareAndSwapLong(Object obj, long offset, long expect, long update) 方法
    比较对象 obj 中偏移量为 offset 的变量的值是不是和 expect 相等,相等则使用 update 值更新,然后返回 true,否者返回 false

  • public native long getLongVolatile(Object obj, long offset) 方法
    获取对象 obj 中偏移量为 offset 的变量对应的 volatile 内存语义的值。

  • void putLongVolatile(Object obj, long offset, long value) 方法
    设置 obj 对象中内存偏移为 offset 的 long 型变量的值为 value,支持 volatile 内存语义。

  • void putOrderedLong(Object obj, long offset, long value) 方法
    设置 obj 对象中 offset 偏移地址对应的 long 型 field 的值为 value。这是有延迟的 putLongVolatile 方法,并不保证值修改对其它线程立刻可见。变量只有使用 volatile 修饰并且期望被意外修改的时候使用才有用。

  • void park(boolean isAbsolute, long time)
    阻塞当前线程,其中参数 isAbsolute 等于 false 时候,time 等于 0 表示一直阻塞,time 大于 0 表示等待指定的 time 后阻塞线程会被唤醒,这个 time 是个相对值,是个增量值,也就是相对当前时间累加 time 后当前线程就会被唤醒。

    如果 isAbsolute 等于 true,并且 time 大于 0 表示阻塞后到指定的时间点后会被唤醒,这里 time 是个绝对的时间,是某一个时间点换算为 ms 后的值。

    另外当其它线程调用了当前阻塞线程的 interrupt 方法中断了当前线程时候,当前线程也会返回,当其它线程调用了 unpark 方法并且把当前线程作为参数时候当前线程也会返回。

  • void unpark(Object thread)
    唤醒调用 park 后阻塞的线程,参数为需要唤醒的线程。

下面是 Jdk8 新增的方法,这里简单的列出 Long 类型操作的方法

  • long getAndSetLong(Object obj, long offset, long update) 方法
    获取对象 obj 中偏移量为 offset 的变量 volatile 语义的值,并设置变量 volatile 语义的值为 update。
public final long getAndSetLong(Object obj, long offset, long update) {    long l;
    do
    {
      l = getLongVolatile(obj, offset);//(1)
    } while (!compareAndSwapLong(obj, offset, l, update));    return l;
  }

从代码可知内部代码 (1) 处使用 getLongVolatile 获取当前变量的值,然后使用 CAS 原子操作进行设置新值,这里使用 while 循环是考虑到多个线程同时调用的情况 CAS 失败后需要自旋重试。

  • long getAndAddLong(Object obj, long offset, long addValue) 方法
    获取对象 obj 中偏移量为 offset 的变量 volatile 语义的值,并设置变量值为原始值 +addValue。
public final long getAndAddLong(Object obj, long offset, long addValue) {    long l;
    do
    {
      l = getLongVolatile(obj, offset);
    } while (!compareAndSwapLong(obj, offset, l, l + addValue));    return l;
  }

类似 getAndSetLong 的实现,只是这里使用CAS的时候使用了原始值+传递的增量参数 addValue 的值。

2.2 如何使用 Unsafe 类

看到 Unsafe 这个类如此牛叉,你肯定会忍不住撸下下面代码,期望能够使用 Unsafe 做点事情。

public class TestUnSafe {    //获取Unsafe的实例(2.2.1)
    static final Unsafe unsafe = Unsafe.getUnsafe();    //记录变量state在类TestUnSafe中的偏移值(2.2.2)
    static final long stateOffset;    //变量(2.2.3)
    private volatile long state=0;    static {        try {            //获取state变量在类TestUnSafe中的偏移值(2.2.4)
            stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));

        } catch (Exception ex) {

            System.out.println(ex.getLocalizedMessage());            throw new Error(ex);
        }

    }    public static void main(String[] args) {        //创建实例,并且设置state值为1(2.2.5)
        TestUnSafe test = new TestUnSafe();        //(2.2.6)
        Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
        System.out.println(sucess);

    }
}

如上代码(2.2.1)获取了 Unsafe 的一个实例,代码(2.2.3)创建了一个变量 state 初始化为 0。

代码(2.2.4)使用 unsafe.objectFieldOffset 获取 TestUnSafe 类里面的 state 变量在 TestUnSafe 对象里面的内存偏移量地址并保存到 stateOffset 变量。

代码(2.2.6)调用创建的 unsafe 实例的 compareAndSwapInt 方法,设置 test 对象的 state 变量的值,具体意思是如果 test 对象内存偏移量为 stateOffset 的 state 的变量为 0,则更新该值为 1。

运行上面代码我们期望会输出 true,然而执行后会输出如下结果:

640?wx_fmt=png

为研究其原因,必然要翻看 getUnsafe 代码,看看里面做了啥:

private static final Unsafe theUnsafe = new Unsafe(); public static Unsafe getUnsafe(){    //(2.2.7)
    Class localClass = Reflection.getCallerClass();   //(2.2.8)
    if (!VM.isSystemDomainLoader(localClass.getClassLoader())) {      throw new SecurityException("Unsafe");
    }    return theUnsafe;
}  //判断paramClassLoader是不是BootStrap类加载器(2.2.9)
 public static boolean isSystemDomainLoader(ClassLoader paramClassLoader) {    return paramClassLoader == null;
  }

代码(2.2.7)获取调用 getUnsafe 这个方法的对象的 Class 对象,这里是 TestUnSafe.class。

代码(2.2.8)判断是不是 Bootstrap 类加载器加载的 localClass,这里是看是不是 Bootstrap 加载器加载了 TestUnSafe.class。很明显由于 TestUnSafe.class 是使用 AppClassLoader 加载的,所以这里直接抛出了异常。

思考下,这里为何要有这个判断那?

我们知道 Unsafe 类是在 rt.jar 里面提供的,而 rt.jar 里面的类是使用 Bootstrap 类加载器加载的,而我们启动 main 函数所在的类是使用 AppClassLoader 加载的。

所以在 main 函数里面加载 Unsafe 类时候鉴于委托机制会委托给 Bootstrap 去加载 Unsafe 类。

如果没有代码(2.2.8)这鉴权,那么我们应用程序就可以随意使用 Unsafe 做事情了,而 Unsafe 类可以直接操作内存,是不安全的。

所以 JDK 开发组特意做了这个限制,不让开发人员在正规渠道下使用 Unsafe 类,而是在 rt.jar 里面的核心类里面使用 Unsafe 功能。

那么如果开发人员真的想要实例化 Unsafe 类,使用 Unsafe 的功能该如何做那?

方法有很多种,既然正规渠道访问不了,那么就玩点黑科技,使用万能的反射来获取 Unsafe 实例方法:

public class TestUnSafe {    static final Unsafe unsafe;    static final long stateOffset;    private volatile long state = 0;    static {        try {            // 反射获取 Unsafe 的成员变量 theUnsafe(2.2.10)
            Field field = Unsafe.class.getDeclaredField("theUnsafe");            // 设置为可存取(2.2.11)
            field.setAccessible(true);            // 获取该变量的值(2.2.12)
            unsafe = (Unsafe) field.get(null);            //获取 state 在 TestUnSafe 中的偏移量 (2.2.13)
            stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));

        } catch (Exception ex) {

            System.out.println(ex.getLocalizedMessage());            throw new Error(ex);
        }

    }    public static void main(String[] args) {

        TestUnSafe test = new TestUnSafe();
        Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
        System.out.println(sucess);

    }
}

如上代码通过代码(2.2.10),(2.2.11),(2.2.12)反射获取 unsafe 的实例,然后运行结果输出:

640?wx_fmt=png

三、LockSupport类探究

JDK 中的 rt.jar 里面的 LockSupport 是个工具类,主要作用是挂起和唤醒线程,它是创建锁和其它同步类的基础。

LockSupport 类与每个使用它的线程都会关联一个许可证,默认调用 LockSupport 类的方法的线程是不持有许可证的,LockSupport 内部使用 Unsafe 类实现,下面介绍下 LockSupport 内的几个主要函数:

  • void park() 方法
    如果调用 park() 的线程已经拿到了与 LockSupport 关联的许可证,则调用 LockSupport.park() 会马上返回,否者调用线程会被禁止参与线程的调度,也就是会被阻塞挂起。

如下代码,直接在 main 函数里面调用 park 方法,最终结果只会输出begin park!,然后当前线程会被挂起,这是因为默认下调用线程是不持有许可证的。

 public static void main( String[] args ) {
        System.out.println( "begin park!" );

        LockSupport.park();

        System.out.println( "end park!" );

    }

在其它线程调用 unpark(Thread thread) 方法并且当前线程作为参数时候,调用park方法被阻塞的线程会返回。

另外其它线程调用了阻塞线程的 interrupt() 方法,设置了中断标志时候或者由于线程的虚假唤醒原因后阻塞线程也会返回,所以调用 park() 最好也是用循环条件判断方式。

需要注意的是调用 park() 方法被阻塞的线程被其他线程中断后阻塞线程返回时候并不会抛出 InterruptedException 异常。

  • void unpark(Thread thread) 方法

    当一个线程调用了 unpark 时候,如果参数 thread 线程没有持有 thread 与 LockSupport 类关联的许可证,则让 thread 线程持有。

    如果 thread 之前调用了 park() 被挂起,则调用 unpark 后,该线程会被唤醒。

    如果 thread 之前没有调用 park,则调用 unPark 方法后,在调用 park() 方法,会立刻返回,上面代码修改如下:

 public static void main( String[] args ) {
        System.out.println( "begin park!" );        //使当前线程获取到许可证
        LockSupport.unpark(Thread.currentThread());        //再次调用park
        LockSupport.park();

        System.out.println( "end park!" );

    }

则会输出:
begin park!
end park!

下面再来看一个例子来加深对 park,unpark 的理解

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {            @Override
            public void run() {

                System.out.println("child thread begin park!");                // 调用park方法,挂起自己
                LockSupport.park();

                System.out.println("child thread unpark!");

            }
        });        //启动子线程
        thread.start();        //主线程休眠1S
        Thread.sleep(1000);

        System.out.println("main thread begin unpark!");        //调用unpark让thread线程持有许可证,然后park方法会返回
        LockSupport.unpark(thread);

    }

输出为:

child thread begin park!
main thread begin unpark!
child thread unpark!

上面代码首先创建了一个子线程 thread,启动后子线程调用 park 方法,由于默认子线程没有持有许可证,会把自己挂起。

主线程休眠 1s 为的是主线程在调用 unpark 方法前让子线程输出 child thread begin park! 并阻塞。

主线程然后执行 unpark 方法,参数为子线程,目的是让子线程持有许可证,然后子线程调用的 park 方法就返回了。

park 方法返回时候不会告诉你是因为何种原因返回,所以调用者需要根据之前是处于什么目前调用的 park 方法,再次检查条件是否满足,如果不满足的话还需要再次调用 park 方法。

例如,线程在返回时的中断状态,根据调用前后中断状态对比就可以判断是不是因为被中断才返回的。

为了说明调用 park 方法后的线程被中断后会返回,修改上面例子代码,删除 LockSupport.unpark(thread); 然后添加 thread.interrupt(); 代码如下:

 public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {            @Override
            public void run() {

                System.out.println("child thread begin park!");                // 调用park方法,挂起自己,只有被中断才会退出循环
                while (!Thread.currentThread().isInterrupted()) {
                    LockSupport.park();

                }

                System.out.println("child thread unpark!");

            }
        });        // 启动子线程
        thread.start();        // 主线程休眠1S
        Thread.sleep(1000);

        System.out.println("main thread begin unpark!");        // 中断子线程线程
        thread.interrupt();

    }

输出为:

child thread begin park!
main thread begin unpark!
child thread unpark!

如上代码也就是只有当子线程被中断后子线程才会运行结束,如果子线程不被中断,即使你调用 unPark(thread) 子线程也不会结束。

  • void parkNanos(long nanos)函数

和 park 类似,如果调用 park 的线程已经拿到了与 LockSupport 关联的许可证,则调用 LockSupport.park() 会马上返回,不同在于如果没有拿到许可调用线程会被挂起 nanos 时间后在返回。

park 还支持三个带有 blocker 参数的方法,当线程因为没有持有许可的情况下调用 park 被阻塞挂起时候,这个 blocker 对象会被记录到该线程内部。

使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调 getBlocker(Thread) 方法来获取该 blocker 对象的,所以 JDK 推荐我们使用带有 blocker 参数的 park 方法,并且 blocker 设置为 this,这样当内存 dump 排查问题时候就能知道是那个类被阻塞了。

例如下面代码:

public class TestPark {    public  void testPark(){
       LockSupport.park();//(1)

    }    public static void main(String[] args) {

        TestPark testPark = new TestPark();
        testPark.testPark();

    }

}

运行后使用 jstack pid 查看线程堆栈时候可以看到如下:

640?wx_fmt=png

修改 代码(1)为 LockSupport.park(this) 后运行在 jstack pid 结果为:

640?wx_fmt=png

可知使用带 blocker 的 park 方法后,线程堆栈可以提供更多有关阻塞对象的信息。

  • park(Object blocker) 函数
public static void park(Object blocker) {   //获取调用线程
    Thread t = Thread.currentThread();   //设置该线程的 blocker 变量
    setBlocker(t, blocker);    //挂起线程
    UNSAFE.park(false, 0L);   //线程被激活后清除 blocker 变量,因为一般都是线程阻塞时候才分析原因
    setBlocker(t, null);
}

Thread 类里面有个变量 volatile Object parkBlocker 用来存放 park 传递的 blocker 对象,也就是把 blocker 变量存放到了调用 park 方法的线程的成员变量里面。

  • void parkNanos(Object blocker, long nanos) 函数
    相比 park(Object blocker) 多了个超时时间。

  • void parkUntil(Object blocker, long deadline)
    parkUntil 的代码如下:

 public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);       //isAbsolute=true,time=deadline;表示到 deadline 时间时候后返回
        UNSAFE.park(true, deadline);
        setBlocker(t, null);
    }

可知是设置一个 deadline,时间单位为 milliseconds,是从 1970 到现在某一个时间点换算为毫秒后的值,这个和 parkNanos(Object blocker, long nanos) 区别是后者是从当前算等待 nanos 时间,而前者是指定一个时间点。

比如我需要等待到 2017.12.11 日 12:00:00,则吧这个时间点转换为从 1970 年到这个时间点的总毫秒数。

最后在看一个例子

class FIFOMutex {    private final AtomicBoolean locked = new AtomicBoolean(false);    private final Queue<Thread> waiters = new ConcurrentLinkedQueue<Thread>();    public void lock() {        boolean wasInterrupted = false;
        Thread current = Thread.currentThread();
        waiters.add(current);        // 只有队首的线程可以获取锁(1)
        while (waiters.peek() != current || !locked.compareAndSet(false, true)) {
            LockSupport.park(this);            if (Thread.interrupted()) // (2)
                wasInterrupted = true;
        }

        waiters.remove();        if (wasInterrupted) // (3)
            current.interrupt();
    }    public void unlock() {
        locked.set(false);
        LockSupport.unpark(waiters.peek());
    }
}

这是一个先进先出的锁,也就是只有队列首元素可以获取锁,代码(1)处如果当前线程不是队首或者当前锁已经被其它线程获取,则调用park方法挂起自己。

然后代码(2)处判断,如果 park 方法是因为被中断而返回,则忽略中断,并且重置中断标志,只做个标记,然后再次判断当前线程是不是队首元素或者当前锁是否已经被其它线程获取,如果是则继续调用 park 方法挂起自己。

然后代码(3)中如果标记为 true 则中断该线程,这个怎么理解那?其实意思是其它线程中断了该线程,虽然我对中断信号不感兴趣,忽略它,但是不代表其它线程对该标志不感兴趣,所以要恢复下。