Coder Social home page Coder Social logo

sayi.github.com's Introduction

sayi.github.com's People

Contributors

sayi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

sayi.github.com's Issues

并发(三)基础之底层技术篇

本文主要针对一些基础算法和底层类(LockSupport)作一个介绍,很多并发的高级特性都是依赖于这些技术。

位操作

位操作作为一个编程技巧或者用二进制思维解决算术问题在许多源码中都出现过,我们不妨回顾一下Java中的位操作。

移位操作

  • 左移<<:符号位不移动,低位补0,高位丢弃。相当于乘以2的N次方
  • 右移>>:符号位不移动,低位丢弃,高位补0。相当于除以2的N次方
  • 无符号右移>>>:最高一位为符号位,也会跟着右移,低位丢弃,高位补0。

位运算

  • 与&:同时为1,才为1,否则为0。
  • 或|:有一个为1则为1,否则为0。
  • 非~:1为0,0为1,相反。
  • 抑或^:相同为1,相反为0。

示例

基础知识读起来很简单,具体拿来用的时候还是需要掌握一些技巧,我们以一个整型变量为例,无符号的高16位和无符号的低16位分别存储两个整数。

所以第一个问题就是怎么通过位操作获得高16位和低16位的值?可以通过无符号右移16位获得高16位的值,获得低16位就要想办法把高16位变为0,我们可能通过与操作,掩码是0000 0000 0000 0000 1111 1111 1111 1111。

int a = 10;
// 无符号的高16位值
System.out.println(a >>> 16);

// 1111 1111 1111 1111
int mask = (1 << 16) - 1;
// 无符号的低16位值
System.out.println(a & mask);

这两个无符号整数该怎么加一呢?对于低16位直接+1,上线是2^16-1=65535,对于高16位,+1其实就是增加2^16=65536。

// 高位加1
System.out.println((a + (1 << 16)) >>> 16);
// 低位加1
System.out.println((a + 1 ) & mask);

Atomic*、CAS

JDK提供了一些原子类,它让我们避免使用synchronized关键字来解决同步问题,我们以AtomicInteger源码为例来介绍其中的核心方法。

我们先来看看声明的一些属性:

public class AtomicInteger extends Number implements java.io.Serializable {

  // 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); }
  }

  private volatile int value;

sun.misc.Unsafe通过内存地址偏移量来操作数据,关于Unsafe参见国外的这一篇文章:

Java Magic. Part 4: sun.misc.Unsafe:http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/

volatile int value 正是存储这个整型变量的值,我们知道volatile声明的变量的读和写都是原子的,并且是可见的,所以不会存在内存一致性问题,我们来看看没有任何多余动作的读和写:

public final int get() {
  return value;
}

public final void set(int newValue) {
  value = newValue;
}

AtomicInteger类的作用就是除了读写之外,提供更多的关于整型的原子操作,比如累加操作:

 /**
 * 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 {@code 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);
}
public final int getAndIncrement() {
  return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndDecrement() {
  return unsafe.getAndAddInt(this, valueOffset, -1);
}
public final int getAndAdd(int delta) {
  return unsafe.getAndAddInt(this, valueOffset, delta);
}
public final int incrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndSet(int newValue) {
  return unsafe.getAndSetInt(this, valueOffset, newValue);
}
public final int decrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}

我们注意到其中有一段特别关键的代码:unsafe.compareAndSwapInt,类似的还有unsafe.compareAndSwapObject等,这就不得不说一种很重要的实现无锁同步的机制:Compare And Swap,即CAS。

CAS是当更新某个V的值时,会比较是否和期望值相同,当且仅当它们相同则更新,否则返回V的最新值,这就有效的防止了在我取得期望值后,V的值被其它线程修改,可以理解成为乐观锁,所有操作都可以执行,但是当更新时如果发现被其它线程更新过,则不允许执行更新。因为1.8用了Unsafe这个类,我们不妨来看看1.7中AtomicInteger的getAndIncrement方法:

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);
}

for循环给出了一个使用CAS很好的示例,不断比较期望值与获取的值是否相同,不相同,则重新获取进而操作。

在使用CAS时,会带来新的问题:ABA问题,即V的值从A修改成B,再修改成A,所以CAS不会察觉到这个变量被修改过,虽然大多数情况下这都不会带来什么问题,但是我们也可以通过记录这个变量的修改次数来解决这个问题。

LockSupport

我们已经知道,Object.wait和synchronized都可以产生阻塞,这些实现都是JVM层面,JDK提供了基础线程阻塞原语LockSupport,它更底层并且能创造出更高级的同步器和锁,接下来我们就来看看LockSupport的源码。

public class LockSupport {
  private LockSupport() {} // Cannot be instantiated.

  private static void setBlocker(Thread t, Object arg) {
    // Even though volatile, hotspot doesn't need a write barrier here.
    UNSAFE.putObject(t, parkBlockerOffset, arg);
  }

  public static void unpark(Thread thread) {
    if (thread != null)
      UNSAFE.unpark(thread);
  }

  public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
  }

  public static void parkNanos(Object blocker, long nanos) {
    if (nanos > 0) {
      Thread t = Thread.currentThread();
      setBlocker(t, blocker);
      UNSAFE.park(false, nanos);
      setBlocker(t, null);
    }
  }

  public static void parkUntil(Object blocker, long deadline) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(true, deadline);
    setBlocker(t, null);
  }

  public static Object getBlocker(Thread t) {
    if (t == null)
      throw new NullPointerException();
    return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
  }

  public static void park() {
    UNSAFE.park(false, 0L);
  }

  public static void parkNanos(long nanos) {
    if (nanos > 0)
      UNSAFE.park(false, nanos);
  }

  public static void parkUntil(long deadline) {
    UNSAFE.park(true, deadline);
  }

  // Hotspot implementation via intrinsics API
  private static final sun.misc.Unsafe UNSAFE;
  private static final long parkBlockerOffset;
  private static final long SEED;
  private static final long PROBE;
  private static final long SECONDARY;
  static {
    try {
      UNSAFE = sun.misc.Unsafe.getUnsafe();
      Class<?> tk = Thread.class;
      parkBlockerOffset = UNSAFE.objectFieldOffset
        (tk.getDeclaredField("parkBlocker"));
      SEED = UNSAFE.objectFieldOffset
        (tk.getDeclaredField("threadLocalRandomSeed"));
      PROBE = UNSAFE.objectFieldOffset
        (tk.getDeclaredField("threadLocalRandomProbe"));
      SECONDARY = UNSAFE.objectFieldOffset
        (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
    } catch (Exception ex) { throw new Error(ex); }
  }
}

从源码中我们看到很重要的几个方法:

  • park(Object blocker) 提供了阻塞当前线程的操作
  • unpark(Thread thread) 提供了唤醒某个线程的操作

park和unpark需要配合使用,注意到park()不建议使用,因为它没有提供线程阻塞的同步对象参数,通过这个参数我们能更清晰的知道当前阻塞的对象。接下来我们不妨看一下源码注释中给的例子先进先出锁的例子:

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);

     // Block while not first in queue or cannot acquire lock
     while (waiters.peek() != current ||
            !locked.compareAndSet(false, true)) {
       LockSupport.park(this);
       if (Thread.interrupted()) // ignore interrupts while waiting
         wasInterrupted = true;
     }

     waiters.remove();
     if (wasInterrupted)          // reassert interrupt status on exit
       current.interrupt();
   }

   public void unlock() {
     locked.set(false);
     LockSupport.unpark(waiters.peek());
   }
 }

我们必须注意,LockSupport.park(obj)方法也必须在守护循环代码块中,因为也可能会虚假唤醒,就和Object.wait一样。

Collections(四)Queue

队列是一个典型的FIFO先进先出结构,从一端放入元素,从另一端取出元素。队列在并发环境中比较有用,但是Java也提供了线程不安全的实现:LinkedList和PriorityQueue,ArrayDeque也实现了Queue的所有方法,它是基于可变数组实现的队列(关于ArrayDeque将在Deque章节中详细介绍)。

LinkedList的实现原理在前文中已经介绍过,下面给出LinkedList作为队列使用的一段示例代码,本文将对PriorityQueue作详细介绍。

LinkedList<String> linkedListDeque = new LinkedList<>();
linkedListDeque.offer("hello");
linkedListDeque.offer("i");
linkedListDeque.offer("am");
linkedListDeque.offer("queue");

org.junit.Assert.assertEquals("hello", linkedListDeque.peek());
org.junit.Assert.assertEquals("hello", linkedListDeque.poll());

Queue接口

我们先来看看Queue接口。

public interface Queue<E> extends Collection<E> {
    boolean add(E e);
    boolean offer(E e);
    E remove();
    E poll();
    E element();
    E peek();
}

队列操作分为三种,每一种操作都提供了两个方法:一个会在操作失败时候抛出异常(比如容量限制等),一个会返回特殊的值,比如null或者boolean。

作用 抛出异常 返回特殊的值
入队 add(E e) offer(E e)
出队 remove() poll()
获取元素 element() peek()

PriorityQueue不允许元素是null,当poll操作出队时,返null是可行的。但是对于LinkedList的来说,它的元素允许为null,所以根据null来判断是否未空队列是不可行的。

PriorityQueue实现原理

PriorityQueue是一个优先级队列,FIFO队列也可以理解为优先级队列,出队的元素是等待时间最长的。PriorityQueue出队的元素则是优先级最高的元素,优先级是使用Comparator和Comparable,我们在《Java Collections Framework(一)概览》中已经描述过。

为了满足这样的结构,我们需要对这个优先级队列的元素进行排序,PriorityQueue是基于堆实现的一个无界队列,使用堆来实现优先级队列,排序的性能达到了O(lgN),如果使用数组来排序,一次排序的时间就可能是O(N)。

PriorityQueue内部维护了一个数组Object[] queue来实现平衡二叉堆的结构,即queue[n]的两个儿子是queue[2*n+1]和queue[2*(n+1)],所以它是有初始容量的,并且提供了容量增长策略。

初始容量

PriorityQueue的默认初始容量是由一个常量指定的,值为11,正如ArrayList一样,为了性能优化,它也提供了一个构造器传入初始容量。

private static final int DEFAULT_INITIAL_CAPACITY = 11;

public PriorityQueue(int initialCapacity) {
    this(initialCapacity, null);
}

接下来我们从入队来分析它的容量增长策略。

入队和增长容量策略分析

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}

可以看到,优先队列是不允许NULL值存在的,当元素个数超过内部数组长度时,就会扩容,我们接着看grow方法:

private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    // Double size if small; else grow by 50%
    int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                     (oldCapacity + 2) :
                                     (oldCapacity >> 1));
    // overflow-conscious code
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    queue = Arrays.copyOf(queue, newCapacity);
}

源码中可以看出容量增长策略:如果当前容量小于64,则扩容后容量是现有容量的两倍多2,否则容量增长现有容量的50%

源码简要分析

peek

public E peek() {
    return (size == 0) ? null : (E) queue[0];
}

可见,是一个大根堆。

poll

public E poll() {
    if (size == 0)
        return null;
    int s = --size;
    modCount++;
    E result = (E) queue[0];
    E x = (E) queue[s];
    queue[s] = null;
    if (s != 0)
        siftDown(0, x);
    return result;
}

正如offer方法调用了siftUp方法,poll方法调用了siftDown方法来保证新增或者删除的时候,它仍然是一个堆。具体保证堆的算法这里就不贴出来了,可以自行查看源码或者算法书籍。

总结

并发包下面提供了很多有界线程安全的队列,提供了一些接口扩展了Queue,比如阻塞队列接口BlockingQueue,这些将在并发系列的文章中讲解。

  • ArrayBlockingQueue 基于数组的有界阻塞队列,线程安全
  • LinkedBlockingQueue 基于链表的可选有界阻塞队列,线程安全
  • PriorityBlockingQueue 基于堆的无界阻塞优先队列,线程安全
  • DelayQueue  基于堆的无界阻塞根据延迟时间优先顺序的队列,线程安全
  • SynchronousQueue  同步队列,线程安全
  • LinkedTransferQueue  线程安全
  • LinkedBlockingDeque 基于链表的可选有界阻塞双向队列,线程安全

Collections(三)List下篇-LinkedList

LinkedList实现了List接口,它使用链表方式实现了有序的序列结构。LinkedList又是一个有趣的存在,它还实现了Deque接口,而Deque接口是实现了Queue接口,所以LinkedList同时也实现了一个双向队列、队列或者栈结构。

本文将阐述LinkedList的实现原理,重点放在List接口方法实现上,关于Deque的方法使用,将在后续关于双向队列的章节中提及。

LinkedList实现原理

image

LinkedList是基于双向链表实现的,内部维护了首尾两个节点:Node first和Node last,其中Node的结构体如下:

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

每个节点包含了两个引用,一个指向前一个元素,一个指向后一个元素,首节点的prev值为null,尾节点的next值为null,即first.prev == null, last.next == null,LinkedList的大小即是节点的个数,由于是链表结构,它没有初试容量的概念,也无需扩容。

正因为双向链表这样的结构,我们可以限定元素插入和删除的方式,从而支持FIFO或者LIFO方式。

除了JDK中的实现方式外,双向链表还有个实现方式,给两个虚的节点,一个代表首虚节点,一个是尾虚节点,所有的元素都会在虚节点之间,这样的好处是我们在首尾插入和删除元素时,两个虚节点是不用变的。

基础源码分析

增加元素

public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

如果是在末尾插入元素,则时间复杂度会是O(1),如果是在中间插入,则会通过node(index)方法先遍历找到指定位置的节点,再进行插入,这样时间复杂度就是O(n)。我们先来看看node方法:

Node<E> node(int index) {
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

它对遍历作了一个简单的性能优化,当index大于list大小的二分之一时,就会从尾部向头部遍历,否则会从头部向尾部遍历。看到这里,可能有人会问,ArrayList的插入操作的时间复杂度是多少了?其实也是O(n),但是它的开销浪费在数组的空间复制上,所以比LikedList代价大。

接下来我们再看看典型的引用操作代码,无非就是对节点的prev和next进行处理:

void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

获取元素

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

前面已经说过,node是一个简单优化的顺序访问方法,获取一个元素的时间复杂度是O(n)。

更新元素

public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}

找到Node节点,重新设置节点item值。`

删除元素

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

删除元素,首先是找到元素,然后对节点的引用进行处理。

iterator源码实现和fail-fast设计

这里给出内部类ListItr的核心代码:

public boolean hasNext() {
    return nextIndex < size;
}

public E next() {
    checkForComodification();
    if (!hasNext())
        throw new NoSuchElementException();

    lastReturned = next;
    next = next.next;
    nextIndex++;
    return lastReturned.item;
}
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

它和ArrayList的Itr内部类的实现方式没有多大的区别,通过modCount机制实现fail-fast设计,获取下一个元素是获取节点的next引用。ListItr实现了ListIterator接口,所以它支持双向遍历和更多操作。

性能

LinkedList会以较低的代价进行插入和删除,在随机访问方面相对比较慢,但这也不是绝对的,如果我们能避免ArrayList去复制空间,比如只在末尾插入元素,那么由于随机访问特性,ArrayList在这样的场景下会是一个不错的选择。

总结

关于List,我们通过两篇文章详细介绍了ArrayList和LinkedList,它们都不是线程安全的。如果需要使用线程安全的List,我们可以通过java.util.Collections.synchronizedList(List)获取包装后的线程同步的List对象。

在java.util.concurrent并发包下,还提供了 CopyOnWriteArrayList 类支持并发操作,它通过一个写时复制的数组实现。

Collections(五)Deque

Deque是一个双向队列,它继承了Queue接口,除了作为双向队列使用外,既可以作为 FIFO队列和栈LIFO使用,即同时实现了堆栈和队列

通用实现有LinkedList和ArrayDeque,前面已经讨论过LinkedList作为队列来使用,下面看一段如何作为栈来使用的代码:

LinkedList<String> linkedListDeque = new LinkedList<>();
linkedListDeque.push("hello");
linkedListDeque.push("i");
linkedListDeque.push("am");
linkedListDeque.push("stack");
org.junit.Assert.assertEquals("stack", linkedListDeque.pop());

本文将会对ArrayDeque详细阐述。

Deque接口

我们先来看看Deque接口。

public interface Deque<E> extends Queue<E> {

    // *** deque methods ***
    void addFirst(E e);
    void addLast(E e);
    boolean offerFirst(E e);
    boolean offerLast(E e);
    E removeFirst();
    E removeLast();
    E pollFirst();
    E pollLast();
    E getFirst();
    E getLast();
    E peekFirst();
    E peekLast();
    boolean removeFirstOccurrence(Object o);
    boolean removeLastOccurrence(Object o);

    // *** Queue methods ***
    boolean add(E e);
    boolean offer(E e);
    E remove();
    E poll();
    E element();
    E peek();

    // *** Stack methods ***
    void push(E e);
    E pop();

    // *** Collection methods ***
    boolean remove(Object o);
    boolean contains(Object o);
    public int size();
    Iterator<E> iterator();
    Iterator<E> descendingIterator();
}

从方法名上大体上就能知道作用了,关于add和offer、remove和poll的区别参见《Java Collections Framework(三)Queue》。

ArrayDeque实现原理

image

ArrayDeque是基于双向可变数组实现的,内部维护一个数组Object[] elements以及两个变量:int head和int tail,其中head是队列头部的下标,tail是队列尾部下一个位置的下标,即head始终指向第一个元素,tail指向空元素,它是最后一个元素的下一个插入位置。

之所以称为双向数组,是因为它的元素可以从数组的两头插入,并且它还是一个循环数组,即如果head值为0,在头部插入新元素时,head位置会变为capacity-1,即这个内部数组的最后一个元素下标。

初始容量

在介绍容量的时候,我们先讨论一个明确的要求:ArrayDeque的容量必须是2的幂次方,那么这是为什么呢?它是为了高效的计算下标。
我们来考虑一个长度为8的数组,如果tail是7,那么增加一位后的下标应该是0,计算方式是:

new_tail = (tail + 1) % length = (7 + 1) % 8 = 0;

如果当前位置head是0,那么往前一位是7,计算方式是:

new_head = (head - 1 + length) % length = (0 - 1 + 8) % 8 = 7

由于取余运算在计算机指令中比较慢,但是如果 length是2的幂次方,我们可以用较快的位运算来代替取余运算

(tail + 1) & (length - 1) = (7 + 1) & (8 - 1) = 0
(head - 1) & (length - 1) = (0 - 1) & (8 - 1) = 7

因此,为了迅速定位到数组的下标,要求容量是2的N次方,在HashMap的key的Hash映射到哈希表某个下标的时候,也采用了位运算,这也是为什么HashMap的容量也必须是2的幂次方的原因。

接下来我们分析下ArrayDeque的容量,默认初始容量是16,最小容量是8,由一个常量定义,并且也提供了一个定义初始容量的构造器,确切的说,这个参数numElements并不是初始容量的值,而是初始容量的下界,因为没有限制使用者这个参数的传值,可以传任何非2的幂次方的值,源码如下:

private static final int MIN_INITIAL_CAPACITY = 8;

public ArrayDeque() {
    elements = new Object[16];
}
public ArrayDeque(int numElements) {
    allocateElements(numElements);
}

那么又是如何保证内部数组的容量是2的幂次方呢? numElements是一个下界,向上取2的幂次方值为初始容量。

private void allocateElements(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
    // Find the best power of two to hold elements.
    // Tests "<=" because arrays aren't kept full.
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        initialCapacity++;

        if (initialCapacity < 0)   // Too many elements, must back off
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    elements = new Object[initialCapacity];
}

增加元素和增长容量策略分析

我们直接看增加元素的代码,可以得出结论,ArrayQueue是不允许NULL值存在的,这和PriorityQueue是一致的,和LinkedList允许NULL值是不一致的。

循环数组的下标计算在源码中也可以理解的更清楚,计算head和tail值都运用了位运算。

public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

方法doubleCapacity()是为了容量增长策略设计的,当下一个要插入的元素,让head和tail相遇相等时就会成倍增长容量,我们看下这个方法:

private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // number of elements to the right of p
    int newCapacity = n << 1;
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    Object[] a = new Object[newCapacity];
    System.arraycopy(elements, p, a, 0, r);
    System.arraycopy(elements, 0, a, r, p);
    elements = a;
    head = 0;
    tail = n;
}

其中newCapacity = n << 1;就是扩容成双倍空间。

基础源码分析

获取元素

public E peekFirst() {
    // elements[head] is null if deque empty
    return (E) elements[head];
}

public E peekLast() {
    return (E) elements[(tail - 1) & (elements.length - 1)];
}

可以看到,由于tail是指向尾部的下一个位置,所以取最后一个元素,需要位运算。peek是队列的操作,没有元素时会返回null,当使用get方式获取元素时,如果没有元素,则会抛出异常:

public E getFirst() {
    @SuppressWarnings("unchecked")
    E result = (E) elements[head];
    if (result == null)
        throw new NoSuchElementException();
    return result;
}

/**
 * @throws NoSuchElementException {@inheritDoc}
 */
public E getLast() {
    @SuppressWarnings("unchecked")
    E result = (E) elements[(tail - 1) & (elements.length - 1)];
    if (result == null)
        throw new NoSuchElementException();
    return result;
}

出队

public E pollFirst() {
    int h = head;
    @SuppressWarnings("unchecked")
    E result = (E) elements[h];
    // Element is null if deque empty
    if (result == null)
        return null;
    elements[h] = null;     // Must null out slot
    head = (h + 1) & (elements.length - 1);
    return result;
}

public E pollLast() {
    int t = (tail - 1) & (elements.length - 1);
    @SuppressWarnings("unchecked")
    E result = (E) elements[t];
    if (result == null)
        return null;
    elements[t] = null;
    tail = t;
    return result;
}

这段代码再一次说明了,队列操作如果result为空,则会返回null,如果是pop操作遇到null值,则会抛出NoSuchElementException异常,这里就不附上代码了。

删除首次出现的指定元素

public boolean removeFirstOccurrence(Object o) {
    if (o == null)
        return false;
    int mask = elements.length - 1;
    int i = head;
    Object x;
    while ( (x = elements[i]) != null) {
        if (o.equals(x)) {
            delete(i);
            return true;
        }
        i = (i + 1) & mask;
    }
    return false;
}
private boolean delete(int i) {
    checkInvariants();
    final Object[] elements = this.elements;
    final int mask = elements.length - 1;
    final int h = head;
    final int t = tail;
    final int front = (i - h) & mask;
    final int back  = (t - i) & mask;

    // Invariant: head <= i < tail mod circularity
    if (front >= ((t - h) & mask))
        throw new ConcurrentModificationException();

    // Optimize for least element motion
    if (front < back) {
        if (h <= i) {
            System.arraycopy(elements, h, elements, h + 1, front);
        } else { // Wrap around
            System.arraycopy(elements, 0, elements, 1, i);
            elements[0] = elements[mask];
            System.arraycopy(elements, h, elements, h + 1, mask - h);
        }
        elements[h] = null;
        head = (h + 1) & mask;
        return false;
    } else {
        if (i < t) { // Copy the null tail as well
            System.arraycopy(elements, i + 1, elements, i, back);
            tail = t - 1;
        } else { // Wrap around
            System.arraycopy(elements, i + 1, elements, i, mask - i);
            elements[mask] = elements[0];
            System.arraycopy(elements, 1, elements, 0, t);
            tail = (t - 1) & mask;
        }
        return true;
    }
}

从源码中可见,当删除中间元素时,会出现数组拷贝复制操作,这里会有性能问题,而LinkedList的代价较低。

iterator源码实现和fail-fast设计

private class DeqIterator implements Iterator<E> {
  /**
   * Index of element to be returned by subsequent call to next.
   */
  private int cursor = head;

  /**
   * Tail recorded at construction (also in remove), to stop
   * iterator and also to check for comodification.
   */
  private int fence = tail;

  /**
   * Index of element returned by most recent call to next.
   * Reset to -1 if element is deleted by a call to remove.
   */
  private int lastRet = -1;

  public boolean hasNext() {
    return cursor != fence;
  }

  public E next() {
    if (cursor == fence)
      throw new NoSuchElementException();
    @SuppressWarnings("unchecked")
    E result = (E) elements[cursor];
    // This check doesn't catch all possible comodifications,
    // but does catch the ones that corrupt traversal
    if (tail != fence || result == null)
      throw new ConcurrentModificationException();
    lastRet = cursor;
    cursor = (cursor + 1) & (elements.length - 1);
    return result;
  }

  public void remove() {
    if (lastRet < 0)
      throw new IllegalStateException();
    if (delete(lastRet)) { // if left-shifted, undo increment in next()
      cursor = (cursor - 1) & (elements.length - 1);
      fence = tail;
    }
    lastRet = -1;
  }

  public void forEachRemaining(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    Object[] a = elements;
    int m = a.length - 1, f = fence, i = cursor;
    cursor = f;
    while (i != f) {
      @SuppressWarnings("unchecked") E e = (E)a[i];
      i = (i + 1) & m;
      if (e == null)
        throw new ConcurrentModificationException();
      action.accept(e);
    }
  }
}

当获取迭代器时,会保存当前的head和tail变量,分别为cursor和fence,当cursor不等于fence时,则认为有下一元素。

我们注意到的是,ArrayDeque实现fail-fast设计没有采用modCount机制,而是通过一个 不完全的判断来实现,这个判断只判断了当尾部下标变化时,才会抛出异常,这是值得注意的地方:

(tail != fence || result == null)

性能

ArrayDeque采用了双向可变数组,通过两个下标来标识收尾,它的访问和删除性能,都不会导致数组拷贝,当插入元素并且容量不足时,才会扩容,因为Deque大多数操作只允许在首部或者尾部进行,ArrayDeque和LinkedList之间并没有多少性能差异。

它们之间的性能差异主要体现在 迭代过程中删除元素,因为这样会导致ArrayDeque执行数组复制,正如上文中removeFirstOccurrence方法的源码所示。

最后,ArrayDeque的容量也是性能优化的一个点。

总结

ArrayDeque和LinkedList都不是线程安全的,LinkedBlockingDeque(如果双端队列为空,则方法如takeFirst并takeLast一直等待)是并发环境下一个双向队列的选择。

写一个极简的RPC和Hessian的设计

RPC(Remote Procedure Call)远程过程调用,也可以称作RMI(Remote Method Invoker),是一种client-server的形式,即一台机器调用远程机器的方法,就像执行本地方法一样。目前的远程技术有:

  • Facebook开源的thrift
  • Spring’s HTTP invoker
  • Hessian
  • JDK RMI
  • WebServices
  • ...

本文以一个极简的RPC实现和Spring’s HTTP invoker开始,对RPC的基本原理进行介绍,并进一步分析Hessian的的设计。

写一个极简的RPC

编写一个远程方法调用,我们可能需要做以下几件事:

1. 编写服务

从代码层面上说,我们可以把服务理解成接口,所以我们只要根据业务编写普通的Service Interface即可。在RPC的server端将实现此接口,在client端依赖此接口。

2. server端暴露服务、client端连接服务

这里就涉及到网络通信,我们可以基于传输层协议TCP、UDP,也可以基于应用层协议HTTP,这里我们实现基于HTTP协议的RPC框架。因为每一个不同的URL可以代表一个服务,所以处理方式就变为:
server暴露服务对应server暴露一系列指定的URL,每个URL关联一个服务。
client连接服务即是client发送HTTP请求,调用指定的URL提供的服务。

3. client端像执行本地方法一样,调用服务

因为客户端没有实现类,只依赖服务接口,所以自然的想到使用动态代理可以解决类似本地调用的执行方式。通过URL可以指定服务,那么如何指定服务的具体方法和参数呢?答案是通过HTTP POST的RequestBody:
1):client端将方法和参数写入Body中
2):server端从Body中解析方法和参数,调用服务的实现,响应返回值
3):client端取得返回值
这里有个重点,就是网络传输数据的序列化和反序列化问题

image

Server端实现

服务端我们采用Sevlet的形式,每配置一个Servlet就代表一个服务,每个Servlet需要初始化两个参数:service服务实现类和serviceInterface服务接口。序列化和反序列化采用JDK自带的Object**putStream。

package com.deepoove.onerpc.server;

import static com.deepoove.onerpc.util.NameMangle.mangleName;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class OneRpcServlet extends HttpServlet {

    private static final long serialVersionUID = -6904007509665776812L;

    private Class<?> serviceInterface;
    private Object service;

    private Map<String, Method> methodNameMaping = new HashMap<>();

    @Override
    public void init(ServletConfig config) throws ServletException {
        String serviceInterfaceName = config.getInitParameter("serviceInterface");
        String serviceClassName = config.getInitParameter("service");

        try {
            //根据servlet参数初始化服务和服务实现类
            serviceInterface = loadClass(serviceInterfaceName);
            Class<?> serviceClass = loadClass(serviceClassName);
            service = serviceClass.newInstance();

            //构造服务方法名称和方法的映射
            Method[] methods = serviceInterface.getMethods();
            for (int i = 0; i < methods.length; i++) {
                Method method = methods[i];
                methodNameMaping.put(mangleName(method), method);
            }

        } catch (Exception e) {
            throw new ServletException(e);
        }
    }

    private Class<?> loadClass(String serviceInterfaceName) throws ClassNotFoundException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        if (null != classLoader) {
            return Class.forName(serviceInterfaceName, false, classLoader);
        } else {
            return Class.forName(serviceInterfaceName);
        }
    }

    @Override
    public void service(ServletRequest request, ServletResponse response)
            throws ServletException, IOException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        //限定为post方法
        String httpmethod = req.getMethod();
        if (!"POST".equals(httpmethod)) {
            res.setStatus(500);
            PrintWriter out = res.getWriter();
            res.setContentType("text/html");
            out.println("<h1>Requires POST</h1>");
            out.close();
            return;
        }

        ServletInputStream inputStream = req.getInputStream();
        ServletOutputStream outputStream = res.getOutputStream();
        
        ObjectInputStream ois = new ObjectInputStream(inputStream);
        ObjectOutputStream oos = new ObjectOutputStream(outputStream);
        try {
            //使用JDK自带的反序列化:读取方法名和参数
            String methodName = (String) ois.readObject();
            int length = ois.readInt();
            Object[] args = new Object[length];
            for (int i = 0; i < length; i++) {
                args[i] = ois.readObject();
            }

            //调用指定方法,获得结果
            Method method = methodNameMaping.get(methodName);
            Object result = method.invoke(service, args);

            //序列化方法返回值,写入response流
            oos.writeObject(result);
            oos.flush();

        } catch (Exception e) {
            throw new ServletException(e);
        }
        finally {
            ois.close();
            oos.close();
        }
    }

}

Client端实现

客户端需要指定服务的URL和服务接口,在调用服务接口指定方法时,将方法名和参数写入request body中,获得返回值后,反序列化读取返回值。

package com.deepoove.onerpc.client;

import static com.deepoove.onerpc.util.NameMangle.mangleName;

import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;

public class OneRpcClient implements InvocationHandler {

    private URL url;
    private Class<?> serviceInterface;

    private OneRpcClient() {}

    /**
     * 采用动态代理,获取服务接口的实例
     */
    public static <T> T create(Class<T> serviceInterface, String url) throws MalformedURLException {
        OneRpcClient client = new OneRpcClient();
        client.url = new URL(url);
        client.serviceInterface = serviceInterface;

        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        return (T) Proxy.newProxyInstance(loader, new Class<?>[] { client.serviceInterface },
                client);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        URLConnection conn = url.openConnection();
        conn.setConnectTimeout(10000);
        conn.setReadTimeout(10000);
        conn.setDoOutput(true);
        conn.setRequestProperty("Content-Type", "x-application/onerpc");
        conn.setRequestProperty("Accept-Encoding", "deflate");

        OutputStream outputStream = conn.getOutputStream();
        // 将方法名和参数序列化
        ObjectOutputStream oos = new ObjectOutputStream(outputStream);
        oos.writeObject(mangleName(method));
        oos.writeInt(args.length);
        for (int i = 0; i < args.length; i++) {
            oos.writeObject(args[i]);
        }
        oos.flush();

        // 获得HTTP返回信息
        HttpURLConnection httpConn = (HttpURLConnection) conn;

        int _statusCode = 500;

        try {
            _statusCode = httpConn.getResponseCode();
        } catch (Exception e) {}
        if (_statusCode != 200) {
            throw new RuntimeException("code:" + _statusCode);
        } else {
            // 将返回值反序列化
            InputStream inputStream = conn.getInputStream();
            ObjectInputStream ois = new ObjectInputStream(inputStream);
            Object readObject = ois.readObject();
            ois.close();
            return readObject;

        }
    }

}

demo

我们使用了上述两个类即完成了一个极其基础的RPC框架。接下来看看我们怎么使用:

  1. 编写服务
    简单的服务,User也是个简单的对象,注意要实现Serializable接口。服务端和客户端都依赖此服务接口。
package com.deepoove.hessian.api.service;

import com.deepoove.hessian.api.pojo.User;

public interface UserService {
    
    User get(String id);
    
    User get(int id);
    
    void add(User user);

}
  1. 编写服务实现
    这也是个简单的服务实现。
package com.deepoove.hessian.example.service;

import com.deepoove.hessian.api.pojo.User;
import com.deepoove.hessian.api.service.UserService;

public class UserServiceImpl implements UserService {

    @Override
    public User get(String id) {
        User user = new User();
        user.setName("Sayi");
        System.out.println("string method");
        return user;
    }

    @Override
    public void add(User user) {}

    @Override
    public User get(int id) {
        System.out.println("int method");
        return null;
    }

}
  1. 暴露服务其实就是配置Servlet即可,并且制定服务接口和服务实现类。启动web容器,服务地址如:http://127.0.0.1:8077/userService
<servlet>
    <servlet-name>userService</servlet-name>
    <servlet-class>com.deepoove.onerpc.server.OneRpcServlet</servlet-class>
    <init-param>
        <param-name>serviceInterface</param-name>
        <param-value>com.deepoove.hessian.api.service.UserService</param-value>
    </init-param>
    <init-param>
        <param-name>service</param-name>
        <param-value>com.deepoove.hessian.example.service.UserServiceImpl</param-value>
    </init-param>
</servlet>

<servlet-mapping>
    <servlet-name>userService</servlet-name>
    <url-pattern>/userService</url-pattern>
</servlet-mapping>
  1. 写个客户端,调用服务
    客户端依赖服务接口,通过服务接口和URL创建接口实例。
public static void main(String[] args) throws MalformedURLException {
    UserService userService = OneRpcClient.create(UserService.class, "http://127.0.0.1:8077/userService");
    System.out.println(userService.get("").getName());
}

至此,一个极简的RPC已经完成,它是基于HTTP协议进行网络传输的,并且使用JDK自带的序列化工具进行数据的序列化和反序列化,数据结构的协议格式可以认为就是方法名和参数。

Spring’s HTTP invoker

上文中的代码是个极其简陋,而Spring’s HTTP invoker也是基于HTTP协议和Java序列化的,它用spring的风格设计了远程调用模块。我们先看下Server端的核心源码:

public void handleRequest(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

    try {
        RemoteInvocation invocation = readRemoteInvocation(request);
        RemoteInvocationResult result = invokeAndCreateResult(invocation, getProxy());
        writeRemoteInvocationResult(request, response, result);
    }
    catch (ClassNotFoundException ex) {
        throw new NestedServletException("Class not found during deserialization", ex);
    }
}

Spring’s HTTP invoker对方法名、参数和返回值的数据结构进行了封装,分别是RemoteInvocation和RemoteInvocationResult。从源码中可以看出,服务端也是从request反序列化数据RemoteInvocation,然后调用服务的具体方法,最后序列化返回值RemoteInvocationResult到response流中。

在网络传输中,Java序列化的性能是无法满意的,我们有理由选择更优秀的序列化方案。

Hessian的设计

Hessian同样是基于应用层HTTP协议进行传输的,序列化采用了自有的Hessian二进制序列化,数据格式上使用了Hessian协议。

Hessian的协议:

top       ::= version content
          ::= call-1.0
          ::= reply-1.0

          # RPC-style call
call      ::= 'C' string int value*

call-1.0  ::= 'c' x01 x00 <hessian-1.0-call>

content   ::= call       # rpc call
          ::= fault      # rpc fault reply
          ::= reply      # rpc value reply
          ::= packet+    # streaming packet data
          ::= envelope+  # envelope wrapping content

envelope  ::= 'E' string env-chunk* 'Z'
env-chunk ::= int (string value)* packet int (string value)*

          # RPC fault
fault     ::= 'F' (value value)* 'Z'

          # message/streaming message
packet    ::= (x4f b1 b0 <data>)* packet
          ::= 'P' b1 b0 <data>
          ::= [x70 - x7f] <data>
          ::= [x80 - xff] <data>

          # RPC reply
reply     ::= 'R' value

reply-1.0 ::= 'r' x01 x00 <hessian-1.0-reply>

version   ::= 'H' x02 x00

Hessian协议的设计目标首先它是不依赖任何IDL或者Scheme的,协议对于应用应该是透明的。其次是语言无关的,这样便于支持多语言的RPC。更多协议信息参见官网http://hessian.caucho.com/doc/hessian-ws.html

Hessian协议带有版本信息,区分Hessian的不同版本。协议中也规定了Call和Reply信息。

Hessian的序列化反序列化

在包com.caucho.hessian.io下包含了大量有关的类,它们实现了一个自描述、语言无关的序列化方案。它们也是仅仅依赖JDK的,所以可以在任何地方仅仅使用Hessian的序列化功能。

Hessian的实现

Hessian的实现无非就是hessian协议的实现、序列化反序列化的实现、以及RPC的实现。HessianSkeleton定义了服务端主要的功能,它负责调用指定方法。HessianProxyFactory工厂则提供了获得服务接口动态代理的功能,默认是不支持方法重载的,可以调用factory.setOverloadEnabled(true);方法,支持命名修饰(name mangle)。

Hessian与Spring Remoting

我们知道,大多数框架具有普适性,它定义了大多数人要使用的功能,为一部分人提供了功能的配置,而没有为少数人提供个性化的功能。Hessian本身服务端是基于Servlet的,Spring的org.springframework.remoting.caucho.HessianServiceExporter 可以透明的暴露服务,org.springframework.remoting.caucho.HessianProxyFactoryBean 可以方便的建立对应的服务代理Bean。

HessianServiceExporter的父类RemoteExporter为服务端提供了拦截器的功能,这样我们可以实现自己的拦截器,打印参数日志,耗时,如果公司内部返回值含有code,也可以打印返回值code等信息。我相信,大部分使用Hessian的公司都会定制这方面的功能。

Hessian缺乏一个重试机制,我见过很多使用Hessian的代码都是在业务系统写满了while循坏,以达到超时重试的目的,这份工作我们可以自定义Hessian客户端的HTTP请求来实现。

More 更多

本文所述的RPC都是基于HTTP协议的,HTTP本身是个应用层协议,我们可以基于传输层TCP协议实现,技术选型可以使用Netty。序列化的选型我们可以类比下Hessian序列化、protobuf和Java 序列化的优缺点。

随着业务系统,微服务的增加,我们发现这样的RPC有着明显的缺点,服务分散在各地,缺乏统一管理,服务注册服务治理的技术越来越流行。国内的Dubbo支持多transporter(mina, netty, grizzy)和多protocol(dubbo、hessian、http、thrift、rmi等),正在成为佼佼者。

并发(五)同步控制

同步控制是指在某些场景下,或允许若干线程执行,或阻塞若干线程场景,前面我们已经了解利用QS state实现各种锁,本文我们学习一些利用AQS state的同步控制类和其它实现的类。

IMPORTANT:同步控制下,如果允许多个线程同时执行,那么我们还是要避免多线程干涉的问题,比如使用锁。

Semaphore

信号量,代表了若干许可,通常场景用来限制某个资源的访问数量,每个线程都将获取许可,如果许可用完了就会阻塞,直到许可被其它线程释放。我们来看看JDK提供的一个示例:

class Pool {
   private static final int MAX_AVAILABLE = 100;
   private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);

   public Object getItem() throws InterruptedException {
     available.acquire();
     return getNextAvailableItem();
   }

   public void putItem(Object x) {
     if (markAsUnused(x))
       available.release();
   }

   // Not a particularly efficient data structure; just for demo
   protected Object[] items = ... whatever kinds of items being managed
   protected boolean[] used = new boolean[MAX_AVAILABLE];

   protected synchronized Object getNextAvailableItem() {
     for (int i = 0; i < MAX_AVAILABLE; ++i) {
       if (!used[i]) {
          used[i] = true;
          return items[i];
       }
     }
     return null; // not reached
   }

   protected synchronized boolean markAsUnused(Object item) {
     for (int i = 0; i < MAX_AVAILABLE; ++i) {
       if (item == items[i]) {
          if (used[i]) {
            used[i] = false;
            return true;
          } else
            return false;
       }
     }
     return false;
   }
 }

这个示例只允许有100个线程去访问资源,声明了一个信号量对象,参数true表示唤醒线程时使用公平策略:

Semaphore available = new Semaphore(MAX_AVAILABLE, true);

通过available.acquire()来获取许可,可用许可减1,如果没有可用许可就会阻塞,通过available.release()释放许可。我们看到示例中getNextAvailableItem方法被synchronized修饰,因为信号量是一个同步控制,它允许若干线程同时执行,所以必须解决多线程干涉问题。

我们试想一下,如果new Semaphore(1);是什么意思?其实它就是一个排它锁,称为:binary semaphore(二进制信号量)。

源码

Semaphore是基于AQS state实现的,state的值就是信号量的可用许可数。

abstract static class Sync extends AbstractQueuedSynchronizer {

  Sync(int permits) {
    setState(permits);
  }

  final int getPermits() {
    return getState();
  }

  final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
      int available = getState();
      int remaining = available - acquires;
      if (remaining < 0 ||
        compareAndSetState(available, remaining))
        return remaining;
    }
  }

  protected final boolean tryReleaseShared(int releases) {
    for (;;) {
      int current = getState();
      int next = current + releases;
      if (next < current) // overflow
        throw new Error("Maximum permit count exceeded");
      if (compareAndSetState(current, next))
        return true;
    }
  }

  // 略
}

其中nonfairTryAcquireShared方法是非公平信号量的实现,获取许可时会从state减去相应的许可数,如果值为负数,则表示没有许可可用,阻塞当前线程。tryReleaseShared相反,释放许可会增加相应许可数。我们来看看公平信号量的实现:

static final class FairSync extends Sync {

  FairSync(int permits) {
    super(permits);
  }

  protected int tryAcquireShared(int acquires) {
    for (;;) {
      if (hasQueuedPredecessors())
        return -1;
      int available = getState();
      int remaining = available - acquires;
      if (remaining < 0 ||
        compareAndSetState(available, remaining))
        return remaining;
    }
  }
}

如果读过上一篇《锁》的文章,我们就知道hasQueuedPredecessors是来判断是否有阻塞更久的线程,如果有,返回负数,当前线程阻塞,它确保了公平机制。接下来我们就可以利用Sync来实现Semaphore了,Semaphore只实现了序列化接口,没有实现任何其它接口:

public class Semaphore implements java.io.Serializable {
  private final Sync sync;
  public Semaphore(int permits) {
    sync = new NonfairSync(permits);
  }
  public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
  }
  public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
  }
  public void acquireUninterruptibly() {
    sync.acquireShared(1);
  }
  public boolean tryAcquire() {
    return sync.nonfairTryAcquireShared(1) >= 0;
  }
  public void release() {
    sync.releaseShared(1);
  }
  public void acquire(int permits) throws InterruptedException {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireSharedInterruptibly(permits);
  }
  public void acquireUninterruptibly(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.acquireShared(permits);
  }
  public void release(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.releaseShared(permits);
  }
  // 略
}

我们看到,信号量的acquire()方法其实是可以被中断的阻塞,抛出InterruptedException异常,而acquireUninterruptibly()方法是不可中断的。

CountDownLatch

计数阀门,用来等待N个线程执行完一系列操作后,调用latch.await()方法的线程(一个或多个)才会继续执行,否则阻塞。我们来看看JDK提供的示例:

class Driver2 { // ...
   void main() throws InterruptedException {
     CountDownLatch doneSignal = new CountDownLatch(N);
     Executor e = ...

     for (int i = 0; i < N; ++i) // create and start threads
       e.execute(new WorkerRunnable(doneSignal, i));

     doneSignal.await();           // wait for all to finish
   }
 }

 class WorkerRunnable implements Runnable {
   private final CountDownLatch doneSignal;
   private final int i;
   WorkerRunnable(CountDownLatch doneSignal, int i) {
     this.doneSignal = doneSignal;
     this.i = i;
   }
   public void run() {
     try {
       doWork(i);
       doneSignal.countDown();
     } catch (InterruptedException ex) {} // return;
   }

   void doWork() { ... }
}

这段代码首先声明了N个计数的阀门:

CountDownLatch doneSignal = new CountDownLatch(N);

然后会启动N个线程去解决问题,主线程调用doneSignal.await();等待N个线程执行完某些操作,这些线程执行完工作都会调用:

doneSignal.countDown();

这个示例是一个非常典型的场景,一个线程等待N个线程执行结束才继续执行。我们再来看看一个示例:

class Driver { // ...
   void main() throws InterruptedException {
     CountDownLatch startSignal = new CountDownLatch(1);
     CountDownLatch doneSignal = new CountDownLatch(N);

     for (int i = 0; i < N; ++i) // create and start threads
       new Thread(new Worker(startSignal, doneSignal)).start();

     doSomethingElse();            // don't let run yet
     startSignal.countDown();      // let all threads proceed
     doSomethingElse();
     doneSignal.await();           // wait for all to finish
   }
 }

 class Worker implements Runnable {
   private final CountDownLatch startSignal;
   private final CountDownLatch doneSignal;
   Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
     this.startSignal = startSignal;
     this.doneSignal = doneSignal;
   }
   public void run() {
     try {
       startSignal.await();
       doWork();
       doneSignal.countDown();
     } catch (InterruptedException ex) {} // return;
   }

   void doWork() { ... }
}

这个示例有点不同了,声明了两个计数阀门变量,分别初始化计数为1和N:

CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(N);

startSignal用来让N个线程执行到某一刻等待,然后通过主线程的startSignal.countDown();统一继续执行。主线程通过doneSignal.await();阻塞,等待这N个线程执行完毕后调用doneSignal.countDown();`方法。

源码

CountDownLatch也是利用AQS state实现的,state会初始化为计数值,每次countDown()方法的调用都会将计数减一,await()方法将会判断state值是否为0,如果不为0则认为拿不到许可,阻塞当前线程。

private static final class Sync extends AbstractQueuedSynchronizer {
  private static final long serialVersionUID = 4982264981922014374L;

  Sync(int count) {
    setState(count);
  }

  int getCount() {
    return getState();
  }

  protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
  }

  protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
      int c = getState();
      if (c == 0)
        return false;
      int nextc = c-1;
      if (compareAndSetState(c, nextc))
        return nextc == 0;
    }
  }
}

从源码中可以看到tryAcquireShared方法,如果getState()不为0返回负数,当前线程阻塞。我们来看看CountDownLatch利用Sync类的实现:

public class CountDownLatch {
  private final Sync sync;
  public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
  }
  public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
  }
  public void countDown() {
    sync.releaseShared(1);
  }
  // 略
}

我们注意到await()方法是可以被中断的,抛出InterruptedException异常。

CyclicBarrier

循环栅栏,栅栏的作用是在线程中设置栅栏点,当若干线程执行到这个栅栏点时就会阻塞,直到满足初始化栅栏个数时才会一起继续执行,即一系列线程互相等待到某个共同点才会继续执行,循环的意思是CyclicBarrier对象可以被复用,它的使用场景就在于后续的操作必须之前的操作结束才能执行。我们来看一个示例:

class Solver {
   final int N;
   final float[][] data;
   final CyclicBarrier barrier;

   class Worker implements Runnable {
     int myRow;
     Worker(int row) { myRow = row; }
     public void run() {
       while (!done()) {
         processRow(myRow);

         try {
           barrier.await();
         } catch (InterruptedException ex) {
           return;
         } catch (BrokenBarrierException ex) {
           return;
         }
       }
     }
   }

   public Solver(float[][] matrix) {
     data = matrix;
     N = matrix.length;
     Runnable barrierAction =
       new Runnable() { public void run() { mergeRows(...); }};
     barrier = new CyclicBarrier(N, barrierAction);

     List<Thread> threads = new ArrayList<Thread>(N);
     for (int i = 0; i < N; i++) {
       Thread thread = new Thread(new Worker(i));
       threads.add(thread);
       thread.start();
     }
}

声明栅栏的代码提供了两个参数:

barrier = new CyclicBarrier(N, barrierAction);

N表示栅栏个数,也就是必须N个线程调用barrier.await();方法才能让这些线程继续执行,barrierAction是一个Runnable对象,表示当栅栏打开时后续的执行动作。

示例中可以看到N个线程必须互相等待,直到都处理完自己的行数据processRow(myRow);后才能执行barrierAction里面的mergeRows方法。

源码

CyclicBarrier 没有使用AQS实现,它直接使用Lock和Condition实现:

/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
/** Condition to wait on until tripped */
private final Condition trip = lock.newCondition();

我们直接看核心await阻塞的源码:

/**
 * Updates state on barrier trip and wakes up everyone.
 * Called only while holding lock.
 */
private void nextGeneration() {
  // signal completion of last generation
  trip.signalAll();
  // set up next generation
  count = parties;
  generation = new Generation();
}

/**
 * Sets current barrier generation as broken and wakes up everyone.
 * Called only while holding lock.
 */
private void breakBarrier() {
  generation.broken = true;
  count = parties;
  trip.signalAll();
}

/**
 * Main barrier code, covering the various policies.
 */
private int dowait(boolean timed, long nanos)
  throws InterruptedException, BrokenBarrierException,
       TimeoutException {
  final ReentrantLock lock = this.lock;
  lock.lock();
  try {
    final Generation g = generation;

    if (g.broken)
      throw new BrokenBarrierException();

    if (Thread.interrupted()) {
      breakBarrier();
      throw new InterruptedException();
    }

    int index = --count;
    if (index == 0) {  // tripped
      boolean ranAction = false;
      try {
        final Runnable command = barrierCommand;
        if (command != null)
          command.run();
        ranAction = true;
        nextGeneration();
        return 0;
      } finally {
        if (!ranAction)
          breakBarrier();
      }
    }

    // loop until tripped, broken, interrupted, or timed out
    for (;;) {
      try {
        if (!timed)
          trip.await();
        else if (nanos > 0L)
          nanos = trip.awaitNanos(nanos);
      } catch (InterruptedException ie) {
        if (g == generation && ! g.broken) {
          breakBarrier();
          throw ie;
        } else {
          // We're about to finish waiting even if we had not
          // been interrupted, so this interrupt is deemed to
          // "belong" to subsequent execution.
          Thread.currentThread().interrupt();
        }
      }

      if (g.broken)
        throw new BrokenBarrierException();

      if (g != generation)
        return index;

      if (timed && nanos <= 0L) {
        breakBarrier();
        throw new TimeoutException();
      }
    }
  } finally {
    lock.unlock();
  }
}

CyclicBarrier的await()方法会抛出中断异常和BrokenBarrierException,从源码中可以看到,当后续操作command.run();抛出异常了后就会破坏栅栏。

每次调用await()方法都会将还未到达栅栏的计数减一,当计数为0时,表示是这是最后一个到达栅栏的线程,执行后续动作,nextGeneration()重置栅栏,breakBarrier()来破坏栅栏,我们发现无论重置还是破坏,都会调用trip.signalAll();唤醒其它阻塞的线程,执行后续动作代码片段如下:

if (index == 0) {  // tripped
  boolean ranAction = false;
  try {
    final Runnable command = barrierCommand;
    if (command != null)
      command.run();
    ranAction = true;
    nextGeneration();
    return 0;
  } finally {
    if (!ranAction)
      breakBarrier();
  }
}

如果计数不为0,则会阻塞当前线程,当被最后一个到达栅栏的线程唤醒后,会判断栅栏是重置状态还是破坏状态,如果是破坏状态,这些线程也会接收到BrokenBarrierException。

总结

还有两个同步控制器,Phaser 和 Exchanger 这里就不作介绍了,利用同步控制器,我们可以解决很多实际问题。

[Code-Snippet]JSON转Java

package com.deepoove.hooks.gen;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;

import org.junit.Test;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;

/**
 * json string to java bean,json use FastJson
 * 
 * @author Sayi
 * @version
 */
public class Json2JavaTest {

	List<String> classes = new ArrayList<>();
	List<String> clazzes = new ArrayList<>();
	Map<String, String> class2Feilds = new HashMap<>();

	String packageName = "package com.deepoove;";

	String jsonImport = "import com.alibaba.fastjson.annotation.JSONField;";
	String listImport = "import java.util.List;";
	String dateImport = "import java.util.Date;";

	@SuppressWarnings("resource")
	@Test
	public void test() {

		// String str =
		// "{\"name\":123,\"age\":true,\"create_time\":\"2017-05-23T10:56:12.925+0800\"}";
		String str = "";
		String className = "user";

		Scanner scanner = new Scanner(System.in);
		String endStr = "==1==";
		String line = "";
		line = scanner.nextLine();
		while (!endStr.equals(line)) {
			str += line;
			line = scanner.nextLine();
		}

		Object parse = JSON.parse(str, Feature.OrderedField);
		if (parse instanceof JSONObject) {
			processObj(className, (JSONObject) parse);
		} else if (parse instanceof JSONArray) {
			Object object = ((JSONArray) parse).get(0);
			processObj(className, (JSONObject) object);
		} else {
			throw new IllegalArgumentException("error json format data");
		}

		for (String clazz : classes) {
			if (!clazzes.contains(clazz)) clazzes.add(clazz);
		}
		for (String clazz : clazzes) {
			System.out.println(clazz);
		}

	}

	private String processObj(String className, JSONObject object) {
		Set<String> keySet = object.keySet();
		Iterator<String> iterator = keySet.iterator();
		StringBuffer sb = new StringBuffer();

		boolean a = false, b = false, c = false;

		while (iterator.hasNext()) {
			String key = iterator.next();
			Object value = object.get(key);
			if (value instanceof JSONObject) {
				String clazzName = processObj(key, (JSONObject) value);
				sb.append("private " + upCaseFirstChar(clazzName) + " " + key + ";").append("\n");
			} else if (value instanceof JSONArray) {
				Object obj = ((JSONArray) value).get(0);
				String clazzName = processObj(key, (JSONObject) obj);
				sb.append("private List<" + upCaseFirstChar(clazzName) + "> " + key + ";")
						.append("\n");
				a = true;
			} else {
				// System.out.println(value.getClass());
				if (isOptimusFeild(key)) {
					sb.append("@JSONField(name = \"" + key + "\");\n");
					key = optimusFeild(key);
					b = true;
				}
				if (value instanceof Integer) {
					sb.append("private int " + key + ";").append("\n");
				} else if (value instanceof Boolean) {
					sb.append("private boolean " + key + ";").append("\n");
				} else if (value instanceof Long) {
					sb.append("private long " + key + ";").append("\n");
				} else if (value instanceof Float) {
					sb.append("private float " + key + ";").append("\n");
				} else if (value instanceof String) {
					sb.append("private String " + key + ";").append("\n");
				} else if (value instanceof Date) {
					sb.append("private Date " + key + ";").append("\n");
					c = true;
				} else if (value instanceof Boolean) {
					sb.append("private boolean " + key + ";").append("\n");
				}
			}
		}
		// sb.append("\n}");
		// System.out.println(sb.toString());
		for (Map.Entry<String, String> entry : class2Feilds.entrySet()) {
			String v = entry.getValue();
			String k = entry.getKey();
			if (v.equals(sb.toString())) return k;

		}
		class2Feilds.put(className, sb.toString());

		StringBuffer sbHeader = new StringBuffer();
		sbHeader.append(packageName).append("\n");
		if (a) sbHeader.append(listImport).append("\n");
		if (b) sbHeader.append(jsonImport).append("\n");
		if (c) sbHeader.append(dateImport).append("\n");
		sbHeader.append("public class " + upCaseFirstChar(className) + "{").append("\n");
		classes.add(sbHeader.toString() + sb.toString() + "\n}");

		return className;
	}

	public String upCaseFirstChar(String str) {
		if (null == str) return null;
		char character = str.charAt(0);
		return (character + "").toUpperCase() + str.substring(1);
	}

	public boolean isOptimusFeild(String str) {
		if (null == str) return false;
		String[] split = str.split("_");
		if (split.length <= 1) return false;
		return true;
	}

	public String optimusFeild(String str) {
		if (null == str) return str;
		String[] split = str.split("_");
		if (split.length <= 1) return str;
		StringBuffer sb = new StringBuffer(split[0]);
		for (int i = 1; i < split.length; i++) {
			sb.append(upCaseFirstChar(split[i]));
		}
		return sb.toString();
	}

}

[Code-Snippet]收集GitHub Issues标题

public static void main(String[] args) {
  JSONArray parseArray = JSON.parseArray(str);
  Map<String, List<String>> result = new TreeMap<String, List<String>>(
      new Comparator<String>() {
              public int compare(String obj1, String obj2) {
                  // 降序排序
                  return obj2.charAt(0) - obj1.charAt(0) == 0 ? 1 : obj2.charAt(0) - obj1.charAt(0);
              }
      }
  );
  Map<String, List<String>> ret = new HashMap<String, List<String>>();
  for (int i = 0; i < parseArray.size(); i++) {
    Map map = JSON.parseObject(parseArray.get(i).toString(), Map.class);
    StringBuilder sb = new StringBuilder();
    sb.append("[").append(map.get("title")).append("](").append(map.get("html_url")).append(")\n");
//      System.out.println(sb.toString());
    Object lables = map.get("labels");
    JSONArray parseArray2 = JSON.parseArray(lables.toString());
    for (int j = 0; j < parseArray2.size(); j++) {
      Map parseObject = JSON.parseObject(parseArray2.get(j).toString(), Map.class);
      List<String> list = ret.get(parseObject.get("name"));
      if (null != list) {
        list.add(sb.toString());
      }else {
        list = new ArrayList<String>();
        list.add(sb.toString());
        ret.put(parseObject.get("name").toString(), list);
      }
    }
  }
  for (Map.Entry<String, List<String>> entry : ret.entrySet()) {
    result.put(entry.getValue().size() + entry.getKey(), entry.getValue());
  }
  for (Map.Entry<String, List<String>> entry : result.entrySet()) {
    System.out.println("### " + entry.getKey().substring(1));
    for (String value : entry.getValue()) {
      System.out.println(value);
    }
  }
  
}

并发(四)锁

我们已经知道,synchronized提供了强大的同步功能,volatile提供了稍微弱一点的同步机制,我们也可以通过ThreadLocal和原子类来避免多线程问题,这篇文章将会探讨Java提供更强大的显示锁:Lock,它更加的灵活,在讲解之前,我们先来读一读关于锁的术语:

  • 共享锁、(排它锁、独占锁、互斥锁)
    共享锁表示这个锁可以被多个线程共享,后文中提到的读锁就是一种共享锁,允许多个线程读。排它锁是只有一个线程能占有锁,不允许其它线程获得锁,后文中的写锁就是一个排它锁,不允许任何其它线程读和写,synchronized也是一个排它锁。

  • 悲观锁、乐观锁
    悲观锁是总会认为并发问题会存在,所以总是会加锁,比如synchronized。而乐观锁假设并发问题不存在,而在更新时才会比较是否发生了并发问题,所以不会加锁,比如CAS机制。

  • 重入锁、非重入锁
    当前线程可以多次获得同一个锁,就是重入锁,比如synchronized。

  • 读写锁
    读写锁可以分为读锁和写锁,允许多个线程共享读锁,但是当有写锁被独占时,不允许其它任何线程占有读锁或写锁。

  • 偏向锁、自旋锁
    这涉及到synchronized底层优化的一些概念,偏向锁升级到轻量级锁等。

  • 公平锁、非公平锁
    公平锁总是唤醒最先阻塞的线程,所以它是公平的。非公平锁则不一定按照阻塞顺序来唤醒。

锁的种类大多数之间并不是互斥的关系,它们分别从各自的角度去描述这个锁的功能,在数据并发层面,还有行锁、表锁等,在分布式环境下,有分布式锁的概念

接下来,我们将深入研究Lock及其实现。

Lock、Conditon

Lock是java.util.concurrent.locks包下一个最基本的接口Condition则提供了类似wait/notify机制的await和signal,我们先来看看类图:

image

正如接口所示,lock和unlock分别是加锁和释放锁,我们必须确保使用完锁会释放锁,典型的Lock使用习惯是通过try-finally来释放锁:

Lock lock = ...;
lock.lock();
try {
  // access the resource protected by this lock
} finally {
  lock.unlock();
}

问题:为什么有了synchronized,还需要显示锁Lock呢?它们之间有什么联系和区别? 我觉得可以分以下几:

  1. 它们都创造了Happens-before关系,所以是内存一致的
  2. synchronized是排它锁,而Lock可以实现共享锁,比如下文中的ReadWriteLock
  3. synchronized是非公平锁,而Lock可以实现公平锁,可以按照阻塞顺序唤醒线程
  4. synchronized要求对多个对象加锁的顺序和多个对象释放锁的顺序相反,即小括号匹配原则,而Lock允许加锁和释放锁在不同周期内,多个锁可以交错。
  5. Lock提供四种形式的加锁方式:不可中断lock()、可中断lockInterruptibly()、尝试获取tryLock()、定尝试tryLock(long time, TimeUnit unit)
  6. Lock可以提供更灵活的实现,比如非重入锁,死锁检测等

tryLock

接下来我们看看tryLock方法,提供了尝试获取锁和超时获取锁的方式,这个返回返回一个boolean值,true表示获得了锁,false表示未获得锁,我们可以使用这个特性来解决死锁问题:两个线程各自占有锁并且阻塞在获取对方锁上。我们可以在获取对方锁的时候使用tryLock,如果不能获得,则释放自己已经拥有的锁。下面是《The Java™ Tutorials》上面的一个鞠躬问题,一个人给另一个人鞠躬,直到另一个人也鞠躬才结束,假设两个人同时鞠躬,拥有了自己的锁,它们就会在等待对方的锁产生了阻塞。

static class Friend {
  private final String name;
  private final Lock lock = new ReentrantLock();

  public Friend(String name) {
    this.name = name;
  }

  public String getName() {
    return this.name;
  }

  public boolean impendingBow(Friend bower) {
    Boolean myLock = false;
    Boolean yourLock = false;
    try {
      myLock = lock.tryLock();
      yourLock = bower.lock.tryLock();
    } finally {
      if (! (myLock && yourLock)) {
        if (myLock) {
          lock.unlock();
        }
        if (yourLock) {
          bower.lock.unlock();
        }
      }
    }
    return myLock && yourLock;
  }
    
  public void bow(Friend bower) {
    if (impendingBow(bower)) {
      try {
        System.out.format("%s: %s has"
          + " bowed to me!%n", 
          this.name, bower.getName());
        bower.bowBack(this);
      } finally {
        lock.unlock();
        bower.lock.unlock();
      }
    } else {
      System.out.format("%s: %s started"
        + " to bow to me, but saw that"
        + " I was already bowing to"
        + " him.%n",
        this.name, bower.getName());
    }
  }

  public void bowBack(Friend bower) {
    System.out.format("%s: %s has" +
      " bowed back to me!%n",
      this.name, bower.getName());
  }
}

impendingBow方法利用tryLock解决了死锁问题,从这个例子再次看到了Lock的灵活性:lock和unlock是在不同方法里调用的。

lockInterruptibly()

void lockInterruptibly() throws InterruptedException;这个方法加锁阻塞会被Thread.interrupt打断,并且抛出InterruptedException异常,它是可中断的锁。

newCondition()

Condition提供了await/signal机制,正如同Object.wait/signal必须拥有对象锁(synchronized)一样,Condition是与Lock一起使用的,lock.newCondition()就是生成与当前锁关联的Condition对象,调用condition.await()方法前当前线程必须拥有lock锁。

我们来看看Condition在生产者和消费者问题上的使用,通过notFull发池子不满的信号和notEmpty来发送池子不为空的信号,JDK提供的ArrayBlockingQueue已经实现了这个功能:

class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length)
         notFull.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }

   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0)
         notEmpty.await();
       Object x = items[takeptr];
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }
   }
}

AQS:AbstractQueuedSynchronizer

Lock和Condition作为接口提供了一个很好的设计,如何实现这些接口是有难度的。

AQS编程

AbstractQueuedSynchronizer是一个实现锁和同步控制的框架,AQS是一个抽象类,实现了所有的线程阻塞和排队机制,这些实现依赖一个整型变量state,修改这个变量的值代表了同步时的获取锁和释放锁。AQS提供了protected方法供子类实现,子类通过 getState(), setState(int) 和 compareAndSetState(int, int)操作state变量的值来实现同步,我们来看这些protected方法:

/**
 * The synchronization state.
 */
private volatile int state;

// 排它锁模下尝试通过state变量获取许可
protected boolean tryAcquire(int arg) {
  throw new UnsupportedOperationException();
}

// 排它锁模式下通过设置变量state的值来反映释放许可
protected boolean tryRelease(int arg) {
  throw new UnsupportedOperationException();
}

// 共享锁模下尝试通过state变量获取许可,返回值-1表获取锁失败
protected int tryAcquireShared(int arg) {
  throw new UnsupportedOperationException();
}

// 共享锁模式下通过设置变量state的值来反映释放许可
protected boolean tryReleaseShared(int arg) {
  throw new UnsupportedOperationException();
}
protected boolean isHeldExclusively() {
  throw new UnsupportedOperationException();
}

AQS并没有实现Lock接口,它通常作为一个Lock实现类的辅助抽象类,通过AQS的实现来实现Lock接口的功能。我们来看JDK提供的一个利用AQS实现一个非重入锁的示例:

 class Mutex implements Lock, java.io.Serializable {

   // Our internal helper class
   private static class Sync extends AbstractQueuedSynchronizer {
     // Reports whether in locked state
     protected boolean isHeldExclusively() {
       return getState() == 1;
     }

     // 如果state值为0,则获得锁
     public boolean tryAcquire(int acquires) {
       assert acquires == 1; // Otherwise unused
       if (compareAndSetState(0, 1)) {
         setExclusiveOwnerThread(Thread.currentThread());
         return true;
       }
       return false;
     }

     // 释放锁,设置state值为0
     protected boolean tryRelease(int releases) {
       assert releases == 1; // Otherwise unused
       if (getState() == 0) throw new IllegalMonitorStateException();
       setExclusiveOwnerThread(null);
       setState(0);
       return true;
     }

     // Provides a Condition
     Condition newCondition() { return new ConditionObject(); }

   }

   // The sync object does all the hard work. We just forward to it.
   private final Sync sync = new Sync();

   public void lock()                { sync.acquire(1); }
   public boolean tryLock()          { return sync.tryAcquire(1); }
   public void unlock()              { sync.release(1); }
   public Condition newCondition()   { return sync.newCondition(); }
   public boolean isLocked()         { return sync.isHeldExclusively(); }
   public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
   public void lockInterruptibly() throws InterruptedException {
     sync.acquireInterruptibly(1);
   }
   public boolean tryLock(long timeout, TimeUnit unit)
       throws InterruptedException {
     return sync.tryAcquireNanos(1, unit.toNanos(timeout));
   }
 }

Mutex是一个非重入锁,要求state的值是0时表示可以获取锁,是1时表示锁被占有。Sync是一个实现了AQS的内部类,tryAcquire实现了从0到1的过程,tryRelease实现了从1到0的过程。Mutex实现了Lock接口,所以功能的实现都是通过Sync这个内部类对象来实现。

AbstractQueuedSynchronizer.ConditionObject是一个AQS类中实现了Condition接口的内部类,下文中会介绍更多利用AQS实现的。

AQS实现

我们已经知道,AQS通过state来代表同步状态,接下来我们将简要介绍AQS源码是如何实排队和阻塞机制,如果不关心细节,可以忽略这一节。

image

通过类图,我们可以得出很重要的一个编码原则:如果不希望一个方法被修改,我们应该用final来修饰。 AQS里面除了几个可以重写的方法外,其余都是final的。

我们来看看acquire方法:

public final void acquire(int arg) {
  if (!tryAcquire(arg) &&
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

如果尝试获取许可失败,则会通过addWaiter将当前线程加到FIFO等待队列的队尾:

private Node addWaiter(Node mode) {
  Node node = new Node(Thread.currentThread(), mode);
  // Try the fast path of enq; backup to full enq on failure
  Node pred = tail;
  if (pred != null) {
    node.prev = pred;
    if (compareAndSetTail(pred, node)) {
      pred.next = node;
      return node;
    }
  }
  enq(node);
  return node;
}

其中if语句是为了尝试快速入队,如果入队失败,enq方法会通过loop循环方式接着尝试入队,我们再来看看acquireQueued方法

final boolean acquireQueued(final Node node, int arg) {
  boolean failed = true;
  try {
    boolean interrupted = false;
    for (;;) {
      final Node p = node.predecessor();
      if (p == head && tryAcquire(arg)) {
        setHead(node);
        p.next = null; // help GC
        failed = false;
        return interrupted;
      }
      if (shouldParkAfterFailedAcquire(p, node) &&
        parkAndCheckInterrupt())
        interrupted = true;
    }
  } finally {
    if (failed)
      cancelAcquire(node);
  }
}

如果是队头元素,则会再次尝试tryAcquire获得许可,获取失败就会进行判断是否可以进入阻塞状态。shouldParkAfterFailedAcquire判断是否可以阻塞,parkAndCheckInterrupt就是具体阻塞方法,通过LockSuupport实现。

我们再来看看release方法:

public final boolean release(int arg) {
  if (tryRelease(arg)) {
    Node h = head;
    if (h != null && h.waitStatus != 0)
      unparkSuccessor(h);
    return true;
  }
  return false;
}

private void unparkSuccessor(Node node) {
  /*
   * If status is negative (i.e., possibly needing signal) try
   * to clear in anticipation of signalling.  It is OK if this
   * fails or if status is changed by waiting thread.
   */
  int ws = node.waitStatus;
  if (ws < 0)
    compareAndSetWaitStatus(node, ws, 0);

  /*
   * Thread to unpark is held in successor, which is normally
   * just the next node.  But if cancelled or apparently null,
   * traverse backwards from tail to find the actual
   * non-cancelled successor.
   */
  Node s = node.next;
  if (s == null || s.waitStatus > 0) {
    s = null;
    for (Node t = tail; t != null && t != node; t = t.prev)
      if (t.waitStatus <= 0)
        s = t;
  }
  if (s != null)
    LockSupport.unpark(s.thread);
}

释放许可会在队首取出Node节点唤醒,看似是一个公平锁的实现,其实未必,因为会在唤醒后锁被其它线程抢占。

关于AQS还有很多细节,包括waitStatus和ConditionObject,具体源码不往下看了,我们注意到这个队列的节点是Node,它是一个双端双向队列,被称为CLH队列。

ReentrantLock

ReentrantLock是基于AQS实现的一个重入锁和排它锁,同时他还实现了公平锁和非公平锁。我们来看看其中的内部抽象类Sync:

abstract static class Sync extends AbstractQueuedSynchronizer {
  private static final long serialVersionUID = -5179523762034025860L;

  /**
   * Performs {@link Lock#lock}. The main reason for subclassing
   * is to allow fast path for nonfair version.
   */
  abstract void lock();

  /**
   * Performs non-fair tryLock.  tryAcquire is implemented in
   * subclasses, but both need nonfair try for trylock method.
   */
  final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
      if (compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
      }
    }
    else if (current == getExclusiveOwnerThread()) {
      int nextc = c + acquires;
      if (nextc < 0) // overflow
        throw new Error("Maximum lock count exceeded");
      setState(nextc);
      return true;
    }
    return false;
  }

  protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
      throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
      free = true;
      setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
  }

  protected final boolean isHeldExclusively() {
    // While we must in general read state before owner,
    // we don't need to do so to check if current thread is owner
    return getExclusiveOwnerThread() == Thread.currentThread();
  }

  final ConditionObject newCondition() {
    return new ConditionObject();
  }

  // 略
}

每次重入都会累加state的值,Sync实现了tryRelease(releases)方法,释放相应数目的许可,当前线程不再独占锁时,通过方法setExclusiveOwnerThread设置独占线程为空,Sync还提供了一个抽象方法lock和包内方法nonfairTryAcquire供子类使用,我们来看看具体的公平锁和非公平锁的实现:

static final class NonfairSync extends Sync {
  /**
   * Performs lock.  Try immediate barge, backing up to normal
   * acquire on failure.
   */
  final void lock() {
    if (compareAndSetState(0, 1))
      setExclusiveOwnerThread(Thread.currentThread());
    else
      acquire(1);
  }

  protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
  }
}

static final class FairSync extends Sync {

  final void lock() {
    acquire(1);
  }

  /**
   * Fair version of tryAcquire.  Don't grant access unless
   * recursive call or no waiters or is first.
   */
  protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
      if (!hasQueuedPredecessors() &&
        compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
      }
    }
    else if (current == getExclusiveOwnerThread()) {
      int nextc = c + acquires;
      if (nextc < 0)
        throw new Error("Maximum lock count exceeded");
      setState(nextc);
      return true;
    }
    return false;
  }
}

NonfairSync实现的是非公平锁,tryAcquire方法调用的是nonfairTryAcquire方法,如果state未被线程占用,则当前线程占用锁,如果是被当前线程重入占用,则累加state值。基本的lock方法实现是调用acquire(1)方法,正如公平锁FairSync的lock方法实现那样,但是NonfairSync尝试调用nonfairTryAcquire之前,通过一个CAS操作希望当前线程抢先占用锁,即这一行代码:

if (compareAndSetState(0, 1))
  setExclusiveOwnerThread(Thread.currentThread());

FairSync就是一个公平的实现,当有许可可用,state为0时,许多线程尝试获取许可的时候,它会通过hasQueuedPredecessors()来判断是否有比当前线程等待更久的线程,如果没有,则尝试执行当前线程,hasQueuedPredecessors源码如下,当前线程和队首线程不一致则不允许抢占锁:

public final boolean hasQueuedPredecessors() {
  // The correctness of this depends on head being initialized
  // before tail and on head.next being accurate if the current
  // thread is first in queue.
  Node t = tail; // Read fields in reverse initialization order
  Node h = head;
  Node s;
  return h != t &&
    ((s = h.next) == null || s.thread != Thread.currentThread());
}

接下来ReentrantLock的所有Lock接口方法的实现就可以依靠这些辅助同步类了:

public class ReentrantLock implements Lock, java.io.Serializable {
  private final Sync sync;
  public ReentrantLock() {
    sync = new NonfairSync();
  }
  public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
  }
  public void lock() {
    sync.lock();
  }
  public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
  }
  public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
  }
  public boolean tryLock(long timeout, TimeUnit unit)
      throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
  }
  public void unlock() {
    sync.release(1);
  }
  public Condition newCondition() {
    return sync.newCondition();
  }
  public boolean isHeldByCurrentThread() {
    return sync.isHeldExclusively();
  }
  public final boolean isFair() {
    return sync instanceof FairSync;
  }
  // 略
}

通过源码看到,tryLock都会在非公平模式下获取锁。

ReadWriteLock 和 ReentrantReadWriteLock

ReadWriteLock拥有两个锁,一个读锁,一个写锁,如果没有写锁,读锁可以被多个线程持有,写锁是一个排它锁。

public interface ReadWriteLock {
  /**
   * Returns the lock used for reading.
   *
   * @return the lock used for reading
   */
  Lock readLock();

  /**
   * Returns the lock used for writing.
   *
   * @return the lock used for writing
   */
  Lock writeLock();
}

我们来看看JDK提供的一个使用示例:

class CachedData {
   Object data;
   volatile boolean cacheValid;
   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

   void processCachedData() {
     rwl.readLock().lock();
     if (!cacheValid) {
       // Must release read lock before acquiring write lock
       rwl.readLock().unlock();
       rwl.writeLock().lock();
       try {
         // Recheck state because another thread might have
         // acquired write lock and changed state before we did.
         if (!cacheValid) {
           data = ...
           cacheValid = true;
         }
         // Downgrade by acquiring read lock before releasing write lock
         rwl.readLock().lock();
       } finally {
         rwl.writeLock().unlock(); // Unlock write, still hold read
       }
     }

     try {
       use(data);
     } finally {
       rwl.readLock().unlock();
     }
   }
 }

我们先占有读锁去读缓存,如果无效,我们会释放读锁,占用写锁(写锁是独占锁,必须先释放读锁),操作完写之后,释放写锁,然后重新占有读锁,操作缓存数据,最后释放读锁。

ReentrantReadWriteLock是一个重入的读写锁,内部类Sync是实现了AQS的抽象类,state整型变量只能存储一个同步状态,读写锁需要两个同步状态,这里就用到了位操作,state的高16位无符号数用来记录读锁的计数,低16位无符号数用来记录写锁的计数:

/*
* Read vs write count extraction constants and functions.
* Lock state is logically divided into two unsigned shorts:
* The lower one representing the exclusive (writer) lock hold count,
* and the upper the shared (reader) hold count.
*/
static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT  = (1 << SHARED_SHIFT);
static final int MAX_COUNT    = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

/** Returns the number of shared holds represented in count  */
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count  */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

我们直接看看其中一个获取读锁的方法:

final boolean tryReadLock() {
  Thread current = Thread.currentThread();
  for (;;) {
    int c = getState();
    if (exclusiveCount(c) != 0 &&
      getExclusiveOwnerThread() != current)
      return false;
    int r = sharedCount(c);
    if (r == MAX_COUNT)
      throw new Error("Maximum lock count exceeded");
    if (compareAndSetState(c, c + SHARED_UNIT)) {
      if (r == 0) {
        firstReader = current;
        firstReaderHoldCount = 1;
      } else if (firstReader == current) {
        firstReaderHoldCount++;
      } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
          cachedHoldCounter = rh = readHolds.get();
        else if (rh.count == 0)
          readHolds.set(rh);
        rh.count++;
      }
      return true;
    }
  }
}

获取读锁前,总是会判断排它锁的计数是否为0。ReentrantReadWriteLock是通过内部维护的两个Lock对象来实现读写锁,它们都是通过Sync实现了具体功能,深入学习的可以看源码。

/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;

StampedLock

JDK1.8以后还提供了 StampedLock 来支持读的场景很频繁,写的场景比较少的一种读写锁优化。

总结

本文我们学习了更加强大的Lock锁,在实现自己的锁之前,需要深入研究AQS的机制,使用好优化的锁可以增加我们程序的性能。

运行时扫描Java注解(四)之字节码操作

Java将源码编译成面向虚拟机的字节码(ByteCode),这种程序存储格式可以实现在不同平台,不同虚拟机下运行。Oracle JVM specification

本文将对字节码作一个简单的介绍,列举一些字节码操纵工具。

ByteCode字节码

一个class文件的结构如下:

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

不妨通过一段示例来一窥字节码,下面写一段代码:

public class HelloWorld{
    public static void main(String[] args){
        System.out.println("hello,world");
    }
}

使用javac HelloWorld.java进行编译后,得到class文件:

cafe babe 0000 0034 001d 0a00 0600 0f09
0010 0011 0800 120a 0013 0014 0700 1507
0016 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 046d 6169
6e01 0016 285b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 2956 0100 0a53 6f75
7263 6546 696c 6501 000f 4865 6c6c 6f57
6f72 6c64 2e6a 6176 610c 0007 0008 0700
170c 0018 0019 0100 0b68 656c 6c6f 2c77
6f72 6c64 0700 1a0c 001b 001c 0100 0a48
656c 6c6f 576f 726c 6401 0010 6a61 7661
2f6c 616e 672f 4f62 6a65 6374 0100 106a
6176 612f 6c61 6e67 2f53 7973 7465 6d01
0003 6f75 7401 0015 4c6a 6176 612f 696f
2f50 7269 6e74 5374 7265 616d 3b01 0013
6a61 7661 2f69 6f2f 5072 696e 7453 7472
6561 6d01 0007 7072 696e 746c 6e01 0015
284c 6a61 7661 2f6c 616e 672f 5374 7269
6e67 3b29 5600 2100 0500 0600 0000 0000
0200 0100 0700 0800 0100 0900 0000 1d00
0100 0100 0000 052a b700 01b1 0000 0001
000a 0000 0006 0001 0000 0001 0009 000b
000c 0001 0009 0000 0025 0002 0001 0000
0009 b200 0212 03b6 0004 b100 0000 0100
0a00 0000 0a00 0200 0000 0300 0800 0400
0100 0d00 0000 0200 0e

参考ClassFile的结构,我们可以获得如下信息:

  • magic魔数占了u4个字节,表示文件格式(通过后缀名判断格式是错误的,因为后缀名可能会被改,类似的图片格式也会在文件头加上一些魔数表示格式)。魔数的定义是自由的,能区分开类型即可。Java字节码的魔数是cafe babe,相信这是一个浪漫的开始(引用:《深入理解JVM虚拟机》)。
  • 0000 0034分别代表了minor_version和major_version,可以看到major_version是52,可推断出是Java版本是1.8。
  • 001d即是constant_pool的大小,即constant_pool_count。

javap是java提供给我们查看字节码的命令,我们可以通过命令查看字节码javap -c HelloWorld.class > HelloWorld.javap,得到输出如下:

Compiled from "HelloWorld.java"
public class HelloWorld {
  public HelloWorld();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String hello,world
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

其中aload_0把this装载到了操作数栈中,invokespecial进行方法调用。

字节码操作类库

  1. javassist:Java Programming Assistant
    Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建。
    它提供了源码层面的API,不需要知道太多虚拟机指令的知识,就可以操纵字节码。

  2. ASM
    ASM短小精悍。cglib是基于ASM的。

  3. cglib
    cglib是一个功能强大,高性能高质量的代码生成库。
    Hibernate使用cglib为持久化类生成代理。Spring、Guice也在使用cglib。cglib比java 代理更强大的地方在于不仅可以代理接口,也可以代理普通类的方法。

  4. BCEL
    Byte Code Engineering Library (BCEL),这是Apache Software Foundation 的Jakarta 项目的一部分,需要熟悉Java指令集。

Javassist运行时扫描Java注解

关于javassist如何动态修改字节码,增加方法,操作类文件,这里不作过多介绍,我们来看看javassit读取字节码获取Java注解。

//生成ClassFile对象,对应着Java .class文件
DataInputStream dis = new DataInputStream(new BufferedInputStream(inputStream));
ClassFile cls = new ClassFile(dis);

//获取方法
cls.getMethods();

//获取属性
cls.getFields();

//获取类的Runtime注解
(AnnotationsAttribute) cls.getAttribute(AnnotationsAttribute.visibleTag)
//获取属性的Runtime注解
(AnnotationsAttribute) field.getAttribute(AnnotationsAttribute.visibleTag);
//获取属性的Class注解
(AnnotationsAttribute) field.getAttribute(AnnotationsAttribute.invisibleTag);
//获取方法的Runtime注解
(AnnotationsAttribute) method.getAttribute(AnnotationsAttribute.visibleTag);

//获取方法参数的注解
List<ParameterAnnotationsAttribute> parameterAnnotationsAttributes = Lists.newArrayList((ParameterAnnotationsAttribute) method.getAttribute(ParameterAnnotationsAttribute.visibleTag),
                (ParameterAnnotationsAttribute) method.getAttribute(ParameterAnnotationsAttribute.invisibleTag));

更多More

字节码操作让我们在更底层对Java进行处理,不仅对理解Java原理有帮助,同时对于性能优化也是个高级的主题。

不妨回到文章的开头,重新读一读《Java Virtual Machine Specification》。

命令行交互-JCommander

我喜欢简单,什么是简单?正如若干字符组成的命令行。

有时候我们用Java开发了一个小工具,希望通过命令行(CLI)或者图形界面直接调用。命令行相较于图形界面,实现迅速,交互更接近于程序员人群,本文主要介绍Java在命令行交互上的应用,我们不妨先看看命令行的两种风格:

  • POSIX风格 tar -zxvf foo.tar.gz
  • Java风格 java -Djava.awt.headless=true -Djava.net.useSystemProxies=true Foo

JCommander介绍

JCommander是Java解析命令行参数的工具,作者是cbeust,他的开源测试框架testNG相信很多程序员都有耳闻。

根据官方文档,我简单总结了JCommander的几个特点:

  • 注解驱动
    它的核心功能命令行参数定义是基于注解的,这也是我选择用它的主要原因。我们可以轻松做到命令行参数与属性的映射,属性除了是String类型,还可以是Integer、boolean,甚至是File、集合类型。

  • 功能丰富
    它同时支持文章开头的两种命令行风格,并且提供了输出帮助文档的能力(usage()),还提供了国际化的支持。

  • 高度扩展
    下文会详述。

在看具体应用示例前,我们先读懂核心注解@Parameter的源码(你大可以跳过下面这段长长的源码,直接看示例),以此来了解它向我们展示了哪些方面的能力:

@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({ FIELD, METHOD })
public @interface Parameter {

  /**
   * An array of allowed command line parameters (e.g. "-d", "--outputdir", etc...).
   * If this attribute is omitted, the field it's annotating will receive all the
   * unparsed options. There can only be at most one such annotation.
   */
  String[] names() default {};

  /**
   * A description of this option.
   */
  String description() default "";

  /**
   * Whether this option is required.
   */
  boolean required() default false;

  /**
   * The key used to find the string in the message bundle.
   */
  String descriptionKey() default "";

  /**
   * How many parameter values this parameter will consume. For example,
   * an arity of 2 will allow "-pair value1 value2".
   */
  public static int DEFAULT_ARITY = -1;
  int arity() default DEFAULT_ARITY;

  /**
   * If true, this parameter is a password and it will be prompted on the console
   * (if available).
   */
  boolean password() default false;

  /**
   * The string converter to use for this field. If the field is of type <tt>List</tt>
   * and not <tt>listConverter</tt> attribute was specified, JCommander will split
   * the input in individual values and convert each of them separately.
   */
  Class<? extends IStringConverter<?>> converter() default NoConverter.class;

  /**
   * The list string converter to use for this field. If it's specified, the
   * field has to be of type <tt>List</tt> and the converter needs to return
   * a List that's compatible with that type.
   */
  Class<? extends IStringConverter<?>> listConverter() default NoConverter.class;

  /**
   * If true, this parameter won't appear in the usage().
   */
  boolean hidden() default false;

  /**
   * Validate the parameter found on the command line.
   */
  Class<? extends IParameterValidator>[] validateWith() default NoValidator.class;

  /**
   * Validate the value for this parameter.
   */
  Class<? extends IValueValidator>[] validateValueWith() default NoValueValidator.class;

  /**
   * @return true if this parameter has a variable arity. See @{IVariableArity}
   */
  boolean variableArity() default false;

  /**
   * What splitter to use (applicable only on fields of type <tt>List</tt>). By default,
   * a comma separated splitter will be used.
   */
  Class<? extends IParameterSplitter> splitter() default CommaParameterSplitter.class;
  
  /**
   * If true, console will not echo typed input
   * Used in conjunction with password = true
   */
  boolean echoInput() default false;

  /**
   * If true, this parameter is for help. If such a parameter is specified,
   * required parameters are no longer checked for their presence.
   */
  boolean help() default false;
  
  /**
   * If true, this parameter can be overwritten through a file or another appearance of the parameter
   * @return nc
   */
  boolean forceNonOverwritable() default false;

  /**
   * If specified, this number will be used to order the description of this parameter when usage() is invoked.
   * @return
   */
  int order() default -1;
  
}

JCommander 应用示例

在一般应用场景,我们可能只需要设置@Parameter以下几个属性值:

  • names 设置命令行参数,如-old
  • required 设置此参数是否必须
  • description 设置参数的描述
  • order 设置帮助文档的顺序
  • help 设置此参数是否为展示帮助文档或者辅助功能

下面是一个完整的示例,它用来比较两份文档,然后输出差异。源码在https://github.com/Sayi/swagger-diff上。

/**
 * 
 * @author Sayi
 * @version
 */
public class CLI {

  private static final String OUTPUT_MODE_MARKDOWN = "markdown";

  @Parameter(names = "-old", description = "old api-doc location:Json file path or Http url", required = true, order = 0)
  private String oldSpec;

  @Parameter(names = "-new", description = "new api-doc location:Json file path or Http url", required = true, order = 1)
  private String newSpec;

  @Parameter(names = "-v", description = "swagger version:1.0 or 2.0", validateWith = RegexValidator.class, order = 2)
  @Regex("(2\\.0|1\\.0)")
  private String version = SwaggerDiff.SWAGGER_VERSION_V2;

  @Parameter(names = "-output-mode", description = "render mode: markdown or html", validateWith = RegexValidator.class, order = 3)
  @Regex("(markdown|html)")
  private String outputMode = OUTPUT_MODE_MARKDOWN;

  @Parameter(names = "--help", help = true, order = 5)
  private boolean help;

  @Parameter(names = "--version", description = "swagger-diff tool version", help = true, order = 6)
  private boolean v;

  public static void main(String[] args) {
    CLI cli = new CLI();
    JCommander jCommander = JCommander.newBuilder().addObject(cli).build();
    jCommander.parse(args);
    cli.run(jCommander);
  }

  public void run(JCommander jCommander) {
    if (help) {
      jCommander.setProgramName("java -jar swagger-diff.jar");
      jCommander.usage();
      return;
    }
    if (v) {
      JCommander.getConsole().println("1.2.0");
      return;
    }

    //SwaggerDiff diff = null;
  }
}

运行命令行查看帮助文档,输出结果如下:

$ java -jar swagger-diff.jar --help
Usage: java -jar swagger-diff.jar [options]
  Options:
  * -old
      old api-doc location:Json file path or Http url
  * -new
      new api-doc location:Json file path or Http url
    -v
      swagger version:1.0 or 2.0
      Default: 2.0
    -output-mode
      render mode: markdown or html
      Default: markdown
    --help

    --version
      swagger-diff tool version

这个示例像我们展示了JCommander注解的强大,我们仅仅使用注解就完成了所有参数的定义。注意,对于boolean为true的参数,我们只需要输入参数名,比如--help,而不是--help=true

示例中使用了usage()方法即可完美的输出帮助文档。

JCommander扩展:增加正则表达式校验

JCommander是高度扩展的,两个核心接口定义了扩展的能力。

IStringConverter支持String类型的参数值可以转化为任意其他类型的属性。

/**
 * An interface that converts strings to any arbitrary type.
 * 
 * If your class implements a constructor that takes a String, this
 * constructor will be used to instantiate your converter and the
 * parameter will receive the name of the option that's being parsed,
 * which can be useful to issue a more useful error message if the
 * conversion fails.
 * 
 * You can also extend BaseConverter to make your life easier.
 * 
 * @author cbeust
 */
public interface IStringConverter<T> {
  /**
   * @return an object of type <T> created from the parameter value.
   */
  T convert(String value);
}

IParameterValidator支持参数值的校验。

/**
 * The class used to validate parameters.
 *
 * @author Cedric Beust <[email protected]>
 */
public interface IParameterValidator {

  /**
   * Validate the parameter.
   *
   * @param name The name of the parameter (e.g. "-host").
   * @param value The value of the parameter that we need to validate
   *
   * @throws ParameterException Thrown if the value of the parameter is invalid.
   */
  void validate(String name, String value) throws ParameterException;

}

在阅读上文示例中,可能会有些许疑问,比如@Regex是什么注解,JCommander并没有提供正则表达式校验参数值的功能。

对于很多参数,我们都有校验的场景,比如值只能是几个可选值,或者是在一定范围内,IParameterValidator 和IParameterValidator2实现了参数校验了功能,接下来我们将基于接口IParameterValidator2扩展JCommander,同样,我们只需要使用注解即可。

  1. 自定义正则注解,这样我们就可以在需要正则校验的属性上,设置表达式,如@Regex("(2\\.0|1\\.0)")
package com.deepoove.swagger.diff.cli;

import static java.lang.annotation.ElementType.FIELD;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({ FIELD })
public @interface Regex {

  String value() default "";

}
  1. 实现RegexValidator,当有Regex注解的时候,解析正则表达式,应用校验规则。注意这段代码使用了反射,可能并不是最优雅的方式,但是在不修改JCommander源码的情况下,可能是最好的方式了
package com.deepoove.swagger.diff.cli;

import java.lang.reflect.Field;
import java.util.regex.Pattern;

import com.beust.jcommander.IParameterValidator2;
import com.beust.jcommander.ParameterDescription;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameterized;

public class RegexValidator implements IParameterValidator2 {

  private static final String PARAMETERIZED_FIELD_NAME = "field";

  @Override
  public void validate(String name, String value) throws ParameterException {
    return;
  }

  @Override
  public void validate(String name, String value, ParameterDescription pd)
      throws ParameterException {
    Parameterized parameterized = pd.getParameterized();
    Class<? extends Parameterized> clazz = parameterized.getClass();
    try {
      Field declaredField = clazz.getDeclaredField(PARAMETERIZED_FIELD_NAME);
      declaredField.setAccessible(true);
      Field paramField = (Field) declaredField.get(parameterized);
      Regex regex = paramField.getAnnotation(Regex.class);
      if (null == regex) return;
      String regexStr = regex.value();
      if (!Pattern.matches(regexStr, value)) { throw new ParameterException(
          "Parameter " + name + " should match " + regexStr + " (found " + value + ")"); }
    } catch (NoSuchFieldException e) {
      return;
    } catch (IllegalArgumentException e) {
      return;
    } catch (IllegalAccessException e) {
      return;
    }
  }
}
  1. 使用正则注解和正则校验类
@Parameter(names = "-v",  validateWith = RegexValidator.class)
@Regex("(2\\.0|1\\.0)")
private String version = "2.0";

至此,正则校验已完成。

更多More: Apache Commons CLI

从源码中可以看到,JCommander默认提供了不少转化器。

----IStringConverter
  \--BaseConverter
     --\--BigDecimalConverter
     --\--BooleanConverter
     --\--DoubleConverter
     --\--FloatConverter
     --\--IntegerConverter
     --\--ISO8601DateConverter
     --\--LongConverter
     --\--PathConverter
     --\--URIConverter
     --\--URLConverter
 \--EnumConverter
 \--InetAddressConverter
 \--FileConverter

Java在命令行交互的应用,还有很多工具。另一个使用比较广泛的是Apache Commons CLI: http://commons.apache.org/proper/commons-cli/index.html,它比JCommander支持更多的命令行风格,但是扩展能力不够。

PS: 我们一直在招人,Java,杭州,阿里系独角兽公司,E轮融资,欢迎投简历至adasai90Atgmail.com

依赖注入(二)Spring Dependency injection

Spring核心功能之一是IOC容器,Spring凭借优雅的扩展性和强大的配置功能,几乎已经让它成为服务端IOC的标杆。也正因为Spring,才有了Guice等后起之秀。

IOC 容器

我们先从一个简单的例子开始,了解一下Spring IOC容器的用法,更多的用法可以参见Spring官方文档。

  1. 写一个服务接口
package com.deepoove.diexample.service;

public interface AccountService {
  String get();
}
  1. 实现这个接口
package com.deepoove.diexample.service;

public class AccountServiceImpl implements AccountService {

  @Override
  public String get() {
    return "Sayi";
  }
}
  1. 使用XML形式配置服务的实例化
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd">

  <bean id="accountService" class="com.deepoove.diexample.service.AccountServiceImpl">
  </bean>

</beans>
  1. 容器加载XML配置,负责Bean的创建,装配和销毁。
@Test
public void testXMLConifg() {
  ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");

  AccountService accountService = context.getBean("accountService", AccountService.class);
  Assert.assertEquals(accountService.get(), "Sayi");

  context.close();

}

通过这个简单的例子,我们看到ClassPathXmlApplicationContext起了重要的作用,它负责整个Bean的生命周期,我们不妨先看看这个类关系图:
image

暂时我们无需关注细节,只需要知道BeanFactory和ApplicationContext都是接口,它们是整个容器的核心。BeanFactory类似于上一篇提到的门面Injector,提供了统一的DI接口,ApplicationContext是它的子接口,包含了它所有功能之余,还提供了更多的特性,比如AOP。

BeanFactory or ApplicationContext?
Use an ApplicationContext unless you have a good reason for not doing so.

Spring 依赖注入 与 JSR330

Spring提供了自己的注解外,也支持JSR330的注解。下表列出了它们类似的功能,当然Spring提供了等多的注解,比如@Service标识一个服务,@Component标识一个组件。

Spring JSR330
@Autowired @Inject
@Scope("singleton") @Singleton
@Qualifier @Qualifier / @Named
ObjectFactory Provider

源码分析

我们跟着ClassPathXmlApplicationContext来一步步探究下Spring DI的源码。

BeanFactory默认实现是DefaultListableBeanFactory,包含了存储所有Bean的数据结构。

private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);

核心初始化代码在AbstractApplicationContext.refresh()方法,每行代码具体作用可以阅读英文注释来了解。ApplicationContext通过持有DefaultListableBeanFactory对象获得所有BeanFactory的功能。

public void refresh() throws BeansException, IllegalStateException {
  synchronized (this.startupShutdownMonitor) {
    // Prepare this context for refreshing.
    prepareRefresh();

    // Tell the subclass to refresh the internal bean factory.
    ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

    // Prepare the bean factory for use in this context.
    prepareBeanFactory(beanFactory);

    try {
      // Allows post-processing of the bean factory in context subclasses.
      postProcessBeanFactory(beanFactory);

      // Invoke factory processors registered as beans in the context.
      invokeBeanFactoryPostProcessors(beanFactory);

      // Register bean processors that intercept bean creation.
      registerBeanPostProcessors(beanFactory);

      // Initialize message source for this context.
      initMessageSource();

      // Initialize event multicaster for this context.
      initApplicationEventMulticaster();

      // Initialize other special beans in specific context subclasses.
      onRefresh();

      // Check for listener beans and register them.
      registerListeners();

      // Instantiate all remaining (non-lazy-init) singletons.
      finishBeanFactoryInitialization(beanFactory);

      // Last step: publish corresponding event.
      finishRefresh();
    }

    catch (BeansException ex) {
      if (logger.isWarnEnabled()) {
        logger.warn("Exception encountered during context initialization - " +
            "cancelling refresh attempt: " + ex);
      }

      // Destroy already created singletons to avoid dangling resources.
      destroyBeans();

      // Reset 'active' flag.
      cancelRefresh(ex);

      // Propagate exception to caller.
      throw ex;
    }

    finally {
      // Reset common introspection caches in Spring's core, since we
      // might not ever need metadata for singleton beans anymore...
      resetCommonCaches();
    }
  }
}

接下来会对几个核心步骤作一个说明。

obtainFreshBeanFactory()创建DefaultListableBeanFactory,加载BeanDefinition

obtainFreshBeanFactory()获得了具体BeanFactory的实例,是通过如下代码实现的。

  DefaultListableBeanFactory beanFactory = createBeanFactory();
  beanFactory.setSerializationId(getId()); // 设置唯一ID
  customizeBeanFactory(beanFactory); 
  loadBeanDefinitions(beanFactory); // 读取Bean的定义,可以是XML形式,也可以是其它形式
  synchronized (this.beanFactoryMonitor) {
    this.beanFactory = beanFactory;
  }

接下来我们说明下loadBeanDefinitions方法,它的主要作用是将Beans的定义加载到BeanFactory,初始化beanDefinitionMap。

/**
 * Load bean definitions into the given bean factory, typically through
 * delegating to one or more bean definition readers.
 * @param beanFactory the bean factory to load bean definitions into
 * @throws BeansException if parsing of the bean definitions failed
 * @throws IOException if loading of bean definition files failed
 * @see org.springframework.beans.factory.support.PropertiesBeanDefinitionReader
 * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader
 */
protected abstract void loadBeanDefinitions(DefaultListableBeanFactory beanFactory)
        throws BeansException, IOException;

Spring提供了统一的接口BeanDefinitionReader来加载Bean,比如实现类XmlBeanDefinitionReader从XML中读取Bean,Spring也实现了其它的读取器,AnnotatedBeanDefinitionReader从注解读取Bean,JdbcBeanDefinitionReader提供了利用SQL从数据库读取Bean的功能。

XmlBeanDefinitionReader主要通过委托类BeanDefinitionParserDelegate实现,具体如何加载的源码本文不作过多展开。

prepareBeanFactory(beanFactory) 对容器进行标准化配置

比如设置类加载器

beanFactory.setBeanClassLoader(getClassLoader());

设置ApplicationContextAwareProcessor支持对某些特殊组件(Aware)的注入(关于BeanPostProcessor参见下文);

beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));

注册一些默认的Bean。

if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
  beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
}
if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
  beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());
}
if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
  beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
}

invokeBeanFactoryPostProcessors(beanFactory) 调用BeanFactoryPostProcessors

Spring容器的扩展点BeanFactoryPostProcessors提供了在BeanDefinition已加载,还没有任何Bean被实例化的时候操作BeanFactory的能力。

registerBeanPostProcessors(beanFactory); 注册BeanPostProcessor

Spring容器的扩展点BeanPostProcessor提供了Bean在调用初始化方法(InitializingBean's {@code afterPropertiesSet或者init-method)前后操作Bean的能力。

public interface BeanPostProcessor {

  @Nullable
  default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    return bean;
  }

  @Nullable
  default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    return bean;
  }

}

BeanPostProcessor为Spring提供了优秀的扩展能力,AOP(AspectJAwareAdvisorAutoProxyCreator)和@Autowired(AutowiredAnnotationBeanPostProcessor)的特性都是基于此扩展的插件。

image

在业务开发中,经常需要在关键流程记录日志,代码可能会是这样:

Log logger = LogFactory.getLog(getClass());

我们可以实现BeanPostProcessor接口,在实例化Bean后通过反射获取到logger对象,进而实例化logger来避免频繁的写这串日志变量声明代码。

initApplicationEventMulticaster()初始化事件分发 和 registerListeners()注册监听器

默认广播实现是SimpleApplicationEventMulticaster类,Spring监听事件采用观察者模式,我们可以注册自己的Listener监听器,也可以自定义事件Event。

finishBeanFactoryInitialization(beanFactory) 实例化非Lazy的单例Bean

我们直接看核心代码AbstractAutowireCapableBeanFactory.doCreateBean方法,作用是创建Bean。

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
    throws BeanCreationException {

  // Instantiate the bean.
  BeanWrapper instanceWrapper = null;
  if (mbd.isSingleton()) {
    instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
  }
  if (instanceWrapper == null) {
    instanceWrapper = createBeanInstance(beanName, mbd, args);
  }
  final Object bean = instanceWrapper.getWrappedInstance();
  Class<?> beanType = instanceWrapper.getWrappedClass();
  if (beanType != NullBean.class) {
    mbd.resolvedTargetType = beanType;
  }

  // Allow post-processors to modify the merged bean definition.
  synchronized (mbd.postProcessingLock) {
    if (!mbd.postProcessed) {
      try {
        applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
      }
      catch (Throwable ex) {
        throw new BeanCreationException(mbd.getResourceDescription(), beanName,
            "Post-processing of merged bean definition failed", ex);
      }
      mbd.postProcessed = true;
    }
  }

  // Eagerly cache singletons to be able to resolve circular references
  // even when triggered by lifecycle interfaces like BeanFactoryAware.
  boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
      isSingletonCurrentlyInCreation(beanName));
  if (earlySingletonExposure) {
    if (logger.isDebugEnabled()) {
      logger.debug("Eagerly caching bean '" + beanName +
          "' to allow for resolving potential circular references");
    }
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
  }

  // Initialize the bean instance.
  Object exposedObject = bean;
  try {
    populateBean(beanName, mbd, instanceWrapper);
    exposedObject = initializeBean(beanName, exposedObject, mbd);
  }
  catch (Throwable ex) {
    if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
      throw (BeanCreationException) ex;
    }
    else {
      throw new BeanCreationException(
          mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
    }
  }

  if (earlySingletonExposure) {
    Object earlySingletonReference = getSingleton(beanName, false);
    if (earlySingletonReference != null) {
      if (exposedObject == bean) {
        exposedObject = earlySingletonReference;
      }
      else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
        String[] dependentBeans = getDependentBeans(beanName);
        Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
        for (String dependentBean : dependentBeans) {
          if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
            actualDependentBeans.add(dependentBean);
          }
        }
        if (!actualDependentBeans.isEmpty()) {
          throw new BeanCurrentlyInCreationException(beanName,
              "Bean with name '" + beanName + "' has been injected into other beans [" +
              StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
              "] in its raw version as part of a circular reference, but has eventually been " +
              "wrapped. This means that said other beans do not use the final version of the " +
              "bean. This is often the result of over-eager type matching - consider using " +
              "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
        }
      }
    }
  }

  // Register bean as disposable.
  try {
    registerDisposableBeanIfNecessary(beanName, bean, mbd);
  }
  catch (BeanDefinitionValidationException ex) {
    throw new BeanCreationException(
        mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
  }

  return exposedObject;
}

其中earlySingletonExposure是为了解决循环依赖的问题,我们先把重点放到以下三行代码:

  1. instanceWrapper = createBeanInstance(beanName, mbd, args);用来创建具体对象实例
  2. populateBean(beanName, mbd, instanceWrapper);用来填充对象的属性
  3. initializeBean(beanName, exposedObject, mbd);调用初始化方法

createBeanInstance最核心的代码如下:

// Need to determine the constructor...
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null ||
    mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR ||
    mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args))  {
  return autowireConstructor(beanName, mbd, ctors, args);
}

// No special handling: simply use no-arg constructor.
return instantiateBean(beanName, mbd);

实现原理是通过反射获得构造器,然后创建实例。此处采用了策略模式,实例化对象的接口为InstantiationStrategy,两个实现类SimpleInstantiationStrategy和CglibSubclassingInstantiationStrategy(支持Method Injection)。

反射的代码就很简单了,在BeanUtils中提供了静态方法instantiateClass:

try {
  ReflectionUtils.makeAccessible(ctor);
  return (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass()) ?
      KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args));
}

populateBean注入的核心代码在AbstractAutowireCapableBeanFactory.autowireByType,通过DefaultListableBeanFactory.resolveDependency解析依赖对象,PropertyDescriptor注入属性值。

PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName);
// Don't try autowiring by type for type Object: never makes sense,
// even if it technically is a unsatisfied, non-simple property.
if (Object.class != pd.getPropertyType()) {
  MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd);
  // Do not allow eager init for type matching in case of a prioritized post-processor.
  boolean eager = !PriorityOrdered.class.isInstance(bw.getWrappedInstance());
  DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager);
  Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter);
  if (autowiredArgument != null) {
    pvs.add(propertyName, autowiredArgument);
  }
  for (String autowiredBeanName : autowiredBeanNames) {
    registerDependentBean(autowiredBeanName, beanName);
    if (logger.isDebugEnabled()) {
      logger.debug("Autowiring by type from bean name '" + beanName + "' via property '" +
          propertyName + "' to bean named '" + autowiredBeanName + "'");
    }
  }
  autowiredBeanNames.clear();
}

关于Aware注入

在源码分析中,提到过ApplicationContextAwareProcessor类,它实现了BeanPostProcessor接口,在Bean调用初始化方法前,支持对一些实现了Aware接口的Bean的某些特定属性的注入。

if (bean instanceof Aware) {
  if (bean instanceof EnvironmentAware) {
    ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
  }
  if (bean instanceof EmbeddedValueResolverAware) {
    ((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver);
  }
  if (bean instanceof ResourceLoaderAware) {
    ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);
  }
  if (bean instanceof ApplicationEventPublisherAware) {
    ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);
  }
  if (bean instanceof MessageSourceAware) {
    ((MessageSourceAware) bean).setMessageSource(this.applicationContext);
  }
  if (bean instanceof ApplicationContextAware) {
    ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
  }
}

image

更多:扩展点FactoryBean

org.springframework.beans.factory.FactoryBean和BeanFactory只是两个单词的顺序不同,但它们绝对不是一样的东西。

factory-bean提供了通过工厂和工厂方法实例化Bean的xml配置方式。

<!-- the factory bean, which contains a method called createInstance() -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
    <!-- inject any dependencies required by this locator bean -->
</bean>

<!-- the bean to be created via the factory bean -->
<bean id="clientService"
    factory-bean="serviceLocator"
    factory-method="createClientServiceInstance"/>

org.springframework.beans.factory.FactoryBean为我们提供了生成Bean的工厂方式的接口。

public interface FactoryBean<T> {

  T getObject() throws Exception;

  Class<?> getObjectType();

  boolean isSingleton();

}

getObject是具体实例化的Bean,getObjectType是Bean的类型,我们实现这个方法来实例化我们所需要的所有对象。
Spring很多地方使用了FactoryBean,比如ListFactoryBean、MapFactoryBean和spring-remoting模块:HttpInvokerProxyFactoryBean、HessianProxyFactoryBean,这里就不作过度延伸了。

循环依赖问题和@lazy

Spring解决了单例下属性注入的循环依赖问题。对于构造器依赖,或者非单例情况下,可以使用@lazy注解延迟实例化。

关于Spring循环依赖,这里有篇英文文章,写的很易懂。http://www.baeldung.com/circular-dependencies-in-spring

我们先来分析下原理,单例下属性注入在上文提到的核心三行代码已经看得很清楚,采取提前暴露对象+延迟注入属性的方式,先实例化所有单例实例,再对属性进行注入,对于prototype类型的实例,或者构造器循环依赖就会抛出BeanCurrentlyInCreationException异常。

@lazy的核心原理是采取了动态代理的方式,当构造器依赖一个@lazy修饰的Bean时,会优先返回这个Bean的动态代理,而将这个Bean的实际初始化工作延迟,Spring的代理的核心类是ProxyFactory,会在我的另一篇文章《面向切面编程AOP》中详细阐述。

解析@lazy注解的入口在DefaultListableBeanFactory.resolveDependency,源码如下:

  Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
        descriptor, requestingBeanName);

生成代理的方法源码如下:

protected Object buildLazyResolutionProxy(final DependencyDescriptor descriptor, final @Nullable String beanName) {
  Assert.state(getBeanFactory() instanceof DefaultListableBeanFactory,
      "BeanFactory needs to be a DefaultListableBeanFactory");
  final DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) getBeanFactory();
  TargetSource ts = new TargetSource() {
    @Override
    public Class<?> getTargetClass() {
      return descriptor.getDependencyType();
    }
    @Override
    public boolean isStatic() {
      return false;
    }
    @Override
    public Object getTarget() {
      Object target = beanFactory.doResolveDependency(descriptor, beanName, null, null);
      if (target == null) {
        Class<?> type = getTargetClass();
        if (Map.class == type) {
          return Collections.emptyMap();
        }
        else if (List.class == type) {
          return Collections.emptyList();
        }
        else if (Set.class == type || Collection.class == type) {
          return Collections.emptySet();
        }
        throw new NoSuchBeanDefinitionException(descriptor.getResolvableType(),
            "Optional dependency not present for lazy injection point");
      }
      return target;
    }
    @Override
    public void releaseTarget(Object target) {
    }
  };
  ProxyFactory pf = new ProxyFactory();
  pf.setTargetSource(ts);
  Class<?> dependencyType = descriptor.getDependencyType();
  if (dependencyType.isInterface()) {
    pf.addInterface(dependencyType);
  }
  return pf.getProxy(beanFactory.getBeanClassLoader());
}

总结

至此,我们对Spring的容器和Bean的创建过程进行了源码分析,正因为Spring优秀的扩展机制,当我们了解这些机制后,可以为技术赋能,实现更高级的功能。

下一篇我们将重点解读google guice框架,它同样优秀,还轻量级,待续。

面向切面编程(一)之AOP和代理模式

什么是AOP

AOP全称为Aspect Oriented Programming。

AOP的关注点是在切面,它的核心单元是Aspect,面向对象编程中核心单元是类Class。

AOP为我们从切面角度去解决一类问题,比如数据库事务、日志、安全性等。

AOP Alliance

Java许多通用技术都有一套规范,AOP也不例外。AOP Alliance定义了一组AOP技术的基础API,所有实现AOP功能的框架都应该遵守这个约定。

org.aopalliance.aop包定义了一个最通用的接口Advice,拦截器都集成于它。

package org.aopalliance.aop;
public interface Advice {
}

org.aopalliance.intercept包定义了拦截机制所需要的一些列接口,主要分为两类,一类是拦截器Interceptor,一类是连接点Joinpoint,下表是官方的javadoc:

拦截器接口 功能
Interceptor This interface represents a generic interceptor.
MethodInterceptor Intercepts calls on an interface on its way to the target.
ConstructorInterceptor Intercepts the construction of a new object.
FieldInterceptor Intercepts field access on a target object.
连接点接口 功能
Joinpoint This interface represents a generic runtime joinpoint (in the AOP terminology).
Invocation This interface represents an invocation in the program.
MethodInvocation Description of an invocation to a method, given to an interceptor upon method-call.
ConstructorInvocation Description of an invocation to a constuctor, given to an interceptor upon construtor-call.
FieldAccess This interface represents a field access in the program.

连接点可以获取到切入点的信息,比如具体切入的方法等。拦截器是用户自己定义,通过Joinpoint的procced方法执行原先的代码或者指向执行下一个拦截器链。一个可能的拦截器代码如下:

 class DebuggingInterceptor implements MethodInterceptor, 
     ConstructorInterceptor, FieldInterceptor {

   Object invoke(MethodInvocation i) throws Throwable {
     debug(i.getMethod(), i.getThis(), i.getArgs());
     return i.proceed();
   }

   Object construct(ConstructorInvocation i) throws Throwable {
     debug(i.getConstructor(), i.getThis(), i.getArgs());
     return i.proceed();
   }
 
   Object get(FieldAccess fa) throws Throwable {
     debug(fa.getField(), fa.getThis(), null);
     return fa.proceed();
   }

   Object set(FieldAccess fa) throws Throwable {
     debug(fa.getField(), fa.getThis(), fa.getValueToSet());
     return fa.proceed();
   }

   void debug(AccessibleObject ao, Object this, Object value) {
     ...
   }
 }

代理模式

看完AOP规范,我们考虑如何来实现AOP,答案是代理模式:代理模式通过代理类对真实对象进行代理,这样我们就可以在真实对象的调用前后,执行自己的代码,这就是切面。

image

JDK动态代理

Java提供了java.lang.reflect.Proxy.newProxyInstance静态方法实现代理模式,这种代理叫做动态代理。

ApectJ提供了语言方面的支持,它是Java语言的一个扩展。静态代理模式是在编码过程中就已明确了代理结构,这里不作讨论。

public static Object newProxyInstance(ClassLoader loader,
  Class<?>[] interfaces,
  InvocationHandler h)
throws IllegalArgumentException
  • 参数ClassLoader定义了类加载器,可以通过Thread.currentThread().getContextClassLoader()指定为当前加载器
  • 参数interfaces限定了代理接口,所以JDK动态代理只支持接口,不支持没有类
  • 参数InvocationHandler则是具体需要实现的调用处理器,他的定义如下:
public interface InvocationHandler {
 public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable;
}

其中proxy是代理对象,它的名称可能是$Proxy1,method则是具体要拦截的方法,args是方法的参数。

Cglib动态代理

如果需要对类代理,我们可以使用字节码工具,比如cglib,Spring的AOP也是利用了cglib对类进行代理。

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(new net.sf.cglib.proxy.MethodInterceptor() {
   
   @Override
   public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
       throws Throwable {
     
   }
});
Object instance = enhancer.create();
  • 参数Obj表示代理对象,它的名称可能是这样的:XXXService$$EnhancerByCglib$9e453df32
  • method是被拦截的方法
  • args是参数
  • MethodProxy可以调用未拦截的方法

注意:cglib是基于继承特性,所以无法对final的类或者final的方法进行代理

javassist动态代理

javassit是另一个简单的字节码框架,它生产动态代理也很简单,下面直接给出代码:

ProxyFactory f = new ProxyFactory();
f.setSuperclass(key.getType());
Class<?> c = f.createClass();

Object object = c.newInstance();
((javassist.util.proxy.Proxy)object).setHandler(new MethodHandler() {
  
  @Override
  public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args)
      throws Throwable {
    
  }
});
return object;

手动实现一个AOP框架

理解了代理模式后,我们就可以实现一个自己的AOP框架了。

准备设计一个代理工厂用来生成代理对象,封装JDK动态代理和Cglib动态代理,拦截器链的调用则是通过连接点JoinPoint实现的,因为我们主要实现对方法的拦截,所以我们会实现接口MethodInvocation,通过proceed方法实现拦截器的逐一调用。

实现ProxyFactory

直接看源码,提供了两个构造器,一个是对接口的代理,一个对对象的代理,然后提供一个getProxy方法获得代理对象,具体代理策略参见getProxy方法。

package com.deepoove.liteinject.aop;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;

import org.aopalliance.intercept.MethodInterceptor;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodProxy;

public class ProxyFactory {
  
  private Object target;
  private Class<?> interfaceClass;
  
  private List<MethodInterceptor> advisor;
  
  public ProxyFactory(Class<?> clazz, MethodInterceptor... interceptors) {
    this.interfaceClass = clazz;
    if (null != interceptors) this.advisor = Arrays.asList(interceptors);
  }
  
  
  public ProxyFactory(Object obj, MethodInterceptor... interceptors) {
    this.target = obj;
    if (null != interceptors) this.advisor = Arrays.asList(interceptors);
  }
  
  
  public Object getProxy(){
     if (null != interfaceClass && interfaceClass.isInterface()) {
       //jdk dynamic proxy
       return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{interfaceClass}, new InvocationHandler() {
         @Override
         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
           ReflectMethodInvocation methodInvocation = new ReflectMethodInvocation(method, args,  null, new Pointcut(), advisor);
           return methodInvocation.proceed();
         }
       });
     }else{
       //bytecode dynamic proxy
       Enhancer enhancer = new Enhancer();
       enhancer.setSuperclass(target.getClass());
       enhancer.setCallback(new net.sf.cglib.proxy.MethodInterceptor() {
         
         @Override
         public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
             throws Throwable {
           CglibMethodInvocation  methodInvocation = new CglibMethodInvocation(method, args, target, proxy,  new Pointcut(), advisor);
           return methodInvocation.proceed();
         }
       });
       return enhancer.create();
       
     }
  }
}

实现ReflectMethodInvocation和CglibMethodInvocation

ReflectMethodInvocation和CglibMethodInvocation都是对接口MethodInvocation的实现,他们的代码大同小异,这里看下ReflectMethodInvocation的代码。

package com.deepoove.liteinject.aop;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Method;
import java.util.List;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class ReflectMethodInvocation implements MethodInvocation {

  private Object obj;
  private Pointcut pointcut;
  private List<MethodInterceptor> interceptors;

  private Method method;
  private Object[] args;
  
  private int i = 0;

  public ReflectMethodInvocation(Method method, Object[] args, Object newInstance,
      Pointcut pointcut, List<MethodInterceptor> interceptors) {
    this.method = method;
    this.args = args;
    this.obj = newInstance;
    this.pointcut = pointcut;
    this.interceptors = interceptors;
  }

  @Override
  public Object[] getArguments() {
    return args;
  }

  @Override
  public Object proceed() throws Throwable {
    if (!pointcut.matcher(method)) return method.invoke(obj, args);
    
    if (i == interceptors.size()) return method.invoke(obj, args);
    
    return interceptors.get(i++).invoke(this);
  }

  @Override
  public Object getThis() {
    return obj;
  }

  @Override
  public AccessibleObject getStaticPart() {
    return null;
  }

  @Override
  public Method getMethod() {
    return null;
  }

}

它的核心方法就是proceed,实现了拦截器链的调用,在方法的执行前后,依次执行拦截器前置方法代码,再执行具体方法,再反过来依次执行拦截器后置方法代码。

关于Pointcut类这里无需关注,它代表一个切入点,表示是否需要拦截,我们姑且认为pointcut.matcher(method)会返回True,即拦截所有方法。

接下来,我们可以写个单元测试来验证下,先对一个接口动态代理,然后对一个对象进行代理:

@Test
public void testProxyFactory() {
  
  UserService proxy = (UserService)new ProxyFactory(UserService.class, new MethodInterceptor() {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
      System.out.println("jdk AOP .");
      return null;
    }
  }).getProxy();
  
  System.out.println(proxy.get(""));
  
  UserService userService = new LiteService();
  UserService proxy2 = (UserService)new ProxyFactory(userService, new MethodInterceptor() {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
      System.out.println("cglib AOP Before.");
      Object proceed = invocation.proceed();
      System.out.println("cglib AOP After.");
      return proceed;
    }
  }).getProxy();
  
  User user = proxy2.get("id");
  System.out.println(user.getName());
}

输出结果如下:

jdk AOP .
null
cglib AOP Before.
cglib AOP After.
Sayi

至此,我们已经完成了一个符合AOP规范的AOP框架。

为DI框架增加AOP功能

AOP作为依赖注入框架的完善,很多DI框架都提供了AOP的功能,回到以前的一篇文章《依赖注入(一)实现一个简单的DI框架》,我们为lite-inject框架实现AOP功能。

在DI实现AOP,主要需要做到两步,指定哪些类,哪些方法(切入点)需要切面操作,并且绑定到具体的拦截器,其次就是在Bean生成的时候,可以生成代理类。

切入点的绑定

首先我们定义一个切入点类,表示哪些类,哪些用了注解的类,哪些用了注解的方法可以被切入,类名是Pointcut。

package com.deepoove.liteinject.aop;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Pointcut {

  private List<Class<?>> clazzes = new ArrayList<>();
  private List<Class<? extends Annotation>> clazzAnnotationes = new ArrayList<>();;

  private List<Class<? extends Annotation>> methodAnnotationes = new ArrayList<>();;

  public Pointcut() {}

  public Pointcut clazz(Class<?> clazz) {
    clazzes.add(clazz);
    return this;
  }

  public Pointcut clazzOrAnnotatedWith(Class<? extends Annotation> anno) {
    clazzAnnotationes.add(anno);
    return this;
  }

  public Pointcut methodAnnotatedWith(Class<? extends Annotation> clazz) {
    methodAnnotationes.add(clazz);
    return this;
  }

  public boolean matcher(Class<?> clazz) {
    if (null != clazzes && -1 != clazzes.indexOf(clazz)) return true;
    if (null == clazzAnnotationes || clazzAnnotationes.isEmpty()) return false;
    Annotation[] annotations = clazz.getAnnotations();
    if (null == annotations || annotations.length == 0) return false;
    return new ArrayList<>(clazzAnnotationes).retainAll(Arrays.asList(annotations));
  }

  public boolean matcher(Method method) {
    if (null == methodAnnotationes || methodAnnotationes.isEmpty()) return true;
    Annotation[] annotations = method.getAnnotations();
    if (null == annotations || annotations.length == 0) return false;
    return new ArrayList<>(methodAnnotationes).retainAll(Arrays.asList(annotations));
  }

}

定义好切入点,我们就可以在InjectModule中绑定切入点和拦截器了:

bindInterceptor(new Pointcut().clazz(LoginService.class).clazzOrAnnotatedWith(Log.class).methodAnnotatedWith(Log.class), new MethodInterceptor() {
  
  @Override
  public Object invoke(MethodInvocation invocation) throws Throwable {
    System.out.println("AOP Before.");
    Object proceed = invocation.proceed();
    System.out.println("AOP After.");
    return proceed;
  }
});

bindInterceptor(new Pointcut().clazz(LoginServiceImpl.class).methodAnnotatedWith(Log.class), new MethodInterceptor() {
  @Override
  public Object invoke(MethodInvocation invocation) throws Throwable {
    System.out.println("cgLib AOP Before.");
    Object proceed = invocation.proceed();
    System.out.println("cgLib AOP After.");
    return proceed;
  }
});

第一步代码的切入点是接口LoginService或者使用了注解@log的类,方法是使用了注解@log的方法;
第二步代码的切入点是类LoginServiceImpl,方法是使用了注解@log的方法。

为Bean生成代理

在生成Bean的时候,我们就可以对先前写的AOP框架进行改造,生成代理了。

Object createAopProxy(Key<?> key, Object newInstance, Map<Pointcut, List<MethodInterceptor>> advisor) {
  for (Entry<Pointcut, List<MethodInterceptor>> entry : advisor.entrySet()) {
    Pointcut pointcut = entry.getKey();
    List<MethodInterceptor> interceptors = entry.getValue();
    if (pointcut.matcher(key.getType())){
      if (key.getType().isInterface()) {
        // jdk proxy
        return getJdkProxyInstance(key, newInstance, pointcut, interceptors);
      } else {
        //cglib proxy
        return getCgLibProxyInstance(key, newInstance, pointcut, interceptors);  
      }
    }
  }
  return newInstance;
}

从现在开始lite-inject也是一个专注于DI和AOP的小型框架了。

总结

技术的实现是百花齐放,所以一定要有约定,AOP盟约统一了AOP实现的规范,理解这个规范对理解AOP很有好处。

下篇文章将会对Spring AOP和Guice AOP的源码进行解读,待续。

函数式Java编程

函数式编程是使用像数学里面的函数来编程,函数是给定了输入值,返回结果的运算,函数应该相互独立,不会影响函数外的变量,即无副作用,对于同样的输入值,多次调用函数的结果应该是一致的,即引用透明性。

Haskell就是这样一门语言,它用数学函数描述这个世界,被称为纯函数编程语言,我们来看看这样一段代码:

f = 3 

在函数式编程中,函数是中心概念,上面这段代码其实是定义了一个常函数,它并不是一个赋值,因为Haskell中的每个函数都是数学意义上的函数(即“纯粹”),即使是副作用的IO操作也只是对纯代码生成的操作的描述,没有语句或指令。

相比于面向对象编程是在描述“如何做”(命令式编程),函数式编程在描述“做什么”的问题(声明式编程),Java8将函数作为一等公民,我们可以改变下自己的**,利用函数来编程。

行为参数化、函数对象和策略模式

行为参数化是将行为作为参数传递,在没有函数式编程时,我们已经这样去做了:我们可以在一个对象方法内,调用参数对象的方法。我们来看看对一个List进行排序的代码:

List<String> words = Arrays.asList("hello", "sayi", "hi");
words.sort(new Comparator<String>() {
    public int compare(String str1, String str2) {
        return Integer.compare(str1.length(), str2.length());
    }
});

排序的逻辑就是一种行为,匿名类是面向对象设计中表示行为参数化的一种方式,我们把这种只有一个方法的接口称为函数类型,实现称为函数对象,Comparator称为抽象策略,不同的实现方式表示了不同的具体策略,函数对象即是具体的策略实现。

这样的写法总要生成一个类的对象,然后传递对象参数,调用参数的方法,如果能直接传递函数:Int f(x,y),代码不是更贴切吗?

表示行为:@FunctionalInterface函数式接口和lambda表达式

只有一个方法的接口(允许有默认方法)以前称之为函数类型(function types),Java8将这些接口用注解@FunctionalInterface标注,它们有了一个正式的名字:函数接口(function interface)。Comparator正是函数式接口,所有函数式接口都可以声明函数,这个函数也可以被传递,即参数化。

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

函数使用lambda表达式来表示,我们可以通过下面代码声明一个比较根据字符串长度比较的函数:

Comparator<String> func = (str1, str2) -> Integer.compare(str1.length(), str2.length());

lambda表达式无法单独存在,必须是一个函数式接口类型,具体是什么函数式接口,需要给出明确定义或者在参数中进行类型推导来确定,上面这个lambda表达式是无法赋值给一个Object对象的,func是一个函数的定义,它不应该被理解为对象。

接下来我们使用lambda表达式改写一下排序的代码:

words.sort((str1, str2) -> Integer.compare(str1.length(), str2.length()));

到这里我们可以声明一个函数,并且传递函数了。可能有人有疑问,因为所有事物都是对象,它不就是用lambda表示了一个函数对象,传递的仍然是对象吗?的确,JAVA语言因为背负了面向对象的历史包袱,背后的设计逻辑可能如此,但是它给了我们函数式编程思考的能力,我觉得更应该这样理解:它是传递了函数,而不是传递了对象,上文中的func变量,更应该理解为是个函数定义,而不是一个对象

java.util.function:通用函数式接口

当我们编写代码,需要传递函数时,我们是新写一个函数式接口吗?不是的,java.util.function提供了43个常用函数定义,作为通用目的的函数式接口,主要分为六类(参见《Effective Java》)。

接口 函数签名 示例
Function<T,R> R apply(T t) Arrays::asList
Predicate boolean test(T t) Collection::isEmpty
Consumer void accept(T t) String::toLowerCase
Supplier T get() Instant::now
UnaryOperator T apply(T t) System.out::println
BinaryOperator T apply(T t1, T t2) BigInteger::add

Function是最基础的函数式接口,其余函数式接口可以用Function来表示,比如Function<T, Void>和Consumer是等价的,但是Consumer的名称更能体现这个函数的含义(消费处理某个元素),所以增加了这个函数式接口。

根据这六类通用函数定义,还有很多变种:

  1. 泛型是不允许使用原生类型的,提供了很多原生类型(int、long、double)的变种,比如:IntFunction、LongPredicate、DoubleConsumer、LongToDoubleFunction、ToIntFunction、BooleanSupplier等。 永远优先使用原生类型函数,而不是通过原生类型的包装类来使用通用函数,因为包装需要代价。
  2. 比如两个参数版本的:BiFunction<T, U, R>、BiPredicate<T, U>、BiConsumer<T,U>、ObjIntConsumer、ToDoubleBiFunction<T,U>等。

我们在设计时应该尽量使用这些通用的函数式接口,但是也有一些极少情况需要自定义函数式接口,比如JDK的Comparator,尽管和ToIntBiFunction<T,T>是等价的,但是我们有理由去定义这样一个新的接口:比较操作是一个通用的函数,并且Comparator是个有意义的命名,我们还可以在Comparator的接口中定义相关的接口默认方法。

有含义的函数式接口

Java提供了一些有含义的函数式接口。

  • Comparator
  • Runnable
  • Callable
  • java.io.FileFilter
  • java.io.FilenameFilter
  • java.util.logging.Filter

这些是极少没有使用通用函数式接口的例子,它至少满足了下面两个理由:

  1. 通用的操作,用命名来表示操作含义
  2. 接口实现相关的默认方法

Method References:方法引用

方法引用作为lambda表达式更简洁的表示方式,通过有意义的方法名,我们可以更容易阅读代码:

// words.sort((str1, str2) -> Integer.compare(str1.length(), str2.length()));
words.sort(Comparator.comparingInt(String::length));

其中Comparator.comparingInt是接口的默认实现方法:

public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) {
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T> & Serializable)
        (c1, c2) -> Integer.compare(keyExtractor.applyAsInt(c1), keyExtractor.applyAsInt(c2));
}

我们再来看个方法引用的简单例子,它是把两个整数相加,可以利用Integer提供的sum方法作为函数传递:

process((a, b) -> a + b);

process(Integer::sum);

方法引用在语义上更通俗,Integer::sum就是一个函数,我们传递了一个加法的函数,相比较lambda表达式,应该优先使用方法引用

方法引用是lambda的简洁表示,我们来看看方法引用的种类有哪些和它们对应的lambda表达式语法:

种类 方法引用 lambda
引用静态方法ContainingClass::staticMethodName Integer::parseInt str -> Integer.parseInt(str)
引用对象的某个方法containingObject::instanceMethodName Instant.now()::isAfter Instant then = Instant.now(); t -> then.isAfter(t)
引用参数类型的对象方法ContainingType::methodName String::toLowerCase str -> str.toLowerCase()
引用构造函数ClassName::new 增加元素ClassName::new TreeMap<K,V>::new

需要注意的是,String::toLowerCase和str -> str.toLowerCase()是等价的

构造器方法引用

我们可以使用::new方式引用构造器,比如:

// () -> new HashMap<String, String>()
Supplier<Map<String, String>> supplier = HashMap::new;
Map<String, String> map = supplier.get();

但是构造方法会被重载,比如我们希望引用参数为int类型的HashMap构造函数,该怎么办呢?

我们知道,lambda表达式函数的类型是需要明确声明或者类型推导的,所以我们可以声明一个需要函数类型为IntFunction<Map<String, String>>,这个函数类型就会对new HashMap(int)构造函数引用,代码如下:

// (initialCapacity) -> new HashMap<String, String>(initialCapacity)
IntFunction<Map<String, String>> func = HashMap::new;
Map<String, String> map = func.apply(32);

综上,重载方法的引用,可以通过函数类型来确定具体的方法。

总结

使用对象思维和函数思维的实现偏差会很大,比如加减乘除,我们先来看看面向对象的写法:

public enum Operation {
    PLUS("+") {
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        public double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES("*") {
        public double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE("/") {
        public double apply(double x, double y) {
            return x / y;
        }
    };
    private final String symbol;

    Operation(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        return symbol;
    }

    public abstract double apply(double x, double y);
}

函数式编程就会将这些操作声明为函数,最后来看看函数式写法:

public enum Operation {
    PLUS("+", (x, y) -> x + y), 
    MINUS("-", (x, y) -> x - y), 
    TIMES("*", (x, y) -> x * y), 
    DIVIDE("/", (x, y) -> x / y);
    
    private final String symbol;
    private final DoubleBinaryOperator op;

    Operation(String symbol, DoubleBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }

    @Override
    public String toString() {
        return symbol;
    }

    public double apply(double x, double y) {
        return op.applyAsDouble(x, y);
    }
}

函数式编程,可以改变我们既往的编码习惯,比如模板设计模式中的抽象方法可以通函数参数来实现,Java的Stream编程中大量使用了函数式编程的**。

总之,Java在面向对象编程语言里面,为函数式编程打开了大门,是时候使用函数**来编程了。

Good Game 工程师的平台

今年来我一直在思考一个问题:作为工程师,你一直在忍受着什么?

是公司流程冗余,每个人的利益各不相同?是产品功能复杂到越来越不满意,缺乏内心的满足感?忍受着每个人的埋怨,经济给你带来的压力?...
当我不断的思考我们忍受的事情,我发现我忽略了问题中重要的一点:作为工程师。是的,我们是工程师,工程师遇到的问题得用工程师思维去解决。

有趣的灵魂终会相遇

和吉米商量这件事情的时候,我们一拍即合,通过一次团队周会,我们明确了beta版本中的功能。

Good Game

image

前端技术:Ant-Design-Pro
服务端:sslb(三十六变,基于公司框架开发)

项目开始的时候,我起了个名字,叫做Good Game,简称GG,寓意无论何时,我们应该热爱我们所从事的事业,并且commit了第一行信息:Of course i still love this game

我们选用了蚂蚁金服的ant-design为前端框架,因此我花了一丢丢时间更新了整个前端技术栈(对于年轻人来人,学习一门新技术根本算不上挑战)。

代码质量

image

工程师应该关注自己的代码质量,并且正视那些潜在的问题,我们基于sonar做了一个大屏,对代码中的bugs、坏味道、重复行数进行展示。

开发者数据

image
image

吉米负责了整个Git数据的分析,最终呈现的效果也很有趣,我们可以通过周视图看到每个人提交的代码量和提交次数,如果不是排第一的小伙子那周最后时刻改了几个BUG,我相信我会勇夺第一。

周报

周报对于大家的痛点比较大,我一直坚信写周报是限制了工程师的自由。但反过来说,周报也能反映本周的一些情况,所以我们不排斥。我们推崇写的很细节的周报,又推崇工程师对简单的事情比较懒惰的情绪,所以我们做了一款聪明的周报,并且让他聪明的发邮件给领导。

image

我们导入GIT记录从未如此方便,以至于大家为了写好周报,开始写好提交记录。
image

如图,我们还打通了钉钉群推送的功能,尽管钉钉是一个多么糟糕的APP。

有了这样的平台,我们可以更加优雅的输出周报。
image

关于我们

我一直希望有个关于我们的页面,它能带给人温暖和震撼。它用每个人的头像直白的告诉大家,我们是一个TERM。
image

写在最后

当迅速迭代出这个版本,我同时感受到团队每个人有更多的潜力做更优秀的事情。不管是否被世界埋没,我们应该善于挖掘自己,并且 enjoy the good game!
广告:我们正在招人,坐标杭州,阿里系公司,E轮融资,行业独角兽(almost),欢迎投递简历([email protected])

运行时扫描Java注解(二)之reflections解读

Reflections:运行时解析java元数据。
GitHub地址:https://github.com/ronmamo/reflections

Reflections基于反射和字节码,便于在运行时获取Java元数据,本文就它的使用和源码进行一定解读。

Reflections Api

关于如何使用,请参见官网,下面罗列一些提供的Api

//获取所有Reader的子类
Set<Class<? extends Reader>> subTypesOf = reflections.getSubTypesOf(Reader.class);

//获取所有使用了注解@Time的类
Set<Class<?>> typesAnnotatedWith = reflections.getTypesAnnotatedWith(Time.class);

//获取方法参数名称
List<String> names = reflections.getMethodParamNames(GitHubReader.class.getMethods()[0]);

//获取方法使用
Set<Member> usage = reflections.getMethodUsage(GitHubReader.class.getMethods()[0]);

// 获取资源
Set<String> properties = reflections.getResources(Pattern.compile(".*\\.properties"));

//获取使用了注解@Path的方法
Set<Method> resources = reflections.getMethodsAnnotatedWith(javax.ws.rs.Path.class);

注意:默认只会加载SubTypesScannerTypeAnnotationsScanner两个扫描器,需要特别的Api,实例化reflections时可以设置Scanner。

VFS:a simple virtual file system bridge

正如在《运行时扫描Java注解(一)》中提到的,我们首先需要指定扫描目录。reflections对Jar、系统目录、Zip等进行了抽象,即VFS类,而ClasspathHelper则提供了获取扫描目录的一系列辅助方法:

public static Collection<URL> forPackage(String name, ClassLoader... classLoaders);
public static Collection<URL> forResource(String resourceName, ClassLoader... classLoaders);
public static URL forClass(Class<?> aClass, ClassLoader... classLoaders);
public static Collection<URL> forJavaClassPath();
public static Collection<URL> forWebInfLib(final ServletContext servletContext);
public static URL forWebInfClasses(final ServletContext servletContext);
...

扫描器接口Scanner

指定了扫描目录,就可以进行扫描。
reflections提供了若干扫描器,对类、方法、注解、资源、属性等进行扫描。

  1. SubTypesScanner 类型扫描
  2. TypeAnnotationsScanner 类型注解扫描
  3. MethodParameterScanner 方法参数扫描
  4. MethodParameterNamesScanner 方法参数名扫描
  5. MethodAnnotationsScanner 方法注解扫描
  6. MemberUsageScanner 方法使用扫描
  7. FieldAnnotationsScanner 属性注解扫描
  8. ResourcesScanner 资源扫描
  9. ...

这些所有的扫描器,都实现了接口Scanner

public interface Scanner {

    void setConfiguration(Configuration configuration);

    Multimap<String, String> getStore();

    void setStore(Multimap<String, String> store);

    Scanner filterResultsBy(Predicate<String> filter);

    boolean acceptsInput(String file);

    Object scan(Vfs.File file, @Nullable Object classObject);

    boolean acceptResult(String fqn);
}

关于这些扫描器的具体代码实现,提供了Java反射和Javassist两种方式。

Java反射 VS Javassist

reflections默认是通过Javassist实现扫描器的,如果Javassist库不存在,则使用Java反射(注意:针对一些扫描器在这两种方式下,返回结果是不尽相同的)。
实现扫描器的抽象接口:

public interface MetadataAdapter<C,F,M> {

    //
    String getClassName(final C cls);

    String getSuperclassName(final C cls);

    List<String> getInterfacesNames(final C cls);

    //
    List<F> getFields(final C cls);

    List<M> getMethods(final C cls);

    String getMethodName(final M method);

    List<String> getParameterNames(final M method);

    List<String> getClassAnnotationNames(final C aClass);

    List<String> getFieldAnnotationNames(final F field);

    List<String> getMethodAnnotationNames(final M method);

    List<String> getParameterAnnotationNames(final M method, final int parameterIndex);

    String getReturnTypeName(final M method);

    String getFieldName(final F field);

    C getOfCreateClassObject(Vfs.File file) throws Exception;

    String getMethodModifier(M method);

    String getMethodKey(C cls, M method);

    String getMethodFullKey(C cls, M method);

    boolean isPublic(Object o);
    
    boolean acceptsInput(String file);
    
}

实现的类名分别为JavassistAdapterJavaReflectionAdapter。下面我们将以两个示例,对两者的实现进行比较。

  1. 获取注解的方法:getClassAnnotationNames、getFieldAnnotationNames、getMethodAnnotationNames、getParameterAnnotationNames等
    Java反射只能获取到保留策略为运行时Runtime的注解,而无法获得Class策略的注解。而Javassist通过字节码操作这两种注解都可以获取到,比如获取Class注解:
(AnnotationsAttribute) classFile.getAttribute(AnnotationsAttribute.invisibleTag)
  1. 获取方法参数名扫描器:MethodParameterNamesScanner
    众所周知,编译器在编译Java源码的时候,会把参数名编译成arg1等形式,不会保留原始参数名称。在Eclipse的编译选项中,可以勾选Store information about method parameters(usable via reflection),从而通过Java反射获取参数信息。

在Javassist中,通过LocalVariableAttribute可以不需要勾选编译选项即可获得参数名称。以下代码截取自扫描器源码:

LocalVariableAttribute table = (LocalVariableAttribute) ((MethodInfo) method).getCodeAttribute().getAttribute(LocalVariableAttribute.tag);
int length = table.tableLength();
int i = Modifier.isStatic(((MethodInfo) method).getAccessFlags()) ? 0 : 1; //skip this
if (i < length) {
    List<String> names = new ArrayList<String>(length - i);
    while (i < length) names.add(((MethodInfo) method).getConstPool().getUtf8Info(table.nameIndex(i++)));
    getStore().put(key, Joiner.on(", ").join(names));
}

更多More

关于Java反射和Javassist,将在运行时扫描Java注解(三)和运行时扫描Java注解(四)作更进一步的探讨。

JVM(二)故障分析和解决

应用的故障有时候会体现在JVM层面,大多数JVM的问题又是一个时间问题,不是很快能复现或者到某个时间点才发生,分析和解决这类问题就显得尤为重要,本文就从内存和线程两方面阐述问题解决思路以及介绍一些JDK提供的工具,关于JVM致命错误(System Crashes)及更多类型的故障解决,可以参见:

Java Platform, Standard Edition Troubleshooting Guide

内存泄漏Memory Leaks和内存溢出OutOfMemoryError

OutOfMemoryError的种类有很多,参考Understand the OutOfMemoryError ExceptionOutOfMemoryError,通过上篇文章我们已经知道虚拟机运行时哪些数据区域会定义内存溢出,接下来我们就来看看如何解决这些区域的内存问题。

第一条:分析GC日志

当内存空间不够时,会触发GC,Minor GC是年轻代频繁的GC,当发生Full GC我们就需要重点关注了,通过GC日志我们可以清晰的看到每次GC的效果,那么 我们如何查看GC日志呢

我们需要通过参数打开GC日志:

Options 说明
-Xloggc:gc.log 输出GC日志到文件gc.log
-XX:+PrintGCDetails 输出GC详细信息
-XX:+PrintGCDateStamps 输出GC时间戳

接下来我们写一段测试代码来触发FullGC,代码通过不断新增字符串的方式在堆内产生java.lang.OutOfMemoryError: Java heap space异常,由于GC日志根据选用的收集器不同,日志内容会有些许不一样的体现,这里我们使用CMS收集器配合ParNew收集器,命令行参数如下:

-Xloggc:gc.log  -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseConcMarkSweepGC

测试代码如下:

@Test
public void testGC() {
    String base = "hi";
    for (int i = 0; i < 100; i++) {
        String str = base + base;
        base = str;
    }
}

最后生成的gc.log文件行数比较多,我们重点是读懂MinorGC和FullGC信息,如下是年轻代的MinorGC:

[GC (Allocation Failure) 2018-12-04T11:16:47.601-0800: 5.797: [ParNew: 262144K->2K(295552K), 1.0766473 secs] 656148K->656152K(952228K), 1.0768043 secs] [Times: user=1.36 sys=0.24, real=1.07 secs]

可以通过下面这张图进行分解:
image

我们可以看到,在经过一次MinorGC后,堆的使用空间并未减少,我们再来看看FullGC:

[Full GC (Allocation Failure) 2018-12-04T11:16:50.203-0800: 8.399: [CMS: 525076K->525014K(707840K), 0.2031043 secs] 525076K->525014K(1014528K), [Metaspace: 4933K->4933K(1056768K)], 0.2035787 secs] [Times: user=0.13 sys=0.06, real=0.20 secs]

理解了MinorGC,FullGC的信息很好理解,我们还是看下面这张图来解读,可以看到老年代垃圾回收的效果并不理想:
image

至此,我们已经知道如何从GC信息中获取内存中各个区空间的变化以及哪个区可能会发生故障,这里推荐一款 在线分析GC日志 的网站:GC easy,通过上传gc文件可视化分析内存区域,我们将测试代码生成的文件上传到这个网站进行分析,很清楚的看到老年代在GC前后并没有多大变化。

image

我们也可以实时监控GC,比如可视化JvisualVM的Visual GC:

image

还可以通过命令行工具jstat查看gc,5125是进程id,3表示输出次数,100表示输出间隔:

$ jstat -gc 5125 100 3
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
 0.0   4096.0  0.0   4096.0 264192.0 16384.0   277504.0   230441.1  151744.0 138272.6 20216.0 16474.0     72    6.528   0      0.000    6.528
 0.0   4096.0  0.0   4096.0 264192.0 16384.0   277504.0   230441.1  151744.0 138272.6 20216.0 16474.0     72    6.528   0      0.000    6.528
 0.0   4096.0  0.0   4096.0 264192.0 16384.0   277504.0   230441.1  151744.0 138272.6 20216.0 16474.0     72    6.528   0      0.000    6.528

其中S表示survivor区,E表示eden区,O表示Old区,M表示方法区,后缀C表示当前大小,U表示当前使用大小,关于jstat会在下文详细介绍。

第二条:分析堆dump文件

堆dump文件(格式为.hprof)保存了java进程运行时刻的内存快照,当内存问题产生时,我们可以深入堆dump文件来查看到底是什么对象占用了内存。我们首先要考虑的是 怎么产生这个dump文件

  1. 通过VM参数在内存溢出时自动生成堆dump文件:-XX:+HeapDumpOnOutOfMemoryError
  2. 通过可视化工具生成堆dump文件:比如JvisualVM的监视视图除了可以监控内存情况,还可以通过按钮生成dump文件
  3. 通过命令行工具jcmd生成堆dump文件,命令如下,其中38935是进程id,具体如何使用jcmd会在下文详细描述。
jcmd 38935 GC.heap_dump /Users/Sayi/pid38935.hprof

我们还是以上文的GC日志测试代码加上自动生成堆dump的参数,最终会生成文件java_pidXXXX.hprof文件,当我们获取到堆dump文件后,就可以进行分析了,这里推荐三种分析
方法:

  1. 通过在线分析网站http://heaphero.io/,由于一般dump文件会比较大,所以上传会比较慢
  2. 通过Memory Analyzer(MAT)分析,这是一款基于Eclipse的Java Heap分析插件:http://www.eclipse.org/mat/
  3. 通过JvisualVM打开dump文件进行分析,提供了可视化查看堆内存的功能

我们先来看看JvisualVM,当我们打开dump文件后,在概览里面可以看到一些基础信息,同时可以在右侧视图中直接查找大对象:
image

可以点开类视图,查看具体哪个类的实例数和大过大,从下图可以看到,字节数组占用了99.9%的空间:
image

点击具体的某个类可以计入实例数视图,可以看到具体存储的内容是哪些,根据存储内容进而可以知道是在哪块业务里面导致了内存问题:
image

MAT和JvisualVM虽然界面操作有差异,但是功能其实很相似,我们可以找到占用内存比较大的对象以及这些对象的引用关系,MAT里面的Diminator Tree视图可以查看大对象:
image

第三条:分析MetaSpace内存

MetaSpac区主要存储一些类信息,我们可以通过限制MetaSpace区大小,然后加载很多类产生java.lang.OutOfMemoryError: Metaspace异常,这里的测试代码使用不同的URLClassLoader去加载一个字节码文件达到相同的效果:

@Test
public void testMetaSpace() throws Exception {
    // -XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
    List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
    while (true) {
        URLClassLoader classLoader = new URLClassLoader(
                new URL[] { new File("./").toURI().toURL() });
        Class<?> loadClass = classLoader.loadClass("JVMTest");
        classLoaders.add(classLoader);
        System.out.println(classLoaders.size() + "-" + loadClass.getClassLoader());
    }
}

我们可以将堆dump文件通过MAT打开进行分析,通过duplicate_classes可以看到哪些类被多个加载器加载了:
image

我们再来看一种情况,使用cglib生成很多代理类信息也会导致MetaSpace区内存溢出:

@Test
public void testMetaSpace2() throws Exception {
    // -XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
    for (int i = 0;; i++) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(List.class);
        enhancer.setUseCache(false);
        enhancer.setCallback(new net.sf.cglib.proxy.MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
                    throws Throwable {
                return null;
            }
        });
        Object create = enhancer.create();
    }
}

这个时候通过MAT打开dump文件duplicate_classes视图是空的,因为这些都是代理类,不是重复类,那么我们该怎么分析这个问题呢?

我们可以从堆dump文件中找到实例数很多的类(并不一定是最多的几个实例数类,因为业务实例可能真的有那么多),进而可以分析这些实例的具体类型,查找是什么Class信息加载过多,通过MAT或者JvisualVM可以看到net.sf.cglib.proxy.MethodProxy有很多实例,这些实例都被不同的代理类使用。

image

MAT还提供了报表的功能,通过Leak Suspects能查看可能的内存问题,这也是一种方法:

image

类加载信息还可以通过命令行进行分析,jcmd 33984 GC.class_stats或者jmap -clstats 26240,暂时我还没有使用到,这里就不介绍了。

进程挂起和循坏Process Hangs and Loops

应用程序的问题有时候并非是内存问题,可能由于某些原因导致进程挂起或无限循坏,比如大量线程阻塞在一个超时的请求中无法释放,我们需要对某个时刻所有的线程进行分析,找到出问题的线程执行的代码。还是老问题,如何获取某个Java进程的线程dump文件(后缀名.tdump)?

1.jcmd命令行工具,其中38935是进程id,Thread.print是命令

jcmd 38935 Thread.print > /Users/Sayi/pid38935.tdump

2.jstack命令行工具

jstack 38935 > /Users/Sayi/pid50455.tdump

3.JvisualVM可视化工具:通过线程视图可以dump线程信息

还有一些更多的获取线程dump方式,这篇文章介绍了8种方式HOW TO TAKE THREAD DUMPS?

获取到线程dump文件后,它本身是可以阅读的,内容大概如下:

"pool-1-thread-9" #18 prio=5 os_prio=31 tid=0x00007f937096c000 nid=0x5f03 runnable [0x00007000052f3000]
   java.lang.Thread.State: RUNNABLE
        at java.net.PlainSocketImpl.socketConnect(Native Method)
        at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
        - locked <0x00000007aadf7c28> (a java.net.SocksSocketImpl)
        at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
        at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
        at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
        at java.net.Socket.connect(Socket.java:589)
        at org.apache.http.conn.socket.PlainConnectionSocketFactory.connectSocket(PlainConnectionSocketFactory.java:75)
        at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:142)
        at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:359)
        at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:381)
        at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:237)
        at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:185)
        at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89)
        at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:111)
        at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
        at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
        at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108)
        at com.deepoove.java8.JVMOptionTest.lambda$0(JVMOptionTest.java:84)
        at com.deepoove.java8.JVMOptionTest$$Lambda$1/1579572132.run(Unknown Source)
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at java.lang.Thread.run(Thread.java:748)

pool-1-thread-9是线程名称,prio是线程优先级,tid是线程id,nid是线程的nativeId,RUNNABLE表示运行状态。我们需要通过观察dump文件中的线程,或者观察比较相隔一定时间间隔的dump文件来找到可能出问题的线程,比如死锁、线程执行缓慢等。推荐一个在线的线程tdump分析工具:http://fastthread.io,可以总结所有线程的状态和信息,同时这个工具还可以分析Hs_err_pid错误。

image

我们来试想一个问题,如何快速从线程dump文件找到最繁忙的线程?

首先我们可以通过top命令查看某个进程内部的线程信息,其中pid是你的java进程id:

top -H -p <pid>

在top线程视图中的第一列PID就是这些线程的十进制id,转化为16进制后的值对应线程dump文件中nid的字段,进而可以从线程dump文件中获得这个线程的栈信息。

Java故障监控工具

JDK提供了一些故障监控工具,便于我们处理一些虚拟机相关层面的问,接下来我们主要介绍一些常用工具,详细信息参见官网:Java Troubleshooting, Profiling, Monitoring and Management Tools

jcmd

jcmd是一个非常强大的诊断命令,是jps、jinfo、jmap的功能集合,现在它是官方推荐的命令行工具。它的基本使用方式是通过发送command请求给运行的虚拟机:

jcmd <pid> <command>
  • 查看当前运行的Java进程ID
$ jcmd
17416 org.apache.catalina.startup.Bootstrap start
4990 sun.tools.jcmd.JCmd -l

我们也可以通过ps linux命令查找java进程。

  • 查看具体某个进程支持的command命令
$ jcmd 17416 help
17416:
The following commands are available:
JFR.stop
JFR.start
JFR.dump
JFR.check
VM.native_memory
VM.check_commercial_features
VM.unlock_commercial_features
ManagementAgent.stop
ManagementAgent.start_local
ManagementAgent.start
GC.rotate_log
Thread.print
GC.class_stats
GC.class_histogram
GC.heap_dump
GC.run_finalization
GC.run
VM.uptime
VM.flags
VM.system_properties
VM.command_line
VM.version
help

通过help我们可以看到很多可用的command,其中有些非常有用,具体每个命令如何使用,可以通过jcmd <pid> help <command>阅读。

  • 查看VM参数
$ jcmd 5125 VM.command_line
5125:
VM Arguments:
jvm_args: -Dosgi.requiredJavaVersion=1.8 [email protected]/eclipse-workspace -XX:+UseG1GC -XX:+UseStringDeduplication --add-modules=ALL-SYSTEM -XstartOnFirstThread -Dorg.eclipse.swt.internal.carbon.smallFonts -Dosgi.requiredJavaVersion=1.8 -Dosgi.dataAreaRequiresExplicitInit=true -Xms256m -Xmx1024m --add-modules=ALL-SYSTEM -Xdock:icon=../Resources/Eclipse.icns -XstartOnFirstThread -Dorg.eclipse.swt.internal.carbon.smallFonts -Dosgi.requiredJavaVersion=1.8 [email protected]/eclipse-workspace -XX:+UseG1GC -XX:+UseStringDeduplication -XstartOnFirstThread -Dorg.eclipse.swt.internal.carbon.smallFonts -Dosgi.requiredJavaVersion=1.8 -Dosgi.dataAreaRequiresExplicitInit=true -Xms256m -Xmx1024m -Xdock:icon=../Resources/Eclipse.icns -XstartOnFirstThread -Dorg.eclipse.swt.internal.carbon.smallFonts 
java_command: <unknown>
java_class_path (initial): /Applications/Eclipse.app/Contents/MacOS//../Eclipse/plugins/org.eclipse.equinox.launcher_1.5.100.v20180827-1352.jar
Launcher Type: generic

这个命令显示我Mac上运行的Eclipse的虚拟机参数,可以看到使用的垃圾回收策略是G1。

  • 查看系统属性
$ jcmd 17416 VM.system_properties
17416:
#Tue Dec 04 23:49:00 CST 2018
java.vendor=Oracle Corporation
sun.java.launcher=SUN_STANDARD
catalina.base=/home/Sayi/projects/demo/.base
sun.management.compiler=HotSpot 64-Bit Tiered Compilers
sun.nio.ch.bugLevel=
catalina.useNaming=true
os.name=Linux
java.vm.specification.vendor=Oracle Corporation
java.runtime.version=1.8.0_77-b03
user.name=Sayi
shared.loader=
tomcat.util.buf.StringCache.byte.enabled=true
user.language=en
java.naming.factory.initial=org.apache.naming.java.javaURLContextFactory
APP_HOME=/home/souche/projects/cupid
java.version=1.8.0_77
// 略
  • 生成堆dump文件
$ jcmd 38935 GC.heap_dump /Users/Sayi/pid38935.hprof
38935:
Heap dump file created
  • 生成线程dump文件
$ jcmd 38935 Thread.print > /Users/Sayi/pid38935.tdump
  • 查看堆内存中每个类的图表
$ jcmd 5125 GC.class_histogram > class.histogram

这个命令可以 实时查看堆中内存类的情况

  • 查看类的meta data 信息
$ jcmd 33984 GC.class_stats

这条命令需要开启VM参数-XX:+UnlockDiagnosticVMOptions,提供了类加载相关的信息,在分析metaspace区类信息非常有用。

jstat

这是一个非常强大的监控工具,它的基本用法如下:

jstat -<option>  <pid> [<interval> [<count>]]

因为是实时监控,所有有了按一定时间间隔输出的参数,interval表示时间间隔,count表示输出次数。option是一些可选命令,每个命令有其内在的含义,我们通过jstat -options查看所有可用选项:

$ jstat -options
-class
-compiler
-gc
-gccapacity
-gccause
-gcmetacapacity
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcutil
-printcompilation
  • 监控类加载器统计数据
$ jstat -class 5125 500 3
Loaded  Bytes  Unloaded  Bytes     Time   
 39656 74207.2      409   619.4      93.50
 39656 74207.2      409   619.4      93.50
 39656 74207.2      409   619.4      93.50
  • 监控GC数据总结
$ jstat -gcutil 5125 500 3
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00 100.00  12.50  31.79  91.54  83.16    163   23.747     2   38.079   61.825
  0.00 100.00  12.50  31.79  91.54  83.16    163   23.747     2   38.079   61.825
  0.00 100.00  12.50  31.79  91.54  83.16    163   23.747     2   38.079   61.825

这里给出所有可选项的含义,摘自Java Tools Reference: jstat

option 含义
class Displays statistics about the behavior of the class loader.
compiler Displays statistics about the behavior of the Java HotSpot VM Just-in-Time compiler.
gc Displays statistics about the behavior of the garbage collected heap.
gccapacity Displays statistics about the capacities of the generations and their corresponding spaces.
gccause Displays a summary about garbage collection statistics (same as -gcutil)
gcnew Displays statistics of the behavior of the new generation.
gcnewcapacity Displays statistics about the sizes of the new generations and its corresponding spaces.
gcold Displays statistics about the behavior of the old generation and metaspace statistics.
gcoldcapacity Displays statistics about the sizes of the old generation.
gcmetacapacity Displays statistics about the sizes of the metaspace.
gcutil Displays a summary about garbage collection statistics.
printcompilation Displays Java HotSpot VM compilation method statistics.

jstack

查看线程信息,使用方法为:

jstack [-l] <pid>

其中-l参数会打印更多关于锁相关的信息。

JvisualVM

这是一个可视化工具,自称为“All-in-One Java Troubleshooting Tool”,基本上命令行实现的故障诊断和监控功能,都可以可视化呈现。

它可以用来实时监控Java进程信息,下面这张图是监视视图,可以查看内存、CPU、类和线程的情况,同时右上角提供了生成堆dump的功能。

image

线程视图展示了当前进程运行的所有线程信息,右上角也提供了生成线程dump功能。

image

JvisualVM还可以通过插件方式进行扩展,安装VisualGC后可以实时查看GC信息。

JvisualVM同时也是一个分析工具,可以加载堆dump和线程tdump文件进行分析,上文在堆内存分析中已经介绍过如何使用。

更多工具

jconsole和JMC都是可视化工具,jstatd可以进行远程监控,jps、jinfo、jmap基本已经过时了,这些本文都不作介绍。

总结

当我们了解了常见故障和故障的分析方法,接下来就需要我们在实践中去反复磨炼自己的技能了,关于JVM故障的分析和处理,相信所有人终有一天会有【拨开云雾见天日】的感觉。

Collections(二)List上篇-ArrayList

List是一个有序的序列结构,基于 位置访问 ,List接口在Collection接口上新增加了一些方法,允许在指定位置插入和删除元素,也允许对元素的搜索(indexOf)。List是如何实现Iterator迭代器,ArrayList 和 LinkedList的性能差异为何会表现在不同方面,本文将对ArrayList进行详细分析。

List接口

我们先来看看List接口,提供了基于位置访问的一些方法。

方法 作用
E get(int index) 从某个位置获取元素
E set(int index, E element) 某个位置设置元素
void add(int index, E element) 某个位置增加元素
E remove(int index) 移除某个位置的元素
int indexOf(Object o) 搜索某个元素首次出现的位置
int lastIndexOf(Object o) 搜索某个元素最后出现的位置

ArrayList实现原理

image

ArrayList是基于可变数组实现的,内部维护了一个Object[] elementData数组,同时提供了操纵数组容量的方法,List的大小是由size变量决定的,容量是由数组的大小决定的,容量Capacity不满足size大小时,ArrayList就会自动通过合适的扩容策略进行扩容。

初始容量

ArrayList遵循上一节转换构造器的原则:提供了一个无参构造方法和Collection为参数的构造方法。

同时,还提供了一个int类型的参数来定义内部数组的容量public ArrayList(int initialCapacity),这个参数的初始容量默认是定义的一个常量:

/**
 * Default initial capacity.
 */
private static final int DEFAULT_CAPACITY = 10;

两个问题:1.ArrayList什么时候会去增长容量?2.它的容量增长策略是怎么实现的呢?

需要扩容的场景肯定是因为容量无法满足元素的个数了,所以在增加元素add的时候,或者增加集合元素addAll,或者以另一个集合初始化ArrayList的时候,都会去判断是否增加容量,本章以及接下来的所有章节增长容量策略的分析都将依据 增加元素 源码进行分析。

增加元素和增长容量策略分析

直接看源码,数组的复制使用了高效的System.arraycopy方法,参数可以是不同数组,也可以作用于同一个数组。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!

    elementData[size++] = e;
    return true;
}

public void add(int index, E element) {
    rangeCheckForAdd(index); // 检查下标是否越界
    ensureCapacityInternal(size + 1);  // Increments modCount!!

    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

方法ensureCapacityInternal(size + 1);是为了容量增长策略设计的,我们看下这个方法:

private void ensureCapacityInternal(int minCapacity) {
  if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
  }

  ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
  modCount++; // 这是为fail-fast机制设计的变量

  // overflow-conscious code
  if (minCapacity - elementData.length > 0)
    grow(minCapacity);
}

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private void grow(int minCapacity) {
  // overflow-conscious code
  int oldCapacity = elementData.length;
  int newCapacity = oldCapacity + (oldCapacity >> 1);
  if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
  if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
  // minCapacity is usually close to size, so this is a win:
  elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
  if (minCapacity < 0) // overflow
    throw new OutOfMemoryError();
  return (minCapacity > MAX_ARRAY_SIZE) ?
    Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}

这部分源码其实很好理解,我们能得出以下几个结论,这些结论很好的回答了上文中提到的两个问题:

  • if (minCapacity - elementData.length > 0) grow(minCapacity); 当元素个数size超过了内部维护的数组容量大小时,就会去扩容
  • int newCapacity = oldCapacity + (oldCapacity >> 1); 增加的容量是当前容量的二分之一,至于为什么是1/2,这应该是JDK设计者的一个折中选择问题
  • 采用Arrays.copyOf复制元素到新的空间,底层也是采用System.arraycopy方法

基础源码分析

获取元素

public E get(int index) {
    rangeCheck(index);

    return elementData(index);
}

rangeCheck检测角标index是否越界合法。

更新元素

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

通过set方法设置元素,会返回老的元素值。

删除元素

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

和增加元素一样,删除元素的代码也采用了System.arraycopy方法进行数组移位可以得出结论,ArrayList在插入和删除操作会花费一定的代价,随机访问操作的时间复杂度将是常量级的

索引元素

public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

iterator源码实现和fail-fast设计

private class Itr implements Iterator<E> {
  int cursor;     // index of next element to return
  int lastRet = -1; // index of last element returned; -1 if no such
  int expectedModCount = modCount;

  public boolean hasNext() {
    return cursor != size;
  }

  @SuppressWarnings("unchecked")
  public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
      throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
      throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
  }

  public void remove() {
    if (lastRet < 0)
      throw new IllegalStateException();
    checkForComodification();

    try {
      ArrayList.this.remove(lastRet);
      cursor = lastRet;
      lastRet = -1;
      expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
      throw new ConcurrentModificationException();
    }
  }

  @Override
  @SuppressWarnings("unchecked")
  public void forEachRemaining(Consumer<? super E> consumer) {
    Objects.requireNonNull(consumer);
    final int size = ArrayList.this.size;
    int i = cursor;
    if (i >= size) {
      return;
    }
    final Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length) {
      throw new ConcurrentModificationException();
    }
    while (i != size && modCount == expectedModCount) {
      consumer.accept((E) elementData[i++]);
    }
    // update once at end of iteration to reduce heap write traffic
    cursor = i;
    lastRet = i - 1;
    checkForComodification();
  }

  final void checkForComodification() {
    if (modCount != expectedModCount)
      throw new ConcurrentModificationException();
  }
}

Itr是ArrayList的一个内部类,所以可以通过ArrayList.this方式引用ArrayList内部域。

hasNext()和next()方法的实现就是不断向后移动下标cursor,直到cursor==size为止。

上一篇文章说过,ConcurrentModificationException是由迭代器抛出来的异常,从源码中我们可以看到,当modCount != expectedModCount时,就会提前抛出异常,即fail-fast,其中expectedModCount是在构造Itr对象时产生的modCount是存在ArrayList的一个变量,在每次新增元素、删除元素或者结构改变(比如排序等),就会累加modCount,上文ensureExplicitCapacity源码中就能看到modCount++操作。

我们已经知道,在迭代集合时,是不允许更改集合内部结构的,如果希望某个符合条件的元素删除呢?答案是使用java.util.Iterator.remove()方法

ArrayList提供了方法ListIterator<E> listIterator()获取ListIterator迭代器,这是一个为list打造的迭代器,允许双向遍历和更多操作。

public interface ListIterator<E> extends Iterator<E> {
    // Query Operations
    boolean hasNext();
    E next();
    boolean hasPrevious();
    E previous();
    int nextIndex();
    int previousIndex();

    // Modification Operations
    void remove();
    void set(E e);
    void add(E e);
}

性能

性能问题分为时间复杂度和空间复杂度两方面去讨论。

ArrayList实现了RandomAccess接口,它长于随机访问,不擅于插入和删除,这与LinkedList是相反的。

如果初始容量过大实际元素过少,则会造成空间的浪费。如果一次插入的元素很多,而初始容量不够,则会频繁的扩容,这无疑增加的时间复杂度。

为了性能考虑,在插入大量数据的时候,我们可以选择一个合适的初始容量,如果在一个初始容量已定的ArrayList中插入大量数据,可以通过调用方法`public void ensureCapacity(int minCapacity) 确保一次性扩容足够大的容量。

如果ArrayList中元素已经添加完毕,我们可以通过调用方法 public void trimToSize() 将内部数组的容量调整为实际元素的size大小来节约空间。

下篇文章,我们将会对LinkedList进行分析。

运行时扫描Java注解(一)之探讨

本文参考英文原文Scanning Java Annotations at Runtime进行了转述,但是也并非完全一字一句的翻译过来。

当你在开发一个基于注解的框架时,你肯定需要扫描所有使用了特定注解的类,从而初始化你的框架。比如JPA,你需要找到一系列使用了@Entity注解的类,从而定义ORM映射。可以使用很多技术在运行时扫描Java注解,本文将对此作探讨。

指定扫描目录

我们需要找到从哪个目录开始扫描,很多框架可以让用户指定目录,比如SpringMVC,通过以下配置(base-package)指定扫描的包名。

<context:annotation-config />
<context:component-scan base-package="com.xxx.xx" />

根据开发环境,我们可以有不同的方法获取扫描目录。

1.Java classpath

通过系统属性java.class.path指定扫描目录。

2.classloader

通过ClassLoader.getResource()ClassLoader.getResources()获取指定资源,从而解析出扫描目录。

ClassLoader cl = Thread.currentThread().getContextClassLoader();
Enumeration<URL> urls = cl.getResources("com/deepoove/dubbo");

3.Web Applications

对于Web应用,扫描目录在WEB-INF/lib或者WEB-INF/classes下,我们可以通过ServletContext 类获取lib下的资源:

List<URL> urls = new ArrayList<URL>();

Set libJars = servletContext.getResourcePaths("/WEB-INF/lib");
for (Object jar : libJars){
    try{
       urls.add(servletContext.getResource((String) jar));
    }
    catch (MalformedURLException e){
       throw new RuntimeException(e);
    }
}

获取classes下的资源会额外采取一点小措施:

URL classesPath = null;
Set libJars = servletContext.getResourcePaths("/WEB-INF/classes");
for (Object jar : libJars){
   try{
      URL url = servletContext.getResource((String) jar);
      String urlString = url.toString();
      int index = urlString.lastIndexOf("/WEB-INF/classes/");
      urlString = urlString.substring(0, index + "/WEB-INF/classes/".length());
      classesPath = new URL(urlString);       
      //do somethings   
    }catch (MalformedURLException e){
      throw new RuntimeException(e);
   }
}

至于如何获取到servletContext,在web框架中,可以通过监听器ServletContextListener 获取。

开始扫描目录

遍历目录是简单的,值得一提的是我们可以通过JarInputStream扫描jar文件。

找到指定注解的类

至此,我们已经可以获得所有扫描的类路径了,通过ClassLoader加载这些类,然后通过Java反射API去扫描特定注解是一个糟糕的方法:

  1. Java反射API只能扫描运行时可见的注解,注解类型有Source、Class、Runtime,仅仅只有Runtime类型的注解才能被反射API获取到。
  2. 通常你不会使用每一个你扫描的类,所以通过ClassLoader加载这些类,将填充JVM永久代且浪费资源。

所以,如果不通过ClassLoader去加载这些类,那么我们又如何去扫描特定注解呢???
答案是通过字节码操作库 Javassist或者ASM。

在Javassist中,你可以获取InputStream从而实例化ClassFile类,ClassFile对象无需加载这个类就可以获取到特定注解。

DataInputStream dstream = new DataInputStream(new BufferedInputStream(bits));

ClassFile cf =  new ClassFile(dstream);
String className = cf.getName();
AnnotationsAttribute visible = (AnnotationsAttribute) cf.getAttribute(AnnotationsAttribute.visibleTag);
AnnotationsAttribute invisible = (AnnotationsAttribute) cf.getAttribute(AnnotationsAttribute.invisibleTag);
for (javassist.bytecode.Annotation ann : visible.getAnnotations()){
     System.out.println("@" + ann.getTypeName());
}

visible对象对应Runtime类型的注解,invisible对象对象Class类型的注解。Javassist同样提供了遍历方法和属性的方法,获取方法和属性上的注解也是同理。

反射框架

Reflections,一个java 运行时的元数据解析框架,下一篇文章,我们将从Reflections源码层面探讨更多关于反射获取注解的议题。

Collections(七)Map和Set下篇

本文接着上篇继续探讨更多的Map实现和Set实现。

LinkedHashMap

LinkedHashMap是一个有序的HashMap,它 继承了HashMap类,并且通过内部维护一个链表来保证迭代时有序

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

LinkedHashMap.Entry<K,V> head;

LinkedHashMap.Entry<K,V> tail;

关于LinkedHashMap,它的构造器有个有趣的参数accessOrder,这个参数代码有序模式,默认是false,表示按照插入顺序(insertion-order), 如果传入true,则是按照访问顺(access-order):

public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

访问顺序的意思是每次插入元素都会在链表尾部插入,因为这是最近访问的元素,如果是获取元素,则将获取的元素移到链表尾部,链表尾部是最近访问,如果我们限定LinkedList大小,很容易实现一个 LRU缓存算法

package com.deepoove.datastructure;

import java.util.LinkedHashMap;

public class LRUCache implements Cache {

    private LinkedHashMap<String, Object> cache;
    private int size;
    
    public LRUCache(int capacity) {
        size = capacity;
        cache = new LinkedHashMap<>(capacity, 0.75f, true);
    }

    @Override
    public void put(String key, Object value) {
        synchronized (this) {
            if (cache.size() >= size){
                cache.remove(cache.keySet().iterator().next());
            }
            cache.put(key, value);
        }
    }

    @Override
    public Object get(String key) {
        return cache.get(key);
    }
    
    @Override
    public String toString() {
        return cache.toString();
    }
}

这里的元素没有保存访问次数,改进的LRU算法可以将访问次数也作为排序的一个条件。

LinkedHashMap还提供了 removeEldestEntry一种方法,可以重写该方法,这个方法的返回值是个boolean值,以便在将新映射添加到Map时强制执行自动删除过时映射的策略,这使得实现自定义缓存变得非常容易。

TreeMap

TreeMap是一个按照顺序排序的结构,关于顺序性(Comparable和Comparator),在第一篇概览中介绍过了。

TreeMap间接实现了SortedMap接口,实现基础是一个红黑树, 正如HashMap中对恶化的链表的优化。每个节点的数据结构如下:

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;
}

性能

从实现角度来说,如果不希望保证顺序,那应该选用HashMap,因为LinkedHashMap还额外维护了一个链表,HashMap查找一个元素最快是O(1),性能最差的情况下会是O(logN)。

HashMap的性能取决于以下三点:

  • 负载因子:默认值0.75是个空间和时间上最优的考量,不建议修改
  • 容量:我们应当根据预期的存储元素个数来选择合适的初始容量,从而避免重新计算Hash去扩容
  • 哈希函数:默认是通hashcode方法通过计算来实现的

接下来会介绍几个专用的Map,这些都是一些优化这三点或者特殊场景的实现。

Map特殊用途实现

EnumMap

如果我们希望一个枚举类型映射一个对象,代码可能是这样的:

HashMap<DAY, String> map = new HashMap<DAY, String>();
map.put(DAY.MONDAY, "星期一");

JDK提供了一个特殊的Map实现EnumMap来做这样的事情,顾名思义,它并不是一个基于哈希表的实现(名称上没有hash单词):

// DAY是一个枚举类
EnumMap<DAY, String> eummap = new EnumMap<>(DAY.class);
eummap.put(DAY.MONDAY, "星期一");

那么这两者之间有什么差异,为何要新实现一个EnumMap呢?
这是由于枚举的特殊性,每个枚举值都有个整型常量值,它是枚举声明的序数ordinal所以我们无需哈希函数,或者说哈希函数就是

f(x)=ordinal=index

因为枚举常量值的个数一定的,所以根本无需扩容,它的容量就是枚举常量的个数,所有不需要大材小用使用哈希去实现枚举对象的映射关系。

EnumMap是一个基于固定长度数组Object[] vals为实现的Map,长度为枚举常量的个数,下标是枚举的整型值,当使用枚举作为Key时,我们应当使用这个类来代替HashMap。

WeakHashMap

WeakHashMap是一个Key为弱引用的Map,当这个Key没有强引用的时候,就有可能被垃圾回收器回收。

WeakReferenc类是对一个对象的装饰,继承Reference类,提供了get方法获取这个对象,WeakHashMap的Entry继承了这WeakReferenc。

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;

    Entry(Object key, V value,
          ReferenceQueue<Object> queue,
          int hash, Entry<K,V> next) {
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }

    @SuppressWarnings("unchecked")
    public K getKey() {
        return (K) WeakHashMap.unmaskNull(get());
    }

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;
        K k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            V v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }
    // 略
}

源码中可以看到,每个key都会调用super(key, queue);构造为弱引用,在getKey方法中,通过java.lang.ref.Reference.get()方法获取对象。

IdentityHashMap

IdentityHashMap是一个哈希函数特别的基于哈希表的Map结构,它的Key是通过引用相等(reference-equality)来判断的,而我们熟知的HashMap的key是通过对象相等(object-equality)来判断。

我们看看它计算hash的算法:

private static int hash(Object x, int length) {
    int h = System.identityHashCode(x);
    // Multiply by -127, and left-shift to use least bit as part of hash
    return ((h << 1) - (h << 8)) & (length - 1);
}

System.identityHashCode会返回这个对象的hashcode,无论这个对象的类是否实现了hashCode方法,即当且仅当Key1=Key2,它们才会被认为是相同的关键字,IdentityHashMap可以利用在深拷贝的实现上。

Map并发实现

并发包下提供了接口ConcurrentMap和基于哈希表的高并发实现ConcurrentHashMap,具体会在并发章节中阐述。

Set

Set通用实有:HashSet,TreeSet和LinkedHashSet,和Map的命名很相似,Set接口继承于Collection接口,而Collection和Map接口是相对独立的,为什么要把Map和Set放在同一个章节里介绍呢?

因为JDK做了一个巧妙的实现,这三种Set实现底层都是基于对应的Map实现的,Set的集合就是Map中Key的集合(不允许重复),Map重的value统一设置成了一个固定对象new Object():

private transient HashMap<E,Object> map;

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

public HashSet() {
    map = new HashMap<>();
}

public HashSet(Collection<? extends E> c) {
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}

关于Set实现原理请参见Map的实现原理,Set也提供了高性能的枚举实现EnumSet,在并发包下,提供了写时复制的类CopyOnWriteArraySet。

面向切面编程(二)之Spring和Guice

AOP是依赖注入框架的一个完善,本文将会对Spring和Guice的AOP部分进行详细分析。

Spring AOP

我们直接看看AOP的使用示例。

  1. 写一个服务接口和实现
public interface PersonService {
  String get();
}

public class PersonServiceImpl implements PersonService {
  @Log
  @Override
  public String get() {
    System.out.println("execute get method");
    return "Sayi";
  }
}

@log表明这个方法会被切入,使用注解标识了切入点,可以不选择注解切入,直接切入某个类的某个方法,具体配置参见Spring官方文档。

package com.deepoove.diexample.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {

}
  1. 写切面拦截方法,切面类需要用注解@aspect标识
package com.deepoove.diexample.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
public class LogAspect {

  @Around("@annotation(com.deepoove.diexample.annotation.Log)")
  public Object doLog(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println(System.currentTimeMillis() + " Log Aspect before");
    Object obj = pjp.proceed();
    System.out.println(System.currentTimeMillis() + " Log Aspect after");
    return obj;
  }

}

@around方法会包裹这个方法的执行,同时还提供了@before方法执行前的注解和@after方法执行后的注解。
3. 使用XML打开切面代理(<aop:aspectj-autoproxy />)、配置服务和切面拦截器Bean

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
  xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop"
  xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/aop 
    http://www.springframework.org/schema/aop/spring-aop.xsd">

  <aop:aspectj-autoproxy />

  <bean id="personService" class="com.deepoove.diexample.service.PersonServiceImpl">
  </bean>

  <bean id="myAspect" class="com.deepoove.diexample.aop.LogAspect">
  </bean>

</beans>

一切完毕,可以写个单元测试检验下:

@Test
public void testXMLAOPConifg() {
  ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("person.xml");
  PersonService personService = context.getBean(PersonService.class);
  Assert.assertEquals(personService.get(), "Sayi");
  context.close();
}

// 输出结果为:
1533176773600 Log Aspect before
execute get method
1533176773600 Log Aspect after

AOP源码解析

在《依赖注入(二)Spring Dependency injection》文章中详细说明过Bean的初始化过程,我们知道AOP其实是Spring的一个扩展,而BeanPostProcessor的设计为实现这个扩展提供了便捷。关于如何扫描XML配置,如何解析@around注解生成拦截器Advice这里不作介绍,我们直接看为Bean生成代理的源码AbstractAutoProxyCreator,处理链为先调用postProcessAfterInitialization方法再调用wrapIfNecessary方法,wrapIfNecessary方法核心代码如下:

// Create proxy if we have advice.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
  this.advisedBeans.put(cacheKey, Boolean.TRUE);
  Object proxy = createProxy(
      bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
  this.proxyTypes.put(cacheKey, proxy.getClass());
  return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;

我们对上面代码做个简单解释:

  1. getAdvicesAndAdvisorsForBean就是获取当前Bean对应的拦截器,拦截器包含了切入点和具体拦截的方法,返回值类似为
InstantiationModelAwarePointcutAdvisor: expression [@annotation(com.deepoove.diexample.annotation.Log)]; advice method [public java.lang.Object com.deepoove.diexample.aop.LogAspect.doLog(org.aspectj.lang.ProceedingJoinPoint) throws java.lang.Throwable]; perClauseKind=SINGLETON
  1. 如果拦截器不为NULL,则会创建代理
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
    @Nullable Object[] specificInterceptors, TargetSource targetSource) {

  if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
    AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
  }

  ProxyFactory proxyFactory = new ProxyFactory();
  proxyFactory.copyFrom(this);

  if (!proxyFactory.isProxyTargetClass()) {
    if (shouldProxyTargetClass(beanClass, beanName)) {
      proxyFactory.setProxyTargetClass(true);
    }
    else {
      evaluateProxyInterfaces(beanClass, proxyFactory);
    }
  }

  Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
  proxyFactory.addAdvisors(advisors);
  proxyFactory.setTargetSource(targetSource);
  customizeProxyFactory(proxyFactory);

  proxyFactory.setFrozen(this.freezeProxy);
  if (advisorsPreFiltered()) {
    proxyFactory.setPreFiltered(true);
  }

  return proxyFactory.getProxy(getProxyClassLoader());
}

可以从源码看出来,生成代理的核心类为ProxyFactory,接下来会详细阐述它的细节。

ProxyFatory编程

Spring为代理模式提供了一个工厂类ProxyFatory,支持对象的DK动态代理和Cglib代理。如果目标对象至少实现了一个接口,那么优先使用JDK动态代理所有接口,否则会使用Cglib,如果需要强制使用Cglib,可以通过配置实现:

<aop:config proxy-target-class="true" />

正如上篇文章说过,cglib无法对final进行代理。

我们先来看看如何使用ProxyFatory编程:

@Test
public void testAOP() {
  
  ProxyFactory factory = new ProxyFactory(new MyService());
  factory.addAdvice(new MethodInterceptor() {
    
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
      System.out.println("AOP Spring: " + invocation.getMethod());
      return invocation.proceed();
    }
  });
  MyService tb = (MyService) factory.getProxy();

  tb.toString();

}

ProxyFactory构造器支持传入Object对象或者接口Class,通过addAdvice方法增加拦截器org.aopalliance.intercept.Interceptor的实现,AopProxy接口定义了获取代理类的方法,获取AopProxy实例的源码在DefaultAopProxyFactory中,如下:

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
  if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
    Class<?> targetClass = config.getTargetClass();
    if (targetClass == null) {
      throw new AopConfigException("TargetSource cannot determine target class: " +
          "Either an interface or a target is required for proxy creation.");
    }
    if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
      return new JdkDynamicAopProxy(config);
    }
    return new ObjenesisCglibAopProxy(config);
  }
  else {
    return new JdkDynamicAopProxy(config);
  }
}

AopProxy有两个实现,分别是JdkDynamicAopProxy和CglibAopProxy,我们可以实现接口,实现自己的代理策略。

ObjenesisCglibAopProxy继承了CglibAopProxy,使用objenesis技术实例化对象,objenesis技术参见官网http://objenesis.org/

深入JdkDynamicAopProxy的实现,我们发现Spring也是通过实现AOP盟约的MethodInvocation,完成对拦截器链的调用,具体实现类是ReflectiveMethodInvocation,构造完ReflectiveMethodInvocation后,通过其核心方法递归遍历拦截器:

public Object proceed() throws Throwable {
  //  We start with an index of -1 and increment early.
  if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
    return invokeJoinpoint();
  }

  Object interceptorOrInterceptionAdvice =
      this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
  if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
    // Evaluate dynamic method matcher here: static part will already have
    // been evaluated and found to match.
    InterceptorAndDynamicMethodMatcher dm =
        (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
    if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
      return dm.interceptor.invoke(this);
    }
    else {
      // Dynamic matching failed.
      // Skip this interceptor and invoke the next in the chain.
      return proceed();
    }
  }
  else {
    // It's an interceptor, so we just invoke it: The pointcut will have
    // been evaluated statically before this object was constructed.
    return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
  }
}

通过源码我们发现,所有JDK动态代理的对象除了代理对象接口外,还实现了接口:
SpringProxy.class、Advised.class和DecoratingProxy.class,我们可以利用这一特性,获得更多的代理信息。

ProxyFatory应用之Spring-remoting

RPC框架客户端依赖服务接口,调用远程服务实现,HttpInvoker是Spring的一个基于HTTP协议和Java序列化的远程调用框架。参见《写一个极简的RPC和Hessian的设计 》

Spring Http Invoker客户端的实现原理是基于动态代理,把接口的调用代理至HTTP服务,同时利用了FactoryBean的扩展技术。

public class HttpInvokerProxyFactoryBean extends HttpInvokerClientInterceptor implements FactoryBean<Object> {

  @Nullable
  private Object serviceProxy;


  @Override
  public void afterPropertiesSet() {
    super.afterPropertiesSet();
    Class<?> ifc = getServiceInterface();
    Assert.notNull(ifc, "Property 'serviceInterface' is required");
    this.serviceProxy = new ProxyFactory(ifc, this).getProxy(getBeanClassLoader());
  }


  @Override
  @Nullable
  public Object getObject() {
    return this.serviceProxy;
  }

  @Override
  public Class<?> getObjectType() {
    return getServiceInterface();
  }

  @Override
  public boolean isSingleton() {
    return true;
  }

}
  • getObject()方法暴露具体代理对象Bean
  • afterPropertiesSet方法通过ProxyFactory初始化具体代理对象
  • HttpInvokerClientInterceptor实现了MethodInterceptor拦截器的invoke方法,执行远程调用

AOP应用

Spring中AOP的应用还有很多,比如@Transcation、@Cache等,后面有时间会作为一个主题单独分析。

Guice AOP

Guice AOP的设计相对比较简单:切入点匹配和拦截器绑定。
我们先看一个示例,首先在Module中指定切入点和拦截器(这里采用了官方文档的代码,Matchers类用来生成匹配逻辑或者不匹配逻辑),所有使用了注解NotOnWeekends的方法都将会被WeekendBlocker拦截。

public class NotOnWeekendsModule extends AbstractModule {
  protected void configure() {
    bindInterceptor(Matchers.any(), Matchers.annotatedWith(NotOnWeekends.class), 
        new WeekendBlocker());
  }
}
```java
接着就可以实现拦截器了,实现AOP盟约的接口MethodInterceptor:
```java
public class WeekendBlocker implements MethodInterceptor {
  public Object invoke(MethodInvocation invocation) throws Throwable {
    Calendar today = new GregorianCalendar();
    if (today.getDisplayName(DAY_OF_WEEK, LONG, ENGLISH).startsWith("S")) {
      throw new IllegalStateException(
          invocation.getMethod().getName() + " not allowed on weekends!");
    }
    return invocation.proceed();
  }
}

如果有某个Bean的方法加上了注解NotOnWeekends,那么在周末执行的时候就会抛错。

源码解析

我们直接看ConstructorBindingImpl代码来分析如何实现Bean的生成,Guice是在构造Bean的时候直接生成代理的,com.google.inject.internal.ConstructorBindingImpl.initialize方法初始化了ConstructorInjector对象,这个对象包含了ConstructionProxy对象,而ConstructionProxy对象是由内部隐藏的一个代理工厂com.google.inject.internal.ProxyFactory<T>类生成的。

public ConstructionProxy<T> create() throws ErrorsException {
  if (interceptors.isEmpty()) {
    return new DefaultConstructionProxyFactory<T>(injectionPoint).create();
  }

  @SuppressWarnings("unchecked")
  Class<? extends Callback>[] callbackTypes = new Class[callbacks.length];
  for (int i = 0; i < callbacks.length; i++) {
    if (callbacks[i] == net.sf.cglib.proxy.NoOp.INSTANCE) {
    callbackTypes[i] = net.sf.cglib.proxy.NoOp.class;
    } else {
    callbackTypes[i] = net.sf.cglib.proxy.MethodInterceptor.class;
    }
  }

  // Create the proxied class. We're careful to ensure that all enhancer state is not-specific
  // to this injector. Otherwise, the proxies for each injector will waste PermGen memory
  try {
    Enhancer enhancer = BytecodeGen.newEnhancer(declaringClass, visibility);
    enhancer.setCallbackFilter(new IndicesCallbackFilter(methods));
    enhancer.setCallbackTypes(callbackTypes);
    return new ProxyConstructor<T>(enhancer, injectionPoint, callbacks, interceptors);
  } catch (Throwable e) {
    throw new Errors().errorEnhancingClass(declaringClass, e).toException();
  }
  }

ConstructionProxy有三个实现FastClassProxy、ReflectiveProxy和ProxyConstructor,在有拦截器时,采用ProxyConstructor实例化对象。

//com.google.inject.internal.ProxyFactory.ProxyConstructor.newInstance(Object...)
public T newInstance(Object... arguments) throws InvocationTargetException {
  Enhancer.registerCallbacks(enhanced, callbacks);
  try {
    return (T) fastClass.newInstance(constructorIndex, arguments);
  } finally {
    Enhancer.registerCallbacks(enhanced, null);
  }
}

总结

Spring很好的利用扩展机制实现了AOP,Guice采用了简单优雅的方式使用AOP。

在研究实现代理模式的源码,我们发现基本上所有的框架都会有一个类ProxyFactory,它隐藏了JDK动态代理和Cglib动态代理的实现,对外提供一个代理对象。

JVM(三)类和对象的生命周期

本文从虚拟机加载和执行层面探讨下类或者接口(下文统一使用类)、对象的生命周期。

命令行编译和执行代码

在初学Java的时候很多人都学过在没有IDE的情况下编译(javac)和执行(java)代码,后来你会彻底爱上IDE,它可以自动编译和管理依赖,你只需要运行代码就行了,我们这里先来回顾下如何通过命令行编译和执行代码,项目目录结构如下:

+-| com
|----+- deepoove
|-----------+- java8
|---------------+- def
|-------------------\- Apple.java
|---------------\- ClassLoaderTest.java

我们写个非常简单的类ClassLoaderTest,引用了Gson类和目录下面的Apple类:

package com.deepoove.java8;

import com.deepoove.java8.def.Apple;
import com.google.gson.Gson;

public class ClassLoaderTest {

  public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    System.out.println("Apple.class: " + Apple.class.getClassLoader());

    Apple[] array = new Apple[5];
    System.out.println(array.getClass() + ": " + array.getClass().getClassLoader());

    ClassLoader classLoader = Gson.class.getClassLoader();
    System.out.println("Gson.class classloader:");
    while (null != classLoader) {
      System.out.println("\t" + classLoader);
      classLoader = classLoader.getParent();
    }

    System.out.println("String.class: " + String.class.getClassLoader());
  }
}

接下来就可以进行编译了,我们进入com同级目录下执行javac编译命令:

javac -sourcepath . -classpath /Users/Sayi/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.5/de8829/gson-2.8.5.jar com/deepoove/java8/ClassLoaderTest.java

-sourcepath指定源代码路径,-classpath指定了依赖类的路径,命令执行成功后,会在ClassLoaderTest.java同级目录下,生成字节码文件ClassLoaderTest.class。

通过java命令可以执行字节码文件了:

$ java -classpath .:/Users/Sayi/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.8.5/de8829/gson-2.8.5.jar com/deepoove/java8/ClassLoaderTest
Apple.class: sun.misc.Launcher$AppClassLoader@2a139a55
class [Lcom.deepoove.java8.def.Apple;: sun.misc.Launcher$AppClassLoader@2a139a55
Gson.class classloader:
	sun.misc.Launcher$AppClassLoader@2a139a55
	sun.misc.Launcher$ExtClassLoader@4aa298b7
String.class: null

注意的是,在执行字节码时,classpath路径多了一个点和冒号: .:,这就和类加载机制有关系了,会尝试从当前目录和gson.jar寻找并加载类,java命令会把用户类路径存储在java.class.path属性,可以通过查看这个属性看到应用从哪些路径加载类,这个属性的配置可能来自于:

  1. 默认值是 '.', 表示当前目录或者子目录下寻找所有的字节码类文件
  2. 通过系统环境变量CLASSPATH改写默认值
  3. 通过-classpath 或者 -cp 改写默认值和CLASSPATH值

因为路径会被改写,所以在执行代码时 . 和 gson.jar 都需要在-classpath中指定。

字节码文件和javap

代码执行过程是首先从字节码加载类,然后从一个static main方法为入口执行。字节码文件是一个类的二进制表示,我们需要理解它的结构才能很好的阅读,同时也有很多框架提供了字节码操纵:cglib、javassist等,我们可以通过javap命令反编译字节码文件查看:

Classfile /Users/Sayi/reflections-example/bin/com/deepoove/example/Reader.class
  Last modified 2018-11-19; size 435 bytes
  MD5 checksum eb2714fb2d57cda321faf74167890254
  Compiled from "Reader.java"
public interface com.deepoove.example.Reader
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
   #1 = Class              #2             // com/deepoove/example/Reader
   #2 = Utf8               com/deepoove/example/Reader
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               read
   #6 = Utf8               (Ljava/io/File;)Ljava/util/List;
   #7 = Utf8               Signature
   #8 = Utf8               (Ljava/io/File;)Ljava/util/List<Ljava/lang/String;>;
   #9 = Utf8               url
  #10 = Utf8               MethodParameters
  #11 = Utf8               getCode
  #12 = Utf8               ()I
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/deepoove/example/Reader;
  #18 = Utf8               SourceFile
  #19 = Utf8               Reader.java
{
  public abstract java.util.List<java.lang.String> read(java.io.File);
    descriptor: (Ljava/io/File;)Ljava/util/List;
    flags: ACC_PUBLIC, ACC_ABSTRACT
    Signature: #8                           // (Ljava/io/File;)Ljava/util/List<Ljava/lang/String;>;
    MethodParameters:
      Name                           Flags
      url

  public int getCode();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: bipush        100
         2: ireturn
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   Lcom/deepoove/example/Reader;
}
SourceFile: "Reader.java"

字节码二进制文件中并不是所有字段都是存在的,比如MethodParameters(需要在编译时加上-parameter参数)、LocalVariableTable(需要在编译时加上-g或者-g:vars)等, 如果有些功能依赖于字节码中的这些字段,那么就要确保在编译的时候加上合适的参数。

类的生命周期

类从字节码文件被加载开始到使用,直至最后被卸载是它完整的生命周期。

image

类在被Link前,一定会完全被Load;类在被Initialization前一定会完全被verification和Preparation,Resolution是一个可选的阶段。

下面对这些步骤作一个简单介绍。

Load 加载

Load涉及到通过一个binary name和类加载器将一个二进制数据(classFile)分配到方法区,构造Class对象的过程,这个过程是由ClassLoader和其子类实现的。

binary name是一个类的二进制名称,比如java.security.KeyStore$Builder$FileBuilder$1

在Load过程中,可能发生的异常有:

  1. ClassCircularityError:A class or interface could not be loaded because it would be its own superclass or superinterface
  2. ClassFormatError:二进制数据格式错误
  3. NoClassDefFoundError、ClassNotFoundException:找不到关联的Class的定义
  4. OutOfMemoryError:因为涉及到方法区的分配,所以可能会导致内存溢出

Link 链接

链接主要分为三步:verification、Preparation和Resolution,在链接的过程,涉及到内存分配,有可能会抛出OutOfMemoryError。

verification
verification是对字节码文件是否满足一些限制的验证,这一步可能会导致其它的类被Load,但是其他类不是必须要被verification。

当校验无法通过时,会抛出VerifyError异常。

Preparation
对类的静态字段进行创建和初始化默认值,注意的是,明确的对类的一个静态字段赋值是Initialization的一部分,不属于Preparation

Resolution(Optional)
Resolution是决定类对它引用的其它类或者接口符号引用的过程。可能会抛出如下异常:

  1. IllegalAccessError:没有权限引用
  2. InstantiationError:抽象类无法实例化
  3. NoSuchFieldError:找不到属性
  4. NoSuchMethodError:找不到方法
  5. IncompatibleClassChangeError:不兼容的类改变

Initialization 初始化

当调用了静态方法、字段的或者明确创建对象、利用反射API等场景都会触发类的初始化,执行类的静态初始化方法。

Unloading 卸载

一个类可能会被卸载,当且仅当它的加载器被回收,注意的是,bootstrap class loader加载的类不会被卸载。

对象的生命周期

对象从被实例化到使用,直至最后被垃圾回收是它完整的生命周期。

image

instantiation and initialization 实例化和初始化

一个对象可能会被显式或者隐式创建,比如new操作符,比如lambda表达式,字符串常量和装箱等。

对象创建都会涉及到内存分配。

Garbage collection and Finalization of Objects 回收

对象在不可达时会被垃圾回收标记,在第一次标记时会调用每个对象的protected void finalize() throws Throwable { }方法,这是被垃圾回收器自动调用的,在进入第二次标记时,这个对象将会被回收。

注意的是,finalize方法有且仅会被调用一次,我们也可以重写finalize方法,将当前对象重新挂到引用链时从而避免被垃圾回收,但是第二次不可达时则会直接被回收,不会再执行finalize方法。

Class.forName vs ClassLoader

我们已经知道了类从加载到初始化直至被卸载的流程,类加载过程是由ClassLoader实现,JDK还提供了Class.forName加载类获得Class对象,它们有什么区别呢?

数组加载

数组不是由类加载器加载的,它们是在需要的时候被虚拟机自动创建。但是每个数组类里面都有一个ClassLoader的引用,这个值和数组的元素类型的ClassLoader是相同的,如果是原生类型,则这个CLassLoader为空。

我们可以换个角度思考,由于CLassLoader是需要binary name,而数组其实是没有一个二进制名称的,所以无法通过ClassLoader加载,但是数组有一个代表数组类的名称,所以可以通过CLass.forName去加载:

Class<?> forName = Class.forName("[Lcom.deepoove.java8.def.Apple;");

上面这串代码就加在了Apple[]数组类型,由于没有指定维度,所以是无法创建实例的,数组初始化可以使用反射包下的java.lang.reflect.Array.newArray(Class<?>, int)来创建数组实例。

JDBC驱动类加载

学过JDBC的人都知道第一行代码就是加载JDBC驱动:

Class.forName("com.mysql.jdbc.Driver");

从含义上来说,它真的是加载驱动吗?如果只是加载这个类,只要这个类在用户路径下何须手动加载,即使要加载,ClassLoader就可以完成加载?

这里就涉及到类的生命周期和ClassLoader内在实现问题了,ClassLoader加载的类并不会进行Initialization初始化阶段,而Class.forName("")会进行类的初始化,调用Driver类相关的静态初始化方法,而这些初始化方法的代码才是加载JDBC驱动真正要做的事情,我们来看看Class.forName的源码:

public static Class<?> forName(String className) throws ClassNotFoundException {
  Class<?> caller = Reflection.getCallerClass();
  return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

private static native Class<?> forName0(String name, boolean initialize,
                     ClassLoader loader,
                     Class<?> caller) throws ClassNotFoundException;

看到native方法支持initialize和ClassLoader参数,initialize为true则会进行类的初始化。

扩展:JDBC 4.0 以后已经不需要通过Class.forName加载并初始化驱动类了,通过SPI机制实现了加载和初始化。

参考资料

总结

我们知道应用程序如何加载类以及初始化的时机,我们学到了字节码文件从加载到执行的整个过程,理解类和对象的生命周期,可以帮助我们更好的理解问题和优化我们的程序。

下一篇文章,我们研究下类加载机制。

Volley 源码设计

Volley是一个由Google开发的Http网络库,让Android的网络操作更简单,更快。具有以下特点:

  • 自动调度网络请求
  • 并发请求
  • 与HTTP缓存头一致性的透明缓存机制
  • 支持请求的优先级
  • 支持取消请求
  • 重试机制
  • Android异步更新UI

本文将尝试从这些特性入手,去理解Volley的设计以及实现。

底层网络请求

设计网络库,首先要确定底层HTTP请求的处理方式。Volley提供了接口HttpStack,接口只有一个方法,就是处理请求,获得响应值。

根据sdk版本不同,Volley提供了不同的实现,基于HttpURLConnection的类HurlStack(sdk>=9)和基于httpclient的HttpClientStack(sdk<9)。

我们可以自己实现接口,自定义请求过程中的数据。如设置请求header,定义一个固定的User-Agent值,支持URL重写规则等。

定义请求模型

http request
确定了如何处理底层的HTTP请求,接下来就该考虑如何定义一个请求模型,将请求交给HttpStack进行处理。请求包含一个Url、headers、参数、method(GET\POST等)、请求Body等。

熟悉Spring配置请求的方式,我们会想到是否可以用注解去表述一个请求,如注解@RequestMapping、@RequestHeader、@RequestParameter、@post@requestbody,而另一个流行的Android网络库retrofit恰恰使用了注解的方式来定义请求模型。在Volley则提供了抽象模板类Request<T>作为请求模型。

Volley中的Request<T>是可排序的,即实现了Comparable接口,排序即意味着为请求优先级做好了准备。为了实现更多特性,Request除了表述一个请求该有的字段外,还拥有更多属性和方法。

  • 定义了如何解析响应结果的抽象方法parseNetworkResponse,继承Request<T>的类实现此方法,解析对应的响应数据。
  • 注入了监听器listener,提供了deliverResponse和deliverError的方法,方便在主线程上更新UI
  • 通过字段mShouldCache来确定此请求是否是可缓存的
  • 通过字段mCanceled标识此请求是否已经取消
  • 通过枚举Priority和mSequence支持设定优先级和处理顺序
  • 设置RetryPolicy,定义了当前请求的重试策略

处理响应数据

在底层网络请求之上,再做一次封装,负责处理自相关的代码逻辑。这样做的目的是让底层网络请求模块可以任意替换。

Volley提供了分发器ResponseDelivery,利用Request注册的监听器在主线程中处理响应结果。除了处理响应值,分发器还提供了一个Runnable的参数,支持在请求结束后,执行自己自定义的代码,此功能在Soft-expired cache hit时,缓存中的响应结果分发结束后,紧接着执行当前请求时有使用(需要理解Soft-expired)。

多线程并发模型

Request-Process-Response 是一个单线程的操作。支持并发的通用做法是每次请求都在一个单独线程中操作,使用线程池管理每个线程。Volley并没有使用线程池,而是常驻了四个网络调度线程(NetworkDispatcher),这些线程保持阻塞状态,直到接收到新的Request。

在多线程模型中,共享源数据可以使用队列。BlockingQueue是一个阻塞队列,当队列为空时,消费者线程将保持等待状态,它是线程安全的。Volley正是利用了阻塞队列的特性,将新的请求Request推入队列,四个网络调度线程(NetworkDispatcher)从队列中取出请求进行处理。上文提到过,Request是可排序的,所以这并不是一个FIFO队列,而是PriorityBlockingQueue,根据Request进行排序的队列。

从队列中取出Request,进行网络请求调度,为了支持缓存,每个Request会优先进入另一个线程(未指明这个请求是不可缓存的):缓存调度线程(CacheDispatcher)。在后台会常驻一个缓存调度线程,如果命中缓存,返回缓存内容,没有命中,则进入网络调度线程。那么问题来了,如果是同一个队列,从队列已经取出的Request进入缓存调度,如何再次进行网络调度?答案是两个队列,一个Request缓存请求队列,一个是Request网络请求队列,所以每个Request(未指明这个请求是不可缓存的)都会优先进入缓存请求队列,未命中,再次进入网络请求队列。

综上,Volley将Request请求推入具有优先级的缓存阻塞队列和网络请求阻塞队列中,并且使用1个缓存调度线程和4个网络调度线程分别处理这两个队列的请求。
volley.png

缓存调度线程

缓存调度线程处理Request缓存队列。每个Request定义getCacheKey()作为缓存KEY,默认KEY(后文会提到这样的缓存KEY会带来问题)是:

Method:Url

缓存是否命中的逻辑如下:

  1. 是否是个已经取消的请求,如果是,直接取消请求。
  2. 通过缓存KEY是否取到缓存,如果未取到,将Request推入网络请求队列中。
  3. 判断缓存内容是否过期,缓存内容包含了HTTP的缓存头信息,比如Last-Modified、ETag、Expires等。
    如果过期,将Request推入网络请求队列中。
    如果未过期,取缓存内容进行处理,ResponseDelivery将分发响应结果。

默认的缓存是存在磁盘上。

网络调度线程

网络调度线程处理Request网络请求队列,队列的请求可能是应用直接推入的,也可能是缓存未命中推入的。

网络调度线程做的事情很简单,在底层网络请求基础上,发送请求,封装响应结果,存入缓存,由ResponseDelivery分发结果。其中,缓存内容就包括此次响应的HTTP的缓存头信息,同时这里实现了重试机制:在一个无限循坏中,进行网络请求,如果请求成功则返回,退出循环,如果请求失败,根据失败类型判断是否重试,若不满足重试条件,则抛出异常,退出循环。所有的重试设置都是通过接口RetryPolicy来定义的。

这里对网络请求异常做了一次封装,异常用VolleyError表示,返回到主线程处理。同时用户可以在Request.parseNetworkError做再一次异常封装。目前有的异常类型为:

  • AuthFailureError 认证失败,状态码为401或403
  • ClientError 状态码为[400,499]
  • NoConnectionError 无网络连接
  • TimeoutError 超时
  • ServerError 服务异常
  • NetworkError 网络异常

队列调度以及并发下的重复请求

我们可以通过缓存调度线程和网络调度线程处理队列数据,那么我们又如何将管理缓存队列和网络请求队列,以及优化并发下的重复请求?

为了更简单,我们需要掩盖两个队列的细节,造成只有一个请求队列的假象。对用户来说,只需要定义Request<T>,然后加入这个队列即可。Volley提供了RequestQueue将请求分发给不同的队列,它总是会优先加入缓存队列,如果请求指定了是不可缓存的,则会推入网络请求队列。注意RequestQueue并不是一个真正的队列,而是队列调度。我们要做的就是获得RequestQueue对象,然后调用添加请求的方法即可。

public <T> Request<T> add(Request<T> request)

如果不做任何处理,并发情况下同时来了10个一样的请求,程序会如何运行?下面会是一种可能的运行方式:

  1. Request_seq1进入缓存队列,缓存调度线程处理Request_seq1
  2. 缓存调度线程处理Request_seq1,未命中缓存,进入网络请求队列
  3. Request_seq2进入缓存队列,缓存调度线程处理
  4. 缓存调度线程处理Request_seq2,未命中缓存,进入网络请求队列
  5. ...

这个运行方式已经出现了看出了问题,对于同样的请求Request_seq2并未使用到缓存,因为Request_seq1并没有处理结束,所以未产生缓存内容。

Volley采用了对重复请求进行集体等待的策略,当发现已经有同样的请求在处理,就会推入等待队列,保证当前只有一个请求会被处理,直到这一个请求完成,才会唤醒等待队列中同样的请求,进入到缓存队列,所以等待队列是在缓存队列和网络请求队列前的一个保存重复请求的队列。
这里又多了一个等待队列的概念,数据结构是:

Map<String, Queue<Request<?>>>

其中map的KEY是Request的唯一标识CacheKey,Value则是重复请求的队列。

再谈Request

Volley中Request的设计是复杂的,它承担了本来不该属于一个请求的功能,杂糅了请求信息、配置信息、重试机制、响应处理等太多内容。默认提供了以下几种请求:

  • StringRequest
  • JsonObjectRequest
  • JsonArrayRequest
  • ImageRequest

Request没有提供Setter方法设置请求信息,而是定义了一些列的Getter方法,在初始化Request对象的时候重写这些Getter方法。

  • byte[] getBody()
    post、put请求body内容
  • String getBodyContentType()
    请求body的contentType,默认为application/x-www-form-urlencoded,如果是json传输,指定为application/json
  • Map<String, String> getHeaders()
    请求头
  • Map<String, String> getParams()
    请求参数

通过重写Getter方法定义的Request对象,有个特点,就是在初始化后,无法再进行设置操作。请求信息伴随着Request的实例化而定义完毕。如果想统一设置请求头,就要在初始化每个Request的时候重写特定方法,也可以重写底层访问请求,设置请求头。还有个方式,就是实现个基类Request,重写getHeaders()方法,前提是项目中需要设置请求头的请求都要继承重写的Request。

默认Request都是可缓存的,可以显示调用setShouldCache设置此请求是否可缓存。在大多数查询的Request中,我们希望都是可缓存的,但是当我们对查询结果进行增删改后,再次查询时,往往是不希望从缓存获取,而是重新进行网络请求。大多数浏览器可以通过刷新按钮强制过滤掉缓存,重新加载数据,在Volley中,我们可以尝试删除缓存数据,通过RequestQueue可以取到缓存对象,通过Request可以取到CacheKey,删除就水到渠成。

请求失败默认都是执行一次,如果设置了Request的RetryPolicy,可以合理的运用重试机制。DefaultRetryPolicy可以设置超时时间,超时次数,以及每次重试,增加超时时间的系数。

从0开始之化繁为简

化繁为简,整个流程已经简单了很多:

  1. 定义自己的Request
  2. 使用RequestQueue调度,启动缓存调度线程、网络调度线程
  3. 网络请求,分发响应结果到Request中注册的监听器

所有的细节仅仅需要一个好的外观去表述:队列调度和Request。忘记所有的细节,一切从0开始:

  1. Volley.newRequestQueue(context).add(request);

JVM(一)运行时数据区域和垃圾回收

虚拟机和Java语言一开始的设计是独立的,这也产生了很多面向虚拟机的语言,如Groovy等,它们最终生成格式标准的字节码文件被虚拟机装载和解析。本文对虚拟机的一些核心内容进行介绍,包括运行时数据区域和垃圾回收。

The Java® Virtual Machine Specification

运行时数据区域(Run-Time Data Areas)

Java虚拟机定义了在程序执行期间使用的各种运行时数据区域,其中一些数据区域是在Java虚拟机启动时创建的,仅在Java虚拟机退出时销毁。还有一些数据是每个线程拥有,这些线程数据区域是在线程退出时创建和销毁线程时创建的。我们先来看看一张图(Java Garbage Collection Basics):

image

从图中可以看到,总共分为5个区域,方法区和堆是所有线程共享的,其余三个区域是线程私有的

1.程序计数器Program counter register

每个线程都拥有自己的pc Register,可以简单的理解为当前线程的执行位置。在任意时刻,一条JVM线程只会执行一个方法的代码,该方法称为该线程的当前方法(Current Method), 如果该方法是Java方法,那计数器保存JVM正在执行的字节码指令的地址,如果该方法是Native,那PC寄存器的值为空(Undefined)

2.栈Java Stacks

每个线程都私有一个线程栈,存储线程使用的局部变量等信息。当前栈的大小可以通过命令java -XX:+PrintFlagsInitial | grep ThreadStackSize查看,通过-XX:ThreadStackSize 或者 -Xss设置栈大小。

栈描述了Java的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。在主内存中的共享变量在每个线程栈中都有个副本,它们之间会进行同步。

关于栈定义了如下两种异常:

  • StackOverflowError:栈调用深度超过了限制
  • OutOfMemoryError:栈空间不足

3.本地方法栈Native Method Stacks

本地方法栈也是每个线程私有的,用来支持native方法的执行,它和Java栈很相似,异常定义也是一致的。

4.堆Heap

堆是所有线程共享的区域,所有类的实例和数组都会分配到此区域。线程的数据区域是伴随着线程的创建和销毁,而共享区域堆是随着JVM启动的时候创建的。堆中对象存储管理是一种自动存储管理系统:垃圾收集机制。

堆的大小有以下参数可以设置:

  • -Xms 或者 -XX:InitialHeapSize:堆的初始化大小,必须是1024的倍数且大于1M
  • -Xmx 或者 -XX:MaxHeapSize:堆的最大小,必须是1024的倍数且大于2M,如果你知多大的堆会使你的程序工作良好,可以xms和xm通常设成一样的值,服务端一般都是如此设置。

虚拟机中的堆又根据垃圾回收策略不同,分为年轻代(young generation)和老年代(old generation),年轻代又分为Eden区和两个Survivor区,关于每个区的具体作用,会在下文垃圾收集描述中介绍。

调节年轻代大小的JVM参数是:

  • -XX:NewSize 设置堆内年轻代初始化大小
  • -XX:MaxNewSize 设置堆内年轻代最大大小
  • -Xmn 设置年轻代初始化和最大大小

年轻代在堆中的主要作用是用来存储新new的对象,这个区域的G较其它区域更频繁。如果年轻代设置过小,那么就会频繁的GC,如果设置过大,那么产生FullGC的时候又很耗时,一般推荐年轻代是整个堆大小的二分之一或者四分之一,默认比例是1/3,可以通过参数调节:

  • -XX:NewRatio Old/Young的空间比值,默认值为2
  • -XX:SurvivorRatio Eden/Survivor空间比值,默认值为8

通过下面这张图可以很容易体会到这种比值关系:
image

关于堆,定义了如下异常:

  • OutOfMemoryError:堆空间不足

5.Metaspace-方法区Method Area

方法区是所有线程共享的区域,主要存储类的结构信息,比如类信息、属性和方法数据、方法代码、运行时常量池等,方法区是伴随着JVM启动而创建的。

JDK1.7以前,方法区被称为永久代(PermGen),1.8后永久代被废弃,转而被MetaSpace区取代。那么相比较PerGen而言,MetaSpace又有什么进步呢?

MetaSpace空间默认是无上限的,这样就避免永久代经常出现的OutOfMemoryError:PermGen异常,MetaSpace空间也是可以通过参数来调节的,它有个很重要的特点就是会自动调节大小,这是永久代做不到的。

  • -XX:MetaspaceSize 初始化大小,首次GC阀值大小。
  • -XX:MaxMetaspaceSize 自动增长的上限,默认无上限,但是当本地内存占用过多,会影响其它程序。

Metaspace和PermGen都是方法区在具体虚拟机中实现,但是它们存储的信息是有区别的,PermGen会存储运行时常量池,但是Metaspace不包含常量池,这些常量池存储在堆Heap中。

Metaspace区定义了如下异常:

  • OutOfMemoryError:Metaspace空间不足

运行时常量池Run-Time Constant Pool

Class文件中除了有类的版本、字段、方法、接口等信息外,还有一项信息是常量池,用于存放编译期生成的各种字面常量和符号引用Class的基础信息存储在Metaspace中,而常量池存储在堆中,我们来看看常量池中的常量类型:

  • CONSTANT_Class
  • CONSTANT_Fieldref
  • CONSTANT_Methodref
  • CONSTANT_InterfaceMethodref
  • CONSTANT_String
  • CONSTANT_Integer
  • CONSTANT_Float
  • CONSTANT_Long
  • CONSTANT_Double
  • CONSTANT_NameAndType
  • CONSTANT_Utf8
  • CONSTANT_MethodHandle
  • CONSTANT_MethodType
  • CONSTANT_InvokeDynamic

在JDK 1.7 以前,由于常量池在PermGen区,当永久代空间太小,加入常量池的字符串过多,就会导致OutOfMemoryError:PermGen,在JDK1.8以后当堆空间过小就会抛出异常:java.lang.OutOfMemoryError: Java heap space。

String.intern()方法是将字符串加到常量池,如果常量池已经存在该字符串则不操作,最终返回常量池中字符串地址,这个特性在JDK1.6和JDK1.8也是有区别的,我们直接分析1.8的现象:

@Test
public void testStringPool1() {
    String s3 = new String("hello") + new String("Sayi");
    String s5 = s3.intern();
    System.out.println(s3 == s5);
    String s4 = "helloSayi";
    System.out.println(s3 == s4);
    System.out.println(s5 == s4);
}

这段代码最终的输出是:

true
true
true

可以看到,s3是堆内的一个空间,s3.intern()加入常量池后返回的引用s5和原先s3是一致的,即加入常量池中的字符常量地址就是堆内的创建字符串常量地址。我们把s4和s5换一个位置重新运行下:

@Test
public void testStringPool2() {
    String s3 = new String("hello") + new String("Sayi");
    String s4 = "helloSayi";
    String s5 = s3.intern();
    System.out.println(s3 == s5);
    System.out.println(s3 == s4);
    System.out.println(s5 == s4);
}

输出结果是:

false
false
true

因为s4是加入到常量池,s3.intern就会返回s4的引用地址,即s4==s5。

垃圾回收算法

我们先来看看如何判断一个对象已经是垃圾了?通常使用的算法是引用计数法和可达性分析法,引用计数法无法解决循环依赖的问题,虚拟机采用了可达性分析法,判断对象与GC Roots根节点之间是否可达,不可达的对象就可能会被垃圾回收(参考finalize()方法)。

HotSpot Virtual Machine Garbage Collection Tuning Guide

1.Mark-Sweep标记-清除算法

Mark-Sweep算法分为两个阶段:标记阶段和清除阶段。首先标记那些可达对象,然后清除未被引用的垃圾对象。

这个算法会产生内存碎片。

2.Copying复制算法

Copying算法是将内存区域分为两块,每次使用其中一块,垃圾回收时,将所有存活对象复制到内存的另一块区域,然后清除当前区域的对象。

由于采用了整体复制的算法,不会产生内存碎片,适合在垃圾对象较多存活对象较少的情形,但是缺点是当前可用只有部分内存。

3.Mark-Compact标记-整理算法

Mark-Compact算法是在Mark-Sweep的基础上作了优化,分为三个阶段:标记阶段、整理阶段、清除阶段。整理阶段是将所有的存活对象压缩到内存的一端,这样就避免了碎片的产生。

4.分代回收算法

前面已经知道,堆分为老年代和年轻代,老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,分代回收算法就是根据不同代的特点采取最适合的收集算法。

  • 年轻代
    采取Copying算法,划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间,当Survivor空间不足或者如此往复复制某些对象超过一定次数(新生代存活年龄)后就会进入老年代,对于一些大对象的创建可能会直接进入老年代。

  • 老年代
    采取Mark-Compact算法。

垃圾收集器

垃圾收集器是虚拟机中对垃圾回收算法的具体实现,它们有的用在年轻代,有的用在老年代,有的都可以用。关于如何组合使用可以参考一些虚拟机的书籍,我们来看看虚拟机的三种GC和两个指标。

  • Minor GC:年轻代的垃圾回收
  • Major GC VS Full GC:Major GC指老年代的垃圾回收,Full GC指整个堆的垃圾回收

但是每次Major发生时,都至少会出现一次Minor GC,所以这两个概念并没有严格的区分,我们更应该关注指标:

  • Stop the world(STW) event:GC会导致暂停用户线程,对于交互频繁的应用来说,意味着用户等待,所以这个数值越小越好。
  • Throughput:这个指标重点没有放在STW上,而是放在吞吐量上,如果系统运行了 100min,GC 耗时 1min,那么系统的吞吐量就是 (100-1)/100=99%。

接下来我们看看具体的垃圾收集器。

Serial GC和Serial Old

这是历史最悠久的垃圾收集器,单线程收集器,GC时会发生STW。Serial GC采用Coping算法,Serial Old采用Mark-Compact算法。

JVM Options 功能
-XX:+UseSerialGC 使用单线程收集器

ParNew GC和CMS(Concurrent Mark-Sweep)

为了减少STW的时间,出现了Serial GC的并发版本,称为ParNew GC,采用Coping算法,适用于多核CPU情况。

老年代回收通常使用CMS收集器,一个并发的标记-清除算法,它的GC几乎可以做到和用户线程同时进行,极大的减少了STW的时间。主要分为五步:

  1. 初始标记(STW initial mark)
  2. 并发标记(Concurrent marking)
  3. 并发预清理(Concurrent precleaning)
  4. 重新标记(STW remark)
  5. 并发清理(Concurrent sweeping)

其中只有步骤1和步骤4会产生STW事件,而步骤1初始标记产生的STW微乎其微。

JVM Options 功能
-XX:+UseParNewGC 新生代使用ParNew收集器
-XX:+UseConcMarkSweepGC 新生代使用ParNew(无需指定,默认设置),老年代使用CMS

Parallel Scavenge和Parallel Old

相比其它收集器关注STW,这是一组更加关注吞吐量的组合。

JVM Options 功能
-XX:+UseParallelGC 新生代使用ParallelGC,老年代默认使用ParallelOldGC
-XX:+UseParallelOldGC 老年代默认使用ParallelOldGC,新生代默认使用ParallelGC
-XX:ParallelGCThreads 并发GC线程数

G1收集器

G1(garbage-first)是最前沿的虚拟机成果,引入Region的概念,内存块逻辑连续但是物理上可以不连续,年轻代和老年代都使用G1收集器。

JVM Options 功能
-XX:+XX:+UseG1GC 使用G1收集器

总结

了解完本文后,我们希望利用知识去解决更具实际意义的问题,下一篇文章将会分析和解决JVM故障。

依赖注入(三)Guice Dependency Injection

Guice开发者说过,如果没有Spring,Guice可能根本不会出现,或者不会这么早出现。Guice源自于Google的一个项目AdWords,当他们坐下来去思考自己到底想要如何构造这个项目的时候,guice给了他们答案。

相比于Spring这个全面的解决方案栈,Guice则专注于依赖注入,全文搜索引擎elasticsearch使用Guice作为依赖注入组件。

简单示例

Guice是一个轻量级的DI框架,我们先从一个简单的里子开始。

  1. 我们先写一个接口和实现类
public interface AccountService {
  String get();
}

public class AccountServiceImpl implements AccountService {
  final UserService userService;

  @Inject 
  public AccountServiceImpl( UserService userService) {
    this.userService = userService;
  }

  @Override
  public String get() {
    userService.setUserName("Sayi");
    return userService.getUserName();
  }
}
  1. Guice摒弃了XML配置方式,通过bindings关联类型和实现,继承于AbstractModule
public class GuiceModule extends AbstractModule {

  @Override
  protected void configure() {
    bind(AccountService.class).to(AccountServiceImpl.class).in(Singleton.class);
    bind(UserService.class).to(UserServiceImpl.class);
  }

}
  1. 通过Module创建Injector,获得Bean
@Test
public void testGuice() {
  Injector injector = Guice.createInjector(new GuiceModule());
  AccountService accountService = injector.getInstance(AccountService.class);
  Assert.assertEquals(accountService.get(), "Sayi");
}

通过这个例子,我们可以觉察到bind语法的简单易懂,就像写一句话一样把绑定关系描述清楚,它的核心容器是Injector。
image

Guice依赖注入 和 JSR330

参见https://github.com/google/guice/wiki/JSR330

Guice:com.google.inject JSR330:javax.inject
@Inject @Inject
@BindingAnnotation @Qualifier
@Named @Named
@ScopeAnnotation @Scope
@Singleton @Singleton
Provider Provider

源码分析

我们跟随着Guice.createInjector的初始化源码来一探究竟。容器的默认实现InjectorImpl持有State对象,State包含了注入器的数据,其中存储Bean的数据结构为:

private final Map<Key<?>, Binding<?>> explicitBindingsMutable = Maps.newLinkedHashMap();

与Spring的<String, BeanDefinition>不同的设计是,Guice的绑定关系是<Key, Binding>,这也是它巧妙的地方,Key是用来匹配注解或者类型,允许Guice支持泛型Bean的创建(TypeLiteral),Binding则是提供了获取Bean实例的策略。

关于Spring的泛型支持,参见文章https://spring.io/blog/2013/12/03/spring-framework-4-0-and-java-generics

Injector初始化时序图

image
从时序图,我们可以看到几个重点:

第5步configure正是暴露绑定配置的代码,最终生成一些固定元素Element

所有的绑定操作都是通过Binder以及对应的Builder完成的。从下图可以看到,可以对注解绑定(AnnotatedBindingBuilder),可以对类型绑定(LinkedBindingBuilder),可以对Scope绑定(ScopedBindingBuilder)。
image

第7-21步采用了类似的设计,不断处理第6步返回的所有元素Element,这里采用了Vistor访问者设计模式,Element和ElementVistor之间的交互,其中第11、14、17步则会构建Binding,进而完善InjectorImpl的初始化。

我们从源码中可以很清晰的看到这个流程:

List<InjectorShell> build(
    Initializer initializer,
    ProcessedBindingData bindingData,
    Stopwatch stopwatch,
    Errors errors) {
  checkState(stage != null, "Stage not initialized");
  checkState(privateElements == null || parent != null, "PrivateElements with no parent");
  checkState(state != null, "no state. Did you remember to lock() ?");

  // bind Singleton if this is a top-level injector
  if (parent == null) {
    modules.add(0, new RootModule());
  } else {
    modules.add(0, new InheritedScannersModule(parent.state));
  }
  elements.addAll(Elements.getElements(stage, modules));

  // Look for injector-changing options
  InjectorOptionsProcessor optionsProcessor = new InjectorOptionsProcessor(errors);
  optionsProcessor.process(null, elements);
  options = optionsProcessor.getOptions(stage, options);

  InjectorImpl injector = new InjectorImpl(parent, state, options);
  if (privateElements != null) {
    privateElements.initInjector(injector);
  }

  // add default type converters if this is a top-level injector
  if (parent == null) {
    TypeConverterBindingProcessor.prepareBuiltInConverters(injector);
  }

  stopwatch.resetAndLog("Module execution");

  new MessageProcessor(errors).process(injector, elements);

  /*if[AOP]*/
  new InterceptorBindingProcessor(errors).process(injector, elements);
  stopwatch.resetAndLog("Interceptors creation");
  /*end[AOP]*/

  new ListenerBindingProcessor(errors).process(injector, elements);
  List<TypeListenerBinding> typeListenerBindings = injector.state.getTypeListenerBindings();
  injector.membersInjectorStore = new MembersInjectorStore(injector, typeListenerBindings);
  List<ProvisionListenerBinding> provisionListenerBindings =
      injector.state.getProvisionListenerBindings();
  injector.provisionListenerStore =
      new ProvisionListenerCallbackStore(provisionListenerBindings);
  stopwatch.resetAndLog("TypeListeners & ProvisionListener creation");

  new ScopeBindingProcessor(errors).process(injector, elements);
  stopwatch.resetAndLog("Scopes creation");

  new TypeConverterBindingProcessor(errors).process(injector, elements);
  stopwatch.resetAndLog("Converters creation");

  bindStage(injector, stage);
  bindInjector(injector);
  bindLogger(injector);

  // Process all normal bindings, then UntargettedBindings.
  // This is necessary because UntargettedBindings can create JIT bindings
  // and need all their other dependencies set up ahead of time.
  new BindingProcessor(errors, initializer, bindingData).process(injector, elements);
  new UntargettedBindingProcessor(errors, bindingData).process(injector, elements);
  stopwatch.resetAndLog("Binding creation");

  new ModuleAnnotatedMethodScannerProcessor(errors).process(injector, elements);
  stopwatch.resetAndLog("Module annotated method scanners creation");

  List<InjectorShell> injectorShells = Lists.newArrayList();
  injectorShells.add(new InjectorShell(elements, injector));

  // recursively build child shells
  PrivateElementProcessor processor = new PrivateElementProcessor(errors);
  processor.process(injector, elements);
  for (Builder builder : processor.getInjectorShellBuilders()) {
    injectorShells.addAll(builder.build(initializer, bindingData, stopwatch, errors));
  }
  stopwatch.resetAndLog("Private environment creation");

  return injectorShells;
}

所有的Processor都是实现了ElementVIstor接口,他们遍历所有Element,处理所有需要处理的Element。其中InterceptorBindingProcessor处理拦截器元素InterceptorBinding,ScopeBindingProcessor处理会话周期元素ScopeBinding,BindingProcessor则是处理绑定元素Binding。

Vistor的类关系图如下:
image

所有元素的关系图如下(其中上半部分是所有待处理的元素Element,下半部分是处理后的Binding,即绑定的值):

image

获取Bean实例

我们以构造器注入为例,来看看如何实例化一个Bean,核心源码是ConstructorInjector.construct方法。

Object construct(
  final InternalContext context,
  Dependency<?> dependency,
  /* @Nullable */ ProvisionListenerStackCallback<T> provisionCallback)
  throws InternalProvisionException {
final ConstructionContext<T> constructionContext = context.getConstructionContext(this);
// We have a circular reference between constructors. Return a proxy.
if (constructionContext.isConstructing()) {
  // TODO (crazybob): if we can't proxy this object, can we proxy the other object?
  return constructionContext.createProxy(
      context.getInjectorOptions(), dependency.getKey().getTypeLiteral().getRawType());
}

// If we're re-entering this factory while injecting fields or methods,
// return the same instance. This prevents infinite loops.
T t = constructionContext.getCurrentReference();
if (t != null) {
  if (context.getInjectorOptions().disableCircularProxies) {
    throw InternalProvisionException.circularDependenciesDisabled(
        dependency.getKey().getTypeLiteral().getRawType());
  } else {
    return t;
  }
}

constructionContext.startConstruction();
try {
  // Optimization: Don't go through the callback stack if we have no listeners.
  if (provisionCallback == null) {
    return provision(context, constructionContext);
  } else {
    return provisionCallback.provision(
        context,
        new ProvisionCallback<T>() {
          @Override
          public T call() throws InternalProvisionException {
            return provision(context, constructionContext);
          }
        });
  }
} finally {
  constructionContext.finishConstruction();
}
}

constructionContext.createProxy正是为了解决循环依赖问题,和Spring的@lazy原理类似,当需要一个正在构造的Bean的实例时,会先创建代理类注入,等真实的Bean构造完毕后,再将真实的Bean设置到代理类里,具体设置是通过constructionContext.setProxyDelegates(t)方法实现的。

/** Provisions a new T. */
private T provision(InternalContext context, ConstructionContext<T> constructionContext)
  throws InternalProvisionException {
try {
  T t;
  try {
    Object[] parameters = SingleParameterInjector.getAll(context, parameterInjectors);
    t = constructionProxy.newInstance(parameters);
    constructionContext.setProxyDelegates(t);
  } finally {
    constructionContext.finishConstruction();
  }

  // Store reference. If an injector re-enters this factory, they'll get the same reference.
  constructionContext.setCurrentReference(t);

  MembersInjectorImpl<T> localMembersInjector = membersInjector;
  localMembersInjector.injectMembers(t, context, false);
  localMembersInjector.notifyListeners(t);

  return t;
} catch (InvocationTargetException userException) {
  Throwable cause = userException.getCause() != null ? userException.getCause() : userException;
  throw InternalProvisionException.errorInjectingConstructor(cause)
      .addSource(constructionProxy.getInjectionPoint());
} finally {
  constructionContext.removeCurrentReference();
}
}
}

这段代码主要做了下面三件事:

  1. constructionProxy.newInstance(parameters); 构造Bean
  2. 为先前的代理类设置真实代理对象
  3. injectMembers注入依赖

內建绑定(Built-in Bindings)

从源码中我可以看到,Guice默认绑定了两个类型:

bindInjector(injector);
bindLogger(injector);

所以我们可以实现对容器Injector和Logger对象的注入。

关于泛型

比如我们有个泛型类BaseService,可以在Module中进行绑定

bind(new TypeLiteral<BaseService<String>>(){});

接下来就可以获取这个Bean的实例了:

BaseService<String> instance = injector.getInstance(Key.get(new TypeLiteral<BaseService<String>>(){}));

总结

理解Guice的核心就是理解访问者设计模式的运用,整体设计简单且优雅。

这是关于依赖注入的最后一篇文章,我们看到DI框架如何兼容JSR330,如何设计存储数据结构,如何读取绑定配置,如何解决循坏依赖和如何优雅的进行框架设计等,AOP不仅仅作为一门切面编程的技术,还可以很好的与依赖注入融合,关于AOP编程,会在后面的文章中深入阐述。

并发(二)基础之访问安全篇

本篇文章我们尝试探讨多线程带来的问题,多个线程对共享数据的访问可能会产生多线程干涉和内存一致性问题,同步操作引起的竞争可能会带来新的病态行为,死锁、活锁、饿死等。 所有这些问题我们都必须仔细对待,透彻的理解,因为它将影响你程序的正确性。 这篇文章只能算作一个浅浅的导引,因为每次重新理解多线程机制,我总会有新的认识。

多线程干涉

多个线程访问同一个对象,那么在这个对象上的操作对单线程来说,顺序是一定的,但是多线程之间可能会相互交错重叠。比如线程A希望某个对象执行ABC三个步骤,而线程B希望这个对象执行CDE步骤,最终这个对象的操作顺序可能会交错执行。

NOTE:多个线程对同一个对象操作出现交错是步骤交错,而不是语句交错,即如果线程A执行c++,而线程B执行c--,看似单条语句不会交错,但是他们会被虚拟机翻译成多个步骤,在这些步骤中就会出现交错。

class Counter {
  private int c = 0;

  public void increment() {
    c++;
  }

  public void decrement() {
    c--;
  }

  public int value() {
    return c;
  }
}

我们来看看上面的代码,如果假设c++操作被翻译成三个步骤,1:获取c的值->2:将值增加1->3:存储计算后的值到c,线程A执行increment(),线程B执行decrement(),最终结果c就可能很多样,如果线程B在步骤一获取c的值是0,而线程B在步骤三存储-1到c是所有线程执行的最后一步,那么这个c值就是-1,线程A的结果丢失了。

解决这个问题的原则是让increment()这个操作不会被其它对变量c的操作交错,比如使用synchronized给方法加锁。

内存一致性错误

当多个线程对同一个对象数据有着不一致的视图会产生内存一致性错误,比如主存和线程栈中存储的副本不能保持同步刷新就会导致视图不一致。

我们在回到上面的示例上去,即使线程A所有步骤执行完了,此时线程B执行步骤一,也不一定会获取c的值是1,因为此时线程A中c的视图值为1,而线程B的视图值可能还是0,这就是不一致现象。

NOTE:共享变量被修改之后,什么时候被写入主存是不确定的,所以线程A执行完之后,线程B既有可能获得1,也有可能获得最初值0。

happens-before

为了避免这个问题,我们需要保证当共享变量被一个线程写之后,所有其它线程读这个变量都是可见的。Java内存模型为我们提供了一个happens-before机制,它确保了内存中的写对其它线程可见,如果一个操作happens-before其它操作,那么第一个操作对第二个操作是可见的,synchronized关键字已经创建了happens-before关系。

所以上面这个问题还是可以通过synchronized避免内存一致性问题,如果不考虑多线程干涉问题,或者increment()是一个原子操作,那么可以把变量c声明为volatile,这些将在下文中会介绍。能形成happens-before关系的除了 **synchronized,还有volatile、Thread.start()、Thread.join()**等,它们都是可见性的保证,下面这段happens-before描述摘自摘自《Java Language Specification》:

  • An unlock on a monitor happens-before every subsequent lock on that monitor.
  • A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.
  • A call to start() on a thread happens-before any actions in the started thread.
  • All actions in a thread happen-before any other thread successfully returns from a join() on that thread.

活跃度问题

尽管我们通过一些有效的方法避免了多线程干涉和内存一致性问题,但是仍然产生活跃度问题,如何避免或者解决活跃度问题会是一个高级话题,这里只对这些问题的概念和产生的场景进行介绍。

死锁(deadlock)

线程永久阻塞,互相等待会导致死锁。

如何快速写出一个死锁的例子?
答案是两个线程等待对方占有的资源,我们设想有两个线程,并且有两个锁,线程A获得了锁1,线程B获得了锁2,之后线程A需要去占有锁2,就会产生阻塞,此时,线程B需要去占有锁1,也会产生阻塞,这两个线程就会死锁,永远无法执行下去。

解决死锁的方式是尝试让一个线程释放锁,先让另一个线程执行下去,然后再执行这个线程,后续章节我们会学习一个显示锁Lock,它会尝试获取锁,如果失败,可以释放自己占有的锁,进而让其它线程能够获得这个被释放的锁,继续执行。

哲学家就餐问题是一个典型的死锁问题:五个哲学家拥有了一只筷子,他们都等待其它哲学家释放筷子,每个人都进入了永久等待状态,按照上面解决死锁的方式,我们可以限制最多只能有四个哲学家去拥有一只筷子,这样就能保证至少有一个哲学家可以用餐。

活锁(livelock)

活锁是两个线程之间互相谦让,导致无法继续执行下去。相对于死锁线程被阻塞无法继续执行,活锁是线程继续执行(不断的谦让),但是无法执行下去了。

饿死(starvation)

当一个锁长久被一个线程占有时,其它线程无法获得调度的机会,这些线程就会阻塞进入饿死状态。

原子操作和volatile关键字

原子操作(Atomic)是不会被干涉且不可分割的操作,所有操作要么一次做完,要么不做。所以 原子操作不会产生多线程干涉问题,但是会产生内存一致性问题。JDK提供了一个原子操作的包java.util.concurrent.atomic,提供了诸如AtomicInteger、AtomicBoolean等类,我们可以直接使用这个类的方法而无需关心多线程干涉问题,而这些类的实现也同时避免了可视性问题,所以我们可以直接使用而无需使用任何关键字修饰,关于原子类的原理,我们会在下篇文章讲到。

对一个volatile变量的写happens-before后续对这个变量的读,它可以解决内存一致问题,变量的写对所有线程可见,但是它可能还会产生多线程干涉问题,即volatile不能保证原子性,对volatile变量进行非原子操作是危险和错误的,synchronized是可以解决可见性和多线程干涉问题。

问题: 这个时候我们就要问了,原子类提供了原子操作,那么除了这些类,Java中哪些操作还会是原子操作呢?我们已经知道int变量c++不是一个原子操作,那么给int变量赋值是原子操作吗?或者说,给一个boolean类型的变量是原子操作吗?给一个引用类型的变量赋值是原子操作吗?我们来看看《The Java™ Tutorials》中的一段话:

However, there are actions you can specify that are atomic:

  • Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).
  • Reads and writes are atomic for all variables declared volatile (including long and double variables).

Java内存模型规定了对任何变量(不包括long和duoble,因为可能会分为高32位和低32位操作)的读和写都是原子的,对任何声明了volatile的读和写都是原子的,这里注意是读和写,像c++这种就不属于原子的,它可以被认为是一次读和一次写。

单例模式有一个性能更优的双重检查方式,我们先来看下这种写法的代码:没有把synchronized放到整个方法上,不希望锁的范围过大,所以没有获得锁的情况下有了第一重检查,当多线程执行后,获得锁后我们就需要第二重检查,它用到了volatile和synchronized,synchronized已经解决了可视性问题,为何还需要使用volatile关键字呢?

private volatile FieldType field;
private FieldType getField() {
    if (field == null) { // First check (no locking)
        synchronized (this) {
            if (field == null) // Second check (with locking)
                field = new FieldType();
        }
    }
    return field;
}

即使field被声明为volatile,由于不是读和写,所以它并不是原子的,虽然我们使用synchronized给这段代码加锁,但是第一重检查field的代码并没有在加锁的代码块内,如果不使用volatile,有可能会发生指令重排序,即field变量已经被赋值而field还没有初始化完毕的情形,volatile在这里的作用就是保证field = new FieldType()操作不会指令重排序!这里会引入一个新的问题,就是赋值操作是否是原子操作,通过上文知道变量的写是原子操作,例子中的赋值的确是写了,但是不仅仅是写,还有构造过程,所以它不是一个原子操作,如果是field=null这样的赋值,我会认为是简单的写原子操作,如同AtomicReference类的set方法。

综上,我们可以得出一个很重要的结论:volatile可以防止该变量的操作与其他内存操作一起重排序,保证变量的读和写是原子性的,但是不能保证其它操作是原子性,volatile最重要的功能是它的可见性它是一种弱一点的同步机制。最常见是用当作多线程共享的标志:

volatile boolean isShow; 

volatile int 和AtomicInteger

如果你能彻底理解volatile的作用,就能理解这两个的区AtomicInteger保证了可见性和所有方法原子性,volatile int保证了可见性,但是只在读写的时候是原子性,可以看作是AtomicInteger的get和set方法,而++操作就不是原子操作。

不可变对象final or immutable

final常量在初始化后就不可更改,所以无需关心多线程问题,记住不可变对象是值得信赖的。final在内存模型中的语义不止是常量这么简单,这里就不延伸了,这个话题会很庞大,我们给出一个疑惑的例子:当一个线程调用write方法,另一个线程调用reader方法,i的值会被确保是3,而j的值不一定为4。

class FinalFieldExample { 
    final int x;
    int y; 
    static FinalFieldExample f;

    public FinalFieldExample() {
        x = 3; 
        y = 4; 
    } 

    static void writer() {
        f = new FinalFieldExample();
    } 

    static void reader() {
        if (f != null) {
            int i = f.x;  // guaranteed to see 3  
            int j = f.y;  // could see 0
        } 
    } 
}

NOTE:关于不可变对象我们无需同步,但是不可变对象里面的可变部分我们要倍加小心,它们还是会产生多线程问题,比如一个final声明的集合对象,它的元素就是可变的。

ThreadLocal源码

避免多线程的问题就是谨慎处理共享变量的访问,ThreadLocal将共享变量T当每个线程的本地变量使用,简单来说,多线程共享了变量ThreadLocal,但是T相当于每个线程内部的本地变量。

那么这和直接在线程内部声明局部变量有什么不同呢? 首先多个线程需要同一个类型的变量,但是他们的值是各自使用,这时候我们就可以使用ThreadLocal供多线程访问,然后在一个线程内部可能在不同方法中访问这个局部变量,或者根本不在线程对象里面访问,这里就需要通过方法参数不断传递这个局部变量,如果使用ThreadLocal,在同一个线程调用的任何方法内部,都能获取到这个局部变量值。

如何设计ThreadLocal供多线程使用?一个普遍的想法是看作是Map<Thread, T>,这个变量存储了所有线程和他们局部变量的映射关系。这个做法是危险的,因为它存取了所有线程的局部变量,任何一个线程都能轻松通过ThreadLocal对象获取到其它线程的数据,更正确的做法应该是将局部变量存储在每个线程内部,它的实现原理是每个Thread线程内部都维护一个这样的map = Map<ThreadLocal, T>,当我们通过ThreadLocal.get()方法获取数据时,其实是通过Thread.currentThread().map.get(ThreadLocal)来获取本地变量值,我们先来看看Thread源码中这个Map变量:

// Thread.java
// ThreadLocal values pertaining to this thread.
ThreadLocal.ThreadLocalMap threadLocals = null;

接下来我们先深入一下ThreadLocalMap类,它是一个通过哈希表+开放寻址法实现的HashMap,哈希表的实现参见《Java Collections Framework(五)Map》

/**
 * ThreadLocalMap is a customized hash map suitable only for
 * maintaining thread local values. No operations are exported
 * outside of the ThreadLocal class. The class is package private to
 * allow declaration of fields in class Thread.  To help deal with
 * very large and long-lived usages, the hash table entries use
 * WeakReferences for keys. However, since reference queues are not
 * used, stale entries are guaranteed to be removed only when
 * the table starts running out of space.
 */
static class ThreadLocalMap {

  /**
   * The entries in this hash map extend WeakReference, using
   * its main ref field as the key (which is always a
   * ThreadLocal object).  Note that null keys (i.e. entry.get()
   * == null) mean that the key is no longer referenced, so the
   * entry can be expunged from table.  Such entries are referred to
   * as "stale entries" in the code that follows.
   */
  static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
      super(k);
      value = v;
    }
  }

  /**
   * The initial capacity -- MUST be a power of two.
   */
  private static final int INITIAL_CAPACITY = 16;
  private Entry[] table;

  ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
  }

  /**
   * Expunge all stale entries in the table.
   */
  private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
  }

  // 略
}

我们可以得到几个关于ThreadLocalMap的重要知识点:

  1. 它的每个Entry是一个<ThreadLocal k, Object v>映射,而它的键是一个WeakReference类型,所以当某个ThreadLocal变量没有强引用的时候会被JVM垃圾回收,这样这个Map里面就会产生陈旧(Stale)的Entry,其中key是null,value还是一个对象。
  2. Stale entries会被清理掉,具体看方法expungeStaleEntries(),那么清理时机是什么时候?我们重点看下这里的注释:since reference queues are not used, stale entries are guaranteed to be removed only when the table starts running out of space,意思是没有使用引用队列(参见WeakReference),所以这些key为null的entry会在表空间不足时清理掉,这句话可能不像翻译的这么理解,通过源码我们发现,get、set、remove方法都会清理这些Entry,如果这个ThreadLocal变量再也不会使用,线程长时间不会结束,那么这些Entry将永远不会释放,即使执行了GC,所以会导致内存泄漏问题,更重要的是 线程池里面的线程会复用,这些线程不会终止,每个线程的变量threadLocals将会永久保存所有使用过的ThreadLocal-Value映射,除非我们手动删除

IMPORTANT:当我们使用完ThreadLocal变量后,必须调用ThreadLocal.remove()方法,清除数据并将当前本地变量的值从线程的变量里面删除。

核心操作都在ThreadLocalMap类,最后我们来看下ThreadLocal类,它很简单,只是通过当前线程获取到threadLocals,然后获取Entry。

public class ThreadLocal<T> {
  protected T initialValue() {
    return null;
  }

  public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
      ThreadLocalMap.Entry e = map.getEntry(this);
      if (e != null) {
        @SuppressWarnings("unchecked")
        T result = (T) e.value;
        return result;
      }
    }
    return setInitialValue();
  }

  public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) map.set(this, value);
    else createMap(t, value);
  }

  public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) m.remove(this);
  }
  // 略
}

我们还要知道的是,我们可以给ThreadLocal设置初始值。

总结

很多多线程问题可能并不会轻易发现,但是当问题出现时,会带来迷惑,我们必须理解并发机制且谨慎并发程序来避免这些问题。

Charles抓包

  1. 打开Charles
  2. 菜单Proxy-Proxy Settings,设置Http Proxy,输入端口号,勾上enable transparent HTTP proxying
  3. 打开手机wifi,确保与PC连接同一个wifi,高级网络设置,手动设置HTTP代理。输入PC局域网IP和端口号。
  4. Charles会弹出是否允许Proxy,点击允许即可。

Hook机器人

最初看到这些机器人是在bearychat上,后来钉钉也做了类似的机器人,自己也写了点代码,聊当无聊至极之随便写写。GitHub代码地址

本文简单的实现了机器人简单的功能,通过webhook接受平台的事件,从而触发机器人执行SAP(storage、analysis、push)等功能。
文末讲了一种典型的应用场景:基于WebSocket的浏览器实时推送

从GitHub Hook说起

Webhooks允许您构建或设置GitHub应用程序,该应用程序订阅GitHub.com上的某些事件。 当其中一个事件被触发时,我们会发送一个HTTP POST负载到webhook配置的URL。 Webhook可用于更新外部问题跟踪器,触发CI构建,更新备份镜像,甚至部署到生产服务器。

关于每个平台的WebHook的设置,hook的事件类型,事件的数据结构,都可以在文档中找到。GitHub WebHook的文档地址https://developer.github.com/webhooks/

Hook机器人实现的就是这样的一个应用,聚合不同平台的事件,存储,分析,消息通知等。在GitHub项目的Setting中可以配置自己的hook url,如下图。
image

下文,重点介绍下SAP功能。

Storage

框架使用SpringMVC,因为每个平台的事件结构不同,所以存储采用了文档型数据库Mongo db的方案。一个典型的GitHub push 事件接受服务如下:

package com.deepoove.hooks.web.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.deepoove.hooks.ThreadPoolFactory;
import com.deepoove.hooks.core.GitHubHook;
import com.deepoove.hooks.web.body.github.GitHubPushEvent;

@RestController
@RequestMapping("/github")
public class GitHubHookController {

    @Autowired
    private GitHubHook gitHubHook;

    @RequestMapping("/={hookerId}")
    public void hook(@RequestHeader("X-GitHub-Event") String event, @PathVariable String hookerId,
            @RequestBody(required = false) GitHubPushEvent pushEvent) {
        ThreadPoolFactory.getThreadPool().execute(() -> {
            switch (event) {
            case "push":
                gitHubHook.hook(hookerId, pushEvent);
                break;
            }
        });
        return;
    }
}

在抽象模板类Hook.java中,实现了模板方法:

public void hook(String hookerId, Object event) {
    logger.info("pushEvent:" + JSON.toJSONString(event));
    if (null == event) throw new IllegalArgumentException("event cannot be null");
    //存储原始事件数据
    saveEvent(event);

    Stream stream = convert2Stream(event);
    stream.setHookerId(hookerId);

    //处理事件
    if (null != handler) handler.handHook(stream);
    
    //存储Stream
    saveStream(stream);
}

所有平台,默认要做的就是实现convert2Stream方法,转化统一的数据结构。GitHubHook.java继承于模板Hook,实现了convert2Stream方法。

protected Stream convert2Stream(Object event) {
    GitHubPushEvent pushEvent = (GitHubPushEvent) event;
    Stream stream = new Stream();
    StringBuffer sb = new StringBuffer();
    sb.append("pushed ").append(pushEvent.getCommits().size())
            .append(" commit(s) to ").append(pushEvent.getRef())
            .append(" at ").append(pushEvent.getRepository().getFullName());
    stream.setText(sb.toString());

    MDBuilder md = new MDBuilder();
    md.append("pushed ").append(pushEvent.getCommits().size())
            .append(" commit(s) to ").append(pushEvent.getRef())
            .append(" at ")
            .appendLink(pushEvent.getRepository().getFullName(),
                    pushEvent.getRepository().getHtmlUrl());
    stream.setMdText(md.toString());

    stream.setUserAvatar(pushEvent.getSender().getAvatarUrl());
    stream.setUserName(pushEvent.getPusher().getName());
    List<String> attachments = new ArrayList<>();
    List<String> mdAttachments = new ArrayList<>();
    for (Commits commit : pushEvent.getCommits()) {
        attachments.add(commit.getId() + " : " + commit.getMessage());
        mdAttachments.add(new MDBuilder()
                .appendLink(commit.getId(), commit.getUrl()).append(":")
                .append(commit.getMessage()).toString());
    }
    stream.setAttachments(attachments);
    stream.setMdAttachments(mdAttachments);
    stream.setCreated(LocalDateTime.now());
    return stream;
}

至此,我们针对不同平台,实现不同的Hook,可以完整的将事件数据记录在mongo中。

Analysis

待续。

Push

我们注意到,在Hook.java的模板方法中,有如下一段代码:

if (null != handler) handler.handHook(stream);

这段代码就是对事件的处理。默认实现了两个Handler:控制台日志打印和浏览器实时推送。
image

在每个平台的Hook实现类中,可以自定义Handler的顺序(采用了责任链的设计),以及定义哪些Handler来处理此平台的事件。

基于WebSocket的浏览器实时推送

浏览器实时推送采用了WebSocket方案。
前端使用了可重连的reconnecting-websocket

var webSocket = new ReconnectingWebSocket("ws://115.29.10.121:9080/hooks/socket", null, {
    bug: true,
    reconnectInterval: 1000
});
webSocket.onerror = function(event) {
  console.log(event);
};

webSocket.onopen = function(event) {
  console.log("onpen" + event);
};

webSocket.onmessage = function(event) {
  console.log("onMessage:" + event.data);
  addScreean($.parseJSON(event.data), true);
};
webSocket.onclose = function () {
  console.log("reconnect");
};

服务端需要引入jar包:

compile group: 'javax.websocket', name: 'javax.websocket-api', version: '1.1'

服务的端定义Server Endpoint。

package com.deepoove.hooks.socket;

import javax.websocket.CloseReason;
import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/socket")
public class WebSocketServer {
	
	public static SessionHandler sessionHandler = SessionHandler.getInstance();
	
	public WebSocketServer() {
		
		System.out.println("====init WebSocketServer===" + this);
	}

	@OnOpen
	public void onOpen(Session session, EndpointConfig config) {
		sessionHandler.addSession(session);
		System.out.println("WebSocket opened: " + session.getId());
	}

	@OnMessage
	public void handleMessage(String message, Session session) {
		System.out.println("receiver message:" + message);
	}

	@OnClose
	public void onClose(Session session, CloseReason closeReason) {
		sessionHandler.removeSession(session);

	}

	@OnError
	public void onError(Throwable error) {}

}

通过SessionHandler我们可以向每个session发送消息。

package com.deepoove.hooks.socket;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

import javax.websocket.Session;

public class SessionHandler {

	private final Set<Session> sessions = new HashSet<>();

	private static SessionHandler instance = new SessionHandler();

	public static SessionHandler getInstance() {
		return instance;
	}

	public void addSession(Session session) {
		sessions.add(session);
	}

	public void removeSession(Session session) {
		sessions.remove(session);
	}

	
	
	public void sendToAllConnectedSessions(String message) {
		for (Session session : sessions) {
			sendToSession(session, message);
		}
	}

	private void sendToSession(Session session, String message) {
		try {
			System.out.println("send to:" + session.getId() + "===" + message);
			session.getBasicRemote().sendText(message);
		} catch (IOException ex) {
			sessions.remove(session);
		}
	}

}

浏览器实时效果如下:
browser img

More 更多

如果接入的是外网的平台,那么Hook服务的请求合法性是必须要考虑的。
同样,除了浏览器实时显示,我们可以类似钉钉,推送到自己的移动APP上。

React Native 开发实践

搭建环境

  1. 安装包管理器Homebrew,通过brew安装包
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

2.安装node、watchman

brew install node
brew install watchman

3.可选:nrm切换npm源
4.安装react native cli

npm install -g react-native-cli

5.管理android studio
安装Android SDK Build-Tools 23.0.1,并且确保android6.0以下的组件被安装:

Google APIs
Android SDK Platform 23
Intel x86 Atom_64 System Image
Google APIs Intel x86 Atom_64 System Image

6.配置ANDROID_HOME环境变量

vi ~/.profile
export ANDROID_HOME=/**/sdk
source   ~/.profile

Hello world

  1. 初始化项目
react-native init AwesomeProject

2.运行、调试项目
前提:在允许调试的真机上运行项目,在Building and installing the app on the device (cd android && ./gradlew installDebug)...这一步时会去下载gradle,经常容易超时,我们修改项目目录下android/gradle/wrapper/gradle-wrapper.properties文件distributionUrl=gradle-3.1-bin.zip,并且将gradle包拷贝到该目录下,接下来就可以运行项目了

cd AwesomeProject
react-native run-android

至此,在真机上已经可以看到运行的app了,本地浏览器访问http://127.0.0.1:8081/可以看到服务已经启动,支持代码实时修改,通过reload JS应用变化。
若需要重新连接真机,进入adt查看已连接的真机adb devices,输入以下命令访问开发服务器。

adb reverse tcp:8081 tcp:8081

打包APK,发布应用

Collections(八)Collections工具类

Collections工具类方便集合的操作。

不可修改包装Unmodifiable Wrappers

public static <T> Collection <T> unmodifiableCollectionCollection <?extends T> c);
public static <T> Set <T> unmodifiableSetSet <?extends T> s);
public static <T> List <T> unmodifiableListList <?extends T> list);
public static <KV> Map <KV> unmodifiableMapMap <?extends K,?extends V> m);
public static <T> SortedSet <T> unmodifiableSortedSetSortedSet <?extends T> s);
public static <KV> SortedMap <KV> unmodifiableSortedMapSortedMap <K,?extends V> m);

当持有返回集合的引用时,程序是无法对集合进行修改尝试修改会抛出UnsupportedOperationException异常。

以unmodifiableCollection方法为例,它的实现原理返回一个新的内部类对象return new UnmodifiableCollection<>(c),而UnmodifiableCollection遵循了转换构造函数的设计,在改变结构的方法体中(add\remove\removeIf\addAll\removeAll\clear\retainAll等),抛出异常。

这里要注意的是,如果我们继续维护参c的集合引用,任何修改c的操作都将导致不可修改的集合内部元素的变更,那么这种不可变的集合也是可变的

同步包装Synch Wrappers

public static <T> Collection <T> synchronizedCollectionCollection <T> c);
public static <T> Set <T> synchronizedSetSet <T> s);
public static <T> List <T> synchronizedListList <T> list);
public static <KV> Map <KV> synchronizedMapMap <KV> m);
public static <T> SortedSet <T> synchronizedSortedSetSortedSet <T> s);
public static <KV> SortedMap <KV> synchronizedSortedMapSortedMap <KV> m);

通过前面的文章已经知道,并发包提供了一些并发性能优异的集合,如果不考虑性能,想快速的获得一个同步集合的话,可以使用这些方法。

实现原很简单,返回一个内部类对象SynchronizedCollection,在该类的所有方法中,通过关键词synchronized实现同步操作。

static class SynchronizedCollection<E> implements Collection<E>, Serializable {

    final Collection<E> c;  // Backing Collection
    final Object mutex;     // Object on which to synchronize

    SynchronizedCollection(Collection<E> c) {
        this.c = Objects.requireNonNull(c);
        mutex = this;
    }

    public int size() {
        synchronized (mutex) {return c.size();}
    }
    public boolean isEmpty() {
        synchronized (mutex) {return c.isEmpty();}
    }
    public boolean contains(Object o) {
        synchronized (mutex) {return c.contains(o);}
    }

    public Iterator<E> iterator() {
        return c.iterator(); // Must be manually synched by user!
    }
    // 略
}

注意到iterator方法并没有加synchronized关键词,注释也表明了当我们迭代集合时,必须自己手动同步,因为迭代不是一个原子操作,需要一步一步来完成。

Collection<Type> c = Collections.synchronizedCollectionmyCollection);
synchronizedc){
    forType ecFOOE);
}

动态类型安全包装Dynamically typesafe collection wrappers

提供了Collections.checkedXXX方法来确保是类型安全的集合,虽然泛型可以在编译期静态类型检查就确保类型安全,但是这些方法消除了没有用好泛型的顾虑。

空集合Empty collections

public static final <T> Set<T> emptySet();
public static final <T> List<T> emptyList();
public static final <K,V> Map<K,V> emptyMap();
public static <T> Iterator<T> emptyIterator();

当这个集合不需要提供任何元素时,可以使用这些方法。注意,它返回的同样是个内部类的对象,元素个数只能为0,不能修改这个集合。

单个元素集合Singleton collections

public static <T> Set<T> singleton(T o);
public static <T> List<T> singletonList(T o);
public static <K,V> Map<K,V> singletonMap(K key, V value);

当你需要 一个元素的不可变集合时,可以使用这些方法,尤其这些方法可以用在removeAll的参数集合上:集合中删除所有某个元素。

算法Algorithms

集合是一系列数据的集,通常会这这些数据集进行相关操作,我们称之为集合算法,其中算法操作绝大多数是操作在List上的,少部分操作在Collection上

sort:list集合的排序

public static <T extends Comparable<? super T>> void sort(List<T> list);
public static <T> void sort(List<T> list, Comparator<? super T> c);

排序的实现是基于java.util.Arrays.sort(T[], Comparator<? super T>)方法实现的,Arrays提供了一系列操作数组的方法,包括sort方法,对于任何可比较的原生类型,采用 双轴快速排序算法

public static void sort(int[] a) {
    DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}

对于任何对象类型的数组,采用 合并排序算法,时间复杂度为O(n*logn):

public static <T> void sort(T[] a, Comparator<? super T> c) {
    if (c == null) {
        sort(a);
    } else {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a, c);
        else
            TimSort.sort(a, 0, a.length, c, null, 0, 0);
    }
}

具体算法实现参见DualPivotQuicksort和TimSort类。

binarySearch:list集合中二分查找某个元素key

public static <T> int binarySearch(List<? extends Comparable<?super T>> list, T key);
public static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c);

注意: list必须是一个已经升序排序的列表。* 二分查找算法的核心代码如下:

<T> int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key) {
    int low = 0;
    int high = list.size()-1;

    while (low <= high) {
        int mid = (low + high) >>> 1;
        Comparable<? super T> midVal = list.get(mid);
        int cmp = midVal.compareTo(key);

        if (cmp < 0)
            low = mid + 1;
        else if (cmp > 0)
            high = mid - 1;
        else
            return mid; // key found
    }
    return -(low + 1);  // key not found
}

注意到(low + high) >>> 1这行代码,装逼程序员的深藏功与名。

shuffle:list洗牌

public static void shuffle(List<?> list);
public static void shuffle(List<?> list, Random rnd);

重新随机打算list的顺序,核心算法是当前位置与后面的位置随机交换值。

for (int i=size; i>1; i--)
    swap(list, i-1, rnd.nextInt(i));

frequency:元素在collection中的出现频率

public static int frequency(Collection<?> c, Object o) {
    int result = 0;
    if (o == null) {
        for (Object e : c)
            if (e == null)
                result++;
    } else {
        for (Object e : c)
            if (o.equals(e))
                result++;
    }
    return result;
}

disjoint:两个集不相交返回true

针对Collection的操作。

常规操作

  • reverse- 颠倒a中元素的顺序List。
  • fill- List用指定的值覆盖a中的每个元素。
  • copy- 接受两个参数,一个目标List和一个源List,并将源的元素复制到目标中,覆盖其内容。
  • swap- 将元素交换到a中的指定位置List。
  • min- 最小值
  • max- 最大值

总结

在我们为Collection提供操作的时候,同时可以考虑将Collection转化为数组,然后通过Arrays的静态方法来操作。

Google的Guava为集合提供了一系列扩展,除增加新的集合类型外,还提供了一个有用的工具类,如Lists、Maps等,将在下篇文章中详述。

并发(六)Collections

本章重点介绍线程安全的集合类,它们都在java.util.concurrent包下。

阻塞队列:BlockingQueue、BlockingDeque

BlockingQueue阻塞队列相对于普通队列来说,提供了额外的操作,:

  1. 在队列为空时,take()获取元素的时候会阻塞直到这个队列不为空
  2. 在队列满时,put()存储元素的时候会阻塞直到这个队列有剩余的空间
    关于队列,参见以前的一篇文章《Collections(四)Queue》

BlockingQueue阻塞队列常用于生产者-消费者模型,比如Google的Volley框架,使用阻塞队列缓存网络请求,使用若干线程从阻塞队列获取网络请求执行,又比如线程池,如果超过了coreSize,就会通过阻塞队列对任务进行排队,工作线程或者新启动线程从阻塞队列取出任务执行。下面的代码是JDK提供的一个示例,生产者进行put操作,消费者执行take操作:

class Producer implements Runnable {
   private final BlockingQueue queue;
   Producer(BlockingQueue q) { queue = q; }
   public void run() {
     try {
       while (true) { queue.put(produce()); }
     } catch (InterruptedException ex) { ... handle ...}
   }
   Object produce() { ... }
 }

class Consumer implements Runnable {
   private final BlockingQueue queue;
   Consumer(BlockingQueue q) { queue = q; }
   public void run() {
     try {
       while (true) { consume(queue.take()); }
     } catch (InterruptedException ex) { ... handle ...}
   }
   void consume(Object x) { ... }
}

class Setup {
   void main() {
     BlockingQueue q = new SomeQueueImplementation();
     Producer p = new Producer(q);
     Consumer c1 = new Consumer(q);
     Consumer c2 = new Consumer(q);
     new Thread(p).start();
     new Thread(c1).start();
     new Thread(c2).start();
   }
}

接口BlockingDeque继承了接口BlockingQueue,是一个阻塞双端队列。

ArrayBlockingQueue

数组实现的一个有界阻塞队列。它提供的构造函数中,必须指定队列的大小,其中fair表示公平策略:

public ArrayBlockingQueue(int capacity) {
  this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
  if (capacity <= 0)
    throw new IllegalArgumentException();
  this.items = new Object[capacity];
  lock = new ReentrantLock(fair);
  notEmpty = lock.newCondition();
  notFull =  lock.newCondition();
}

在《锁》的章节中讨论过使用Lock和Condition实现生产者和消费者模型,通过notEmpty发送队列已经不为空的信号,通过notFull发送队列还未满的信号,我们来看看ArrayBlockingQueue中的take一个元素的核心源码:

public E take() throws InterruptedException {
  final ReentrantLock lock = this.lock;
  lock.lockInterruptibly();
  try {
    while (count == 0)
      notEmpty.await();
    return dequeue();
  } finally {
    lock.unlock();
  }
}
private E dequeue() {
  // assert lock.getHoldCount() == 1;
  // assert items[takeIndex] != null;
  final Object[] items = this.items;
  @SuppressWarnings("unchecked")
  E x = (E) items[takeIndex];
  items[takeIndex] = null;
  if (++takeIndex == items.length)
    takeIndex = 0;
  count--;
  if (itrs != null)
    itrs.elementDequeued();
  notFull.signal();
  return x;
}

通过当队列为空时,通过notEmpty.await()阻塞当前线程,每次dequeue出队一个元素,都会发送队列未满的信号:notFull.signal();,注意到await必须使用while守护循环代码块。

我们再来看看put入队的源码:

public void put(E e) throws InterruptedException {
  checkNotNull(e);
  final ReentrantLock lock = this.lock;
  lock.lockInterruptibly();
  try {
    while (count == items.length)
      notFull.await();
    enqueue(e);
  } finally {
    lock.unlock();
  }
}
private void enqueue(E x) {
  // assert lock.getHoldCount() == 1;
  // assert items[putIndex] == null;
  final Object[] items = this.items;
  items[putIndex] = x;
  if (++putIndex == items.length)
    putIndex = 0;
  count++;
  notEmpty.signal();
}

当队列满时,通过while循坏守护notFull.await();方法阻塞当前线程,每次入队都会发送队列不为空的信号。

LinkedBlockingQueue

基于链表实现的一个可选有界阻塞队列,队列大小可以通过构造函数指定,默认大小为Integer.MAX_VALUE:

public LinkedBlockingQueue() {
  this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
  if (capacity <= 0) throw new IllegalArgumentException();
  this.capacity = capacity;
  last = head = new Node<E>(null);
}

Note:和ArrayBlockingQueue只使用一个锁实现take和put不一样,LinkedBlockingQueue使用了双锁,提高了吞吐量,编写代码实现双锁还是需要谨慎的。

/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

因为使用双锁,在多线程干涉情况下,队列元素个数的统计可能不正确,所以采用了原子类型存储个数:

/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();

接下来我们就可以看看take()的源码了:

public E take() throws InterruptedException {
  E x;
  int c = -1;
  final AtomicInteger count = this.count;
  final ReentrantLock takeLock = this.takeLock;
  takeLock.lockInterruptibly();
  try {
    while (count.get() == 0) {
      notEmpty.await();
    }
    x = dequeue();
    c = count.getAndDecrement();
    if (c > 1)
      notEmpty.signal();
  } finally {
    takeLock.unlock();
  }
  if (c == capacity)
    signalNotFull();
  return x;
}
private void signalNotFull() {
  final ReentrantLock putLock = this.putLock;
  putLock.lock();
  try {
    notFull.signal();
  } finally {
    putLock.unlock();
  }
}

这段代码和ArrayBlockingQueue的实现也有不一样的地方:

if (c > 1)
  notEmpty.signal();

这段代码的意思是如果在出队前的元素超过1个,那么出队后还是有剩余元素的,则唤醒下一个等待take的线程,所以这里唤醒下一个线程获取元素没有等待通过put方法调用notEmpty.signal();,而是提前判断来唤醒。

put()方法源码:

public void put(E e) throws InterruptedException {
  if (e == null) throw new NullPointerException();
  // Note: convention in all put/take/etc is to preset local var
  // holding count negative to indicate failure unless set.
  int c = -1;
  Node<E> node = new Node<E>(e);
  final ReentrantLock putLock = this.putLock;
  final AtomicInteger count = this.count;
  putLock.lockInterruptibly();
  try {
    /*
     * Note that count is used in wait guard even though it is
     * not protected by lock. This works because count can
     * only decrease at this point (all other puts are shut
     * out by lock), and we (or some other waiting put) are
     * signalled if it ever changes from capacity. Similarly
     * for all other uses of count in other wait guards.
     */
    while (count.get() == capacity) {
      notFull.await();
    }
    enqueue(node);
    c = count.getAndIncrement();
    if (c + 1 < capacity)
      notFull.signal();
  } finally {
    putLock.unlock();
  }
  if (c == 0)
    signalNotEmpty();
}

LinkedBlockingQueue采用双锁和链表实现,在性能上有所提高,它应该是阻塞队列的首选。

PriorityBlockingQueue

基于堆实现的无界阻塞队列,和PriorityQueue实现机制类似。因为是无界的,所以当take元素的时候无需发送notFull信号,具体参考源码。

Volley框架中采用PriorityBlockingQueue来存储网络请求,高优先级的网络请求会被优先处理。

SynchronousQueue

这是一个不会存储任何一个元素的队列,它的容量一直为0,也没有任何变量保存元素。它作为一个传递作用:将元素从put线程传递到take线程。

  • take()获取元素的时候会阻塞,直到调用了put()方法
  • put()元素的时候会阻塞,直到调用了take()方法

我们来看看它的核心代码:

public E take() throws InterruptedException {
  E e = transferer.transfer(null, false, 0);
  if (e != null)
    return e;
  Thread.interrupted();
  throw new InterruptedException();
}
public void put(E e) throws InterruptedException {
  if (e == null) throw new NullPointerException();
  if (transferer.transfer(e, false, 0) == null) {
    Thread.interrupted();
    throw new InterruptedException();
  }
}

这里都使用一个关键对象Transferer,它是整个传递的核心。

/**
 * Shared internal API for dual stacks and queues.
 */
abstract static class Transferer<E> {
    /**
     * Performs a put or take.
     *
     * @param e if non-null, the item to be handed to a consumer;
     *          if null, requests that transfer return an item
     *          offered by producer.
     * @param timed if this operation should timeout
     * @param nanos the timeout, in nanoseconds
     * @return if non-null, the item provided or received; if null,
     *         the operation failed due to timeout or interrupt --
     *         the caller can distinguish which of these occurred
     *         by checking Thread.interrupted.
     */
    abstract E transfer(E e, boolean timed, long nanos);
}
public SynchronousQueue(boolean fair) {
    transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}

SynchronousQueue内部提供了TransferQueue来实现公平模式,TransferStack实现非公平模式,具体源码就不拓展了,使用了Dual Queue和Dual Stack算法。

DelayQueue

DelayQueue是根据延迟时间排列元素的优先级阻塞队列,内部通过PriorityQueue实现优先级队列。当获取元素时,如果队首元素的延迟时间未到,则会阻塞直到队首元素延迟时间过期。DelayQueue通常用在需要过延迟一段时间才操作的场景,将这些操作放进延迟队列,然后线程从延迟队列取元素执行操作。

延迟队列的元素必须是延迟的且可比较,实现java.util.concurrent.Delayed接口。

/**
 * A mix-in style interface for marking objects that should be
 * acted upon after a given delay.
 *
 * <p>An implementation of this interface must define a
 * {@code compareTo} method that provides an ordering consistent with
 * its {@code getDelay} method.
 *
 * @since 1.5
 * @author Doug Lea
 */
public interface Delayed extends Comparable<Delayed> {

  /**
   * Returns the remaining delay associated with this object, in the
   * given time unit.
   *
   * @param unit the time unit
   * @return the remaining delay; zero or negative values indicate
   * that the delay has already elapsed
   */
  long getDelay(TimeUnit unit);
}

如何实现Delayed接口并不是非常简单的事情,compareTo方法需要和getDelay方法相结合使用,这样才能确保延迟最短的元素优先级最高,最先被线程取出来执行。我们来看看ScheduledThreadPoolExecutor.ScheduledFutureTask的源码,它表示一个计划的任务,实现了Delay接口:

private class ScheduledFutureTask<V>
    extends FutureTask<V> implements RunnableScheduledFuture<V> {
  /** Sequence number to break ties FIFO */
  private final long sequenceNumber;

  /** The time the task is enabled to execute in nanoTime units */
  private long time;

   ScheduledFutureTask(Runnable r, V result, long ns) {
    super(r, result);
    this.time = ns;
    this.sequenceNumber = sequencer.getAndIncrement();
  }
  public long getDelay(TimeUnit unit) {
    return unit.convert(time - now(), NANOSECONDS);
  }

  public int compareTo(Delayed other) {
    if (other == this) // compare zero if same object
      return 0;
    if (other instanceof ScheduledFutureTask) {
      ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
      long diff = time - x.time;
      if (diff < 0)
        return -1;
      else if (diff > 0)
        return 1;
      else if (sequenceNumber < x.sequenceNumber)
        return -1;
      else
        return 1;
    }
    long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
    return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
  }
  // 略
}

这里是一个典型Delay接口的实现方式,初始化的时候传入延迟过期的时间点,getDelay方法将这个时间点与当前时间作比较计算出剩余延迟时间,compareTo方法通过比较延迟时间点或者通过getDelay方法比较来确保延迟最短的任务排在队列前面。sequenceNumber字段是用来当延迟时间一致时,按照先来后到的序列号顺序排队。

知道了如何实现Delay接口,我们就可以深入DelayQueue的源码:

public E take() throws InterruptedException {
  final ReentrantLock lock = this.lock;
  lock.lockInterruptibly();
  try {
    for (;;) {
      E first = q.peek();
      if (first == null)
        available.await();
      else {
        long delay = first.getDelay(NANOSECONDS);
        if (delay <= 0)
          return q.poll();
        first = null; // don't retain ref while waiting
        if (leader != null)
          available.await();
        else {
          Thread thisThread = Thread.currentThread();
          leader = thisThread;
          try {
            available.awaitNanos(delay);
          } finally {
            if (leader == thisThread)
              leader = null;
          }
        }
      }
    }
  } finally {
    if (leader == null && q.peek() != null)
      available.signal();
    lock.unlock();
  }
}

细节不再赘述,我们可以了解到几下几点:

  • 当队首元素还未过期时,线程会被阻塞,通过available.awaitNanos(delay);方法阻塞响应延迟时间,延迟时间到了,(多个阻塞线程其中一个)则会取出队首元素
  • 如果队列的元素已经过期了还没有线程来取,当下一个线程来取的时候,直接会取出队首已经过期的元素
long delay = first.getDelay(NANOSECONDS);
    if (delay <= 0)
      return q.poll();

NOTE:DelayQueue还可以用来设计缓存的有效期,缓存时间就是延迟时间。

LinkedTransferQueue

传输作用的阻塞队列,这里不赘述。

LinkedBlockingDeque

基于链表实现的可选有界双端阻塞队列,实现机制很简单,具体参考源码。

同步集合

Java集合框架还提供一些集合的并发实现,它们永远不会抛出ConcurrentModificationException异常,与Collections.synchronizedXXX方法简单的使用synchronized同步机制不同,ConcurrentXXX是并发的,允许多个线程同时操作,CAS在这些并发实现中起到了非常重要的作用。

有一些并发实现是基于跳表SkipList实现的,是树的一个替代实现。

有一些并发实现是基于CopyOnWrite实现的,当需要写时,会拷贝一个新数组进行写,一切操作结束后,会将新数组赋值给旧数组。我们来看看CopyOnWriteArrayList.add(E)的实现:

public boolean add(E e) {
  final ReentrantLock lock = this.lock;
  lock.lock();
  try {
    Object[] elements = getArray();
    int len = elements.length;
    Object[] newElements = Arrays.copyOf(elements, len + 1);
    newElements[len] = e;
    setArray(newElements);
    return true;
  } finally {
    lock.unlock();
  }
}

Arrays.copyOf实现了新数组的复制,setArray实现了操作结束赋值的操作。

  • ConcurrentLinkedDeque
    LinkedDeque的并发实现。

  • ConcurrentLinkedQueue
    LinkedQueue的并发实现。

  • ConcurrentHashMap
    HashMap的并发实现

  • ConcurrentSkipListMap
    TreeMap的并发实现,基于跳表算法。

  • ConcurrentSkipListSet
    TreeSet的并发实现。

  • CopyOnWriteArrayList
    写时复制的ArrayList并发实现。

  • CopyOnWriteArraySet
    写时复制的Set集合。

总结

对于一些无界阻塞队列的使用,我们要倍加小心,因为可能会导致内存不足。

并发(一)基础之线程篇

在并发编程中,我们会遇到一些问题:线程数到底多少合适?如何才能编写出并发访问安全的程序?如果管理这些线程?......理解这些问题有益于我们写出优秀的并发代码,本系列文章将一步步剖析Java并发编程的细节。

本文重点介绍并发基础单元线程,它是进程的一部分。

基础知识:Thread

我们知道,有两个方式去创建线程:

1.通过继承Thread类
Thread实现了Runnable接口,但是run方法什么都没有做,继承Thread类,重写run方法

public class HelloThread extends Thread {
  public void run() {
    System.out.println("Hello from a thread!");
  }
}

接着我们可以通过start方法来启动线程:(new HelloThread()).start();

2.实现Runnable接口
Thread类提供了一个Runnable参数的构造器,我们可以通过Runnable对象创建线程。

public class HelloRunnable implements Runnable {

  public void run() {
    System.out.println("Hello from a thread!");
  }

  public static void main(String args[]) {
    (new Thread(new HelloRunnable())).start();
  }

}

注意:我们更倾向于实现Runnable接口,因为这种方法将线程执行的任务和线程本身分开,并且Runnable对象无需继承Thread类,扩展性更好。

Thread初始化源码

通过源码我们来看看创建一个线程到底初始化了哪些信息,下面的代码是一个完整参数的构造器:

public Thread(ThreadGroup group, Runnable target, String name, long stackSize) {
  init(group, target, name, stackSize);
}

在详细进入init方法前,我们先来仔细研究下这里的参数的含义:

1. ThreadGroup

指定了属于哪个线程组,如果没有指定,默认归属于当前线程的线程组。线程组的作用是维护一组线程,也可以拥有线程组,每个线程组都有一个父线程组,根的父线程组是NULL。

我们来看看线程组可以对一组线程做哪些事情:

  • setMaxPriority可以设置这组线程的最大优先级,原有线程优先级不受影响
  • activeCount获取活跃的线程数
  • enumerate复制所有线程
  • ThreadGroup实现了接口Thread.UncaughtExceptionHandler,这个接口定义了当这组线程运行时出现异常的捕获处理,源码如下:
public void uncaughtException(Thread t, Throwable e) {
  if (parent != null) {
    parent.uncaughtException(t, e);
  } else {
    Thread.UncaughtExceptionHandler ueh =
      Thread.getDefaultUncaughtExceptionHandler();
    if (ueh != null) {
      ueh.uncaughtException(t, e);
    } else if (!(e instanceof ThreadDeath)) {
      System.err.print("Exception in thread \""
               + t.getName() + "\" ");
      e.printStackTrace(System.err);
    }
  }
}

这就是为什么我们线程出现未捕获的异常时,控制台会打印类似这样的字符:Exception in thread "Thread-0" java.lang.RuntimeException: run time thread ex msg

ThreadGroup被认为是一个失败的尝试(《Java编程**》),我们大可不必了解这个类。Thread类也提供了设置异常处理器的方法public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh),可以使用这个方法单独制定某个线程异常处理。

2. target表示要执行的任务

3. name是一个线程的命名

可以重复,如果为空,会默认生成一个名称,生成规则是"Thread-" + nextThreadNum()

private static int threadInitNumber;
private static synchronized int nextThreadNum() {
  return threadInitNumber++;
}

4. stackSize表示线程占用的栈大小

每个线程将会维护一个栈空间存储变量,参数等信息,对于多个线程的共享变量,它们也会在栈内维护一个副本(这里会引入内存一致性问题,会在下一篇文章中详细讨论),stackSize为0将会忽略这个参数,使用默认值,默认值是通过JVM参数-Xss设置的。

-Xss
-Xss sets the thread stack size. Thread stacks are memory areas allocated for each Java thread for their internal use. This is where the thread stores its local execution state.

这个默认值是平台相关的,下面的命令可以看出来,在我的机器上,默认线程占用的栈大小为1M(JVM Options)。

$ java -XX:+PrintFlagsInitial | grep ThreadStackSize
 intx CompilerThreadStackSize                   = 0                                   {pd product}
 intx ThreadStackSize                           = 1024                                {pd product}
 intx VMThreadStackSize                         = 1024                                {pd product}

接下来我们再深入初始化的init方法看看如何构造Thread对象:

private void init(ThreadGroup g, Runnable target, String name,
          long stackSize, AccessControlContext acc,
          boolean inheritThreadLocals) {
  if (name == null) {
    throw new NullPointerException("name cannot be null");
  }

  this.name = name;

  Thread parent = currentThread();
  SecurityManager security = System.getSecurityManager();
  if (g == null) {
    /* Determine if it's an applet or not */

    /* If there is a security manager, ask the security manager
       what to do. */
    if (security != null) {
      g = security.getThreadGroup();
    }

    /* If the security doesn't have a strong opinion of the matter
       use the parent thread group. */
    if (g == null) {
      g = parent.getThreadGroup();
    }
  }

  /* checkAccess regardless of whether or not threadgroup is
     explicitly passed in. */
  g.checkAccess();

  /*
   * Do we have the required permissions?
   */
  if (security != null) {
    if (isCCLOverridden(getClass())) {
      security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
    }
  }

  g.addUnstarted();

  this.group = g;
  this.daemon = parent.isDaemon();
  this.priority = parent.getPriority();
  if (security == null || isCCLOverridden(parent.getClass()))
    this.contextClassLoader = parent.getContextClassLoader();
  else
    this.contextClassLoader = parent.contextClassLoader;
  this.inheritedAccessControlContext =
      acc != null ? acc : AccessController.getContext();
  this.target = target;
  setPriority(priority);
  if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
      ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  /* Stash the specified stack size in case the VM cares */
  this.stackSize = stackSize;

  /* Set thread ID */
  tid = nextThreadID();
}

这样一个线程就初始化好了它归属于一个线程组,并且拥有自己的名称,这段代码主要做了下面这些事情:

  1. 初始化了线程组
  2. 设置是否是守护线程daemon,守线程是一种后台线程,关于守护线程和用户线程,这里不作扩展介绍。
  3. 设置了线程的优先级priority,这和线程调度的先后有关系,Thread类提供了三个常量MIN_PRIORITY=1、NORM_PRIORITY=5和MAX_PRIORITY=10。
  4. 初始化了任务target
  5. 初始化了stackSize
  6. 生成线程tid

我们注意到,此时只是初始化了线程组和线程,并没有将线程加入线程组,g.addUnstarted()方法也仅仅是将线程组里面为启动线程的计数累加,真正将线程加入线程组的代码是在start启动方法中。

Thread启动源码

启动一个线程是通过start()方法,而不是run()方法,当调用start方法后,run方法是由JVM去调用的。

public synchronized void start() {
  /**
   * This method is not invoked for the main method thread or "system"
   * group threads created/set up by the VM. Any new functionality added
   * to this method in the future may have to also be added to the VM.
   *
   * A zero status value corresponds to state "NEW".
   */
  if (threadStatus != 0)
    throw new IllegalThreadStateException();

  /* Notify the group that this thread is about to be started
   * so that it can be added to the group's list of threads
   * and the group's unstarted count can be decremented. */
  group.add(this);

  boolean started = false;
  try {
    start0();
    started = true;
  } finally {
    try {
      if (!started) {
        group.threadStartFailed(this);
      }
    } catch (Throwable ignore) {
      /* do nothing. If start0 threw a Throwable then
        it will be passed up the call stack */
    }
  }
}

private native void start0();

start方法修改了是否启动标志,然后调用native方法启动执行,接下来线程的执行将会由CPU进行调度了。

注意到其中group.add(this)方法正是将当前线程加入了线程组。

基础知识:synchronized

synchronized是Java提供并发同步的基本关键字,它有两种形式:synchronized方法和synchronized语句。

// 序号1
public synchronized void increment() {
  c++;
}

// 序号2
public static synchronized void increment() {
  c++;
}

// 序号3
private Object lock1 = new Object();
public void inc1() {
  synchronized(lock1) {
      c1++;
  }
}

// 序号4
public void addName(String name) {
  synchronized(this) {
    lastName = name;
    nameCount++;
  }
  nameList.add(name);
}

// 序号5
public void add() {
  synchronized(User.class) {
    nameCount++;
  }
}

我们都知道synchronized保证了代码块当且只有一个线程可以进入,其余若干线程将会阻塞,即这个线程拥有了锁,当这个线程退出代码块后,调度器将会调度阻塞的某一个线程拥有锁。

但是我们还必须知道synchronized到底锁住了什么,才能知道哪些线程会被阻塞,才能更合理的使用这个关键字。

Java中每一个对象都关联一个内在锁(intrinsic lock)或者监视器(monitor),线程可以锁住这个内在锁,也可以释放这个内在锁,在某一刻,当且只能有一个线程拥有这个锁,然而同一个线程可以拥有这个锁多次,称之为重入锁。

我们可以查看同步块对应的字节码,可以看到monitorenter和monitorexit命令,代表拥有锁和释放锁,重入的实现机制是对计数器的累加和减少,当计数为0时,表示当前线程释放了锁。

我们理解了内在锁后,再看看上面的示例代码:

  • 序号1-synchronized方法
    对象锁,拥有了某个实例对象的内在锁,其它访问同一个对象的这个方法将会阻塞,不是同一个对象和这个锁没有任何关系。
  • 序号2-synchronized static 方法
    这是一个静态方法,那么内在锁关联的什么对象呢?很多人理解为类锁,我觉得更应该叫 类对象锁。它关联的其实是一个表示当前类的Class对象,即如序号5所示是User.class的内在锁,所以它和序号5会在同一个内在锁上竞争。
  • 序号3-synchronized语句
    对象锁,拥有了lock1这个对象的内在锁。
  • 序号4-synchronized语句
    对象锁,拥有了当前对象的内在锁,和序号1竞争的是当前对象的内在锁。
  • 序号5-synchronized语句
    类对象锁,参见序号2。

当前线程是否拥有某个对象的锁,可以通过Thread提供的静态方法判断:

public static native boolean holdsLock(Object obj);

线程状态转化

从启动一个线程后,线程会被阻塞,也会被重新调度执行,最终会被销毁,这些称之为状态转化。
image

Thread也提供了获取状态的方法public State getState(),我们可以仔细阅读下State的源码注释,对应上图我们把BLOCKED、WAITING、TIME_WAITING统称为阻塞状态:

public enum State {
  /**
   * Thread state for a thread which has not yet started.
   */
  NEW,

  /**
   * Thread state for a runnable thread.  A thread in the runnable
   * state is executing in the Java virtual machine but it may
   * be waiting for other resources from the operating system
   * such as processor.
   */
  RUNNABLE,

  /**
   * Thread state for a thread blocked waiting for a monitor lock.
   * A thread in the blocked state is waiting for a monitor lock
   * to enter a synchronized block/method or
   * reenter a synchronized block/method after calling
   * {@link Object#wait() Object.wait}.
   */
  BLOCKED,

  /**
   * Thread state for a waiting thread.
   * A thread is in the waiting state due to calling one of the
   * following methods:
   * <ul>
   *   <li>{@link Object#wait() Object.wait} with no timeout</li>
   *   <li>{@link #join() Thread.join} with no timeout</li>
   *   <li>{@link LockSupport#park() LockSupport.park}</li>
   * </ul>
   *
   * <p>A thread in the waiting state is waiting for another thread to
   * perform a particular action.
   *
   * For example, a thread that has called <tt>Object.wait()</tt>
   * on an object is waiting for another thread to call
   * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
   * that object. A thread that has called <tt>Thread.join()</tt>
   * is waiting for a specified thread to terminate.
   */
  WAITING,

  /**
   * Thread state for a waiting thread with a specified waiting time.
   * A thread is in the timed waiting state due to calling one of
   * the following methods with a specified positive waiting time:
   * <ul>
   *   <li>{@link #sleep Thread.sleep}</li>
   *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
   *   <li>{@link #join(long) Thread.join} with timeout</li>
   *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
   *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
   * </ul>
   */
  TIMED_WAITING,

  /**
   * Thread state for a terminated thread.
   * The thread has completed execution.
   */
  TERMINATED;
}

obj.wait()、obj.notify()、obj.notifyAll()

wait方法是Object提供的一个final方法,目的是使当前线程进入等待阻塞状态,直到调用这个对象obj的notify或者notifyAll方法,还提供了一个时间参数的重载方法,表示等待阻塞一定时间。

obj.wait方法是让拥有当前对象内在锁的线程进入一个等待集合,然后释放这个对象的内在锁,进入等待状态。 所以当前线程必须拥有这个对象obj的内在锁,才能调用obj.wait方法,因为每个对象都关联一个内在锁,这也是为什么wait方法是作为Object类的一个方法。

obj.notify、obj.notifyAll方法是唤醒这些等待集合中的一个或者全部,这两个方法也 必须在拥有对象内在锁的线程中调用,调用notify后并不会里面唤醒等待阻塞的线程,而是等待释放了拥有的对象内在锁后,再由调度器进行调度。

我们可以假设有10个线程,第一个线程拥有了某个对象内在锁,然后调用此对象的wait方法进入等待阻塞状态,一直到第10个线程,那么这10个线程都会进入等待集合,此时如果第11个线程拥有了这个对象的内在锁,然后调用notifyAll方法,这10个线程都将会被唤醒,重新进行调度,它们之间 仍然会对这个对象的内在锁进行竞争,同一时刻只能有一个线程会执行。

打断等待阻塞状态除了notify、notifyAll方法,还有

  • wait(timeout)的时间到了
  • thread.interrupt()打断
  • 虚假唤醒spurious wakeup

虽然虚假唤醒很少会发生,但是为了避免此种情况,最佳实践是通过线程的一个局部变量标识是否应该等待,然后通过守护循环代码块来执行。

private boolean empty = true;
public synchronized void take() {
  // Wait until empty is false
  while (empty) {
    try {
      wait();
    } catch (InterruptedException e) {}
  }
   
}

Always invoke wait inside a loop that tests for the condition being waited for. Don't assume that the interrupt was for the particular condition you were waiting for, or that the condition is still true. 《The Java™ Tutorials》

t.interrupt()、t.isInterrupted()、Thread.interrupted()

我们试想一下如何中断线程执行,可能在run方法的循环里面,不断判断一个标记,当这个标记符合某个条件时,就停止循环。Thread类自带了这样的标记,我们可以通过t.interrupt()方法(即设置了中断标记interrupt flag)中断线程,通过Thread.interrupted()这个静态方法检查当前线程的中断标记,返回true或者false,调用这个静态方法后,会重置中断标记,即第二次调用总是会返回false,而t.isInterrupted()仅返回当前中断标记,不会重置。

之所以重置中断标记,应该是希望一个中断请求只希望通知一次,不希望代码中响应被通知两次。如果编写的代码对检查中断只会有一次响应操作,那么使用t.isInterrupted()不去重置中断标记也是可以的。

为了让线程能够中断,我们需要 在线程中自己编写代码来实现中断逻辑,类似这样:

for (int i = 0; i < inputs.length; i++) {
    heavyCrunch(inputs[i]);
    if (Thread.interrupted()) {
        // We've been interrupted: no more crunching.
        return;
    }
}

除了友好的通过Thread.interrupted()检查中断外,wait、sleep、join方法会直接抛出InterruptedException异常, 当异常抛出时,中断标记会被重置为false,我们可以在异常处理中实现中断逻辑:

for (int i = 0; i < importantInfo.length; i++) {
    // Pause for 4 seconds
    try {
        Thread.sleep(4000);
    } catch (InterruptedException e) {
        // We've been interrupted: no more messages.
        return;
    }
    // Print a message
    System.out.println(importantInfo[i]);
}

注意的是,上文提过,如果在wait方法执行的时候,不希望被中断,需要通过守护循环代码块来实现。

t.join

join是当前线程等待阻塞,直到另一个线程t结束,或者超过了join(millis)的指定时间,join也是可以被t.interrupt()中断,抛出InterruptedException异常,如果我们不希望被中断,可以通过守护循环代码实现,其中t.isAlive方法来判断线程是否存活:

t.start();
while (t.isAlive()){
    try{
        t.join();
    } catch (InterruptedException e) {
    }
}

接下来我们通过源码来看看join的原理。

public final synchronized void join(long millis)
throws InterruptedException {
  long base = System.currentTimeMillis();
  long now = 0;

  if (millis < 0) {
    throw new IllegalArgumentException("timeout value is negative");
  }

  if (millis == 0) {
    while (isAlive()) {
      wait(0);
    }
  } else {
    while (isAlive()) {
      long delay = millis - now;
      if (delay <= 0) {
        break;
      }
      wait(delay);
      now = System.currentTimeMillis() - base;
    }
  }
}

在主线程执行中,调用t.join方法,由于这个方法是synchronized修饰的,所以获得了线程t的内在锁,然后相当于调用了t.wait()方法进入等待阻塞状态,有个重要的知识点:当一个线程结束时,总会调用notifyAll()方法,所以当线程t运行结束时,主线程会被唤醒继续执行。这里比较容易迷惑的原因是主线程调用了其它线程Thread对象的wait方法,对Thread对象加锁了。

既然线程结束会调用notifyAll方法,说明拥有了此线程的内在锁,所以如果t.start()方法和t.join方法不在一个线程内执行呢?有没有可能在进入join方法时,因为关键字synchronized获取t的内在锁失败而阻塞?这个问题留作一个思考点,应该和JVM执行线程有关。

Thread.sleep

当前线程睡眠一段时间,睡眠过程中,调度器可能会调度其它线程执行,注意:当前线程不会失去任何锁,睡眠可以被t.interrupt()打断。

推荐使用TimeUnit.sleep方法,而不是使用Thread.sleep,TimeUnit枚举类提供了很多的便捷性,其中还包括时间单位的转换,同时它还提供了等待时间参数的方法的包装,比如timedJoin(Thread thread, long timeout)和timedWait(Object obj, long timeout),我们来看看这个枚举常量有哪些:

NANOSECONDS // 纳秒
MICROSECONDS // 微秒
MILLISECONDS // 毫秒
SECONDS // 秒
MINUTES // 分
HOURS // 时
DAYS // 天

JDK设计中,当需要时间作为参数的时候,习惯使用TimeUnit用作单位,比如参数为:(long time, TimeUnit unit)。当我们自己编写代码遇到时间参数时,可以利用TimeUnit来设计友好的代码。

Thread.yield

暗示调度器当前线程希望让步对处理器的占用,调度器可以忽略这个暗示,yield可以在线程做完了最主要工作后做出让步,常见的应用场景是我们在调试或者测试并发时,通过yield重现某些场景。

总结

熟悉线程类Thread和线程状态转化的方式是学习并发编程和多线程相关算法的基础,synchronized通过关键字提供了一种加锁方式,它是简单的,但不是唯一的,也不一定高效,wait、notify、notifyAll也并不是唯一的等待阻塞和唤醒线程的方式,这些都将在后续文章中详细解读。

RestClient:Android下的Rest Testing

Restful是一种设计风格,很有优秀的API都采用了这种风格。

本文介绍一个简单的Android APP:RestClient。得益于自己对美和简洁有着天生的洁癖,也得益于对Volley源码的了如指掌,花了几天的时间,简单的用了所谓的MVP架构,功能是差不多了,凌乱的代码也懒得再花时间重构了(毕竟很多时候,我觉得漂亮就够了)。

功能和UI Design

我一直坚信,简洁可以产生美。

按照极简的设计,APP核心只有三个功能:Add Rest、Rest Testing、Preview Response。

  • Add Rest
    在增加一个Restful接口时,需要包含Rest的各种元素,请求URL、请求Method、请求参数、请求Header、对于POST和PUT请求,还有请求体等。对于参数、请求头、请求体需要有增删改的功能,这一切,都将在一个页面内渲染完成。

  • Rest Testing
    核心的模拟请求功能放在页面的右上角,点击后开始计时,可以随时终止请求。请求结束后,显示响应信息,包含请求耗时。

  • Preview Response
    对于rest接口的响应,我们首先最关心的是成功与否,所以Response Code直接展现在toolbar上,绿色背景的toolbar表示成功,红色则表示错误。其次,我们关心的是响应内容,预览不同格式的响应内容将是很重要的一个功能。

Request之Volley

Volley支持取消请求,并且统计每个请求的耗时。更多内容,参见Volley 源码设计

//设置超时时间为20s
request.setRetryPolicy(new DefaultRetryPolicy(20000, 0, 0f));
//设置不缓存
request.setShouldCache(false);
//Abort 请求
request.cancel();

在响应类NetWorkResponse中,含有如下响应内容:

/** The HTTP status code. */
public final int statusCode;

/** Raw data from this response. */
public final byte[] data;

/** Response headers. */
public final Map<String, String> headers;

/** True if the server returned a 304 (Not Modified). */
public final boolean notModified;

/** Network roundtrip time in milliseconds. */
public final long networkTimeMs;

Response之预览JSON、Html、Xml

内容预览分为两部分:格式化和着色。

  1. 格式化

JSON的格式化采用了Gson(相比较fastjson,我更喜欢用Gson)。

Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();

XML格式化采用Jdom2。

SAXBuilder jdomBuilder = new SAXBuilder();
Document jdomDoc = jdomBuilder.build(new StringReader(text));
XMLOutputter xml = new XMLOutputter();
xml.setFormat(Format.getPrettyFormat());
text = xml.outputString(jdomDoc);
  1. 着色

着色采用WebView+highlight.js方案,注意对于HTML和XML的着色,需要首先对特殊符号进行转义,如<、>等符号,转义可以使用org.apache.commons.text.StringEscapeUtils工具类。

More

RestClient支持Basic Authorization的鉴权方式。

其它也没有什么更多的想说的,就想写一句:能力越大,责任越大,对我如此,对团队也是如此。

探讨Dubbo与Swagger的集成

GitHub项目地址:https://github.com/Sayi/swagger-dubbo

Dubbo是一种透明化的RPC调用方案和服务治理方案,对外暴露服务接口Provider。Swagger构建了符合Open Api规范的API文档,通过SwaggerUI提供了模拟HTTP请求的工具。

本文将探讨的是Dubbo服务接口文档化,以及如何通过HTTP请求访问服务接口,便于应用在单机接口测试、服务快速验证、扩展服务方式等场景。

Dubbo服务接口和Http请求映射

服务接口与HTTP的映射,涉及到请求URL、HTTP方法、参数。

请求URL

一个请求URL需要唯一确定一个接口和一个方法,我们先看看一个接口的定义:

package com.deepoove.swagger.dubbo.example.api.service;

import java.util.List;
import com.deepoove.swagger.dubbo.example.api.pojo.User;

public interface UserService {

    List<User> query(String phone);

    List<User> query(int areaCode);

    User get(String id);

    void save(User user);

    User update(User user);

    void delete(String id);

}

我们采用interfaceClass/method来生成地址(当然也必须是interfaceClass,谁会希望在URL看到你的实现类类名)。即访问地址为http://ip:port/context/com.deepoove.swagger.dubbo.example.api.service.UserService/get,但是这种规则无法唯一确定一个重载的方法,比如代码中query方法。解决方案可能有下面两种:

  1. 在http request中附带参数个数、参数类型等信息。
  2. 对重载的方法设法指定一个别名,将请求地址映射为com.xxx.XxService/method/aliasName

方案1的优点是没有变更请求地址,缺点是对请求的信息做了附加,限制了请求的自由性。方案2的优缺点恰恰和方案1相反。

最终,我选择了方案2,我们需要找到一种生成别名的方法。本来考虑过通过参数个数和类型生成别名,但是我希望别名是可记忆性的,目前的方案是依赖swagger,在重载方法加上注解@ApiOperation,设置其nickname作为方法别名,这一步对重载方法来说,目前是必须的。代码可能变成了这样:

    @ApiOperation(nickname = "byPhone", value = "查询用户", notes = "通过phone取用户信息")
    List<User> query(String phone);

    @ApiOperation(nickname = "byArea", value = "查询用户", notes = "通过城市地区取用户信息")
    List<User> query(int areaCode);
HTTP方法

默认所有请求的方法都是POST,除非通过@ApiOperation指定了特别的httpMethod。对于不符合请求方法的HTTP调用,服务器都将返回404。

参数

参数的映射是个很复杂的问题,Spring在参数类型转化时做了很多事。http请求参数的值默认都是string的,如果接口参数恰恰是string类型,这样的映射没有问题。但是对于我们来说,有以下两点需要另处理:

  1. 参数类型为原生类型,参数值为string
    我们需要将string类型的参数值转化为原生类型的值。

  2. 参数类型为JavaBean
    第一次我会想到用request body去容纳JavaBean,但是当参数为多个JavaBean时,将多个参数值组合容纳在body中会导致无法理解参数的含义(body只有一个)。
    如果我们将JavaBean拆解成field作为参数,这样可能会让参数个数泛滥。
    最终我们把JavaBean类型的参数定义成表单参数,类型为json string,即将JavaBean序列化成json字符串作为参数。这样就解决了多个JavaBean作为参数的问题,尽管这一方案在模拟参数值时是复杂的。

  3. 参数类型为原生类型,参数值为null
    Spring不允许null赋值给原生类型的,因为我们不知道null代表原生类型的什么值。所以对于原生类型的参数,都应该为必填。

Swagger扫描Dubbo服务

定义好了映射关系,我们就可以让Swagger扫描Dubbo服务了。有以下几点需要做:

  1. 通过Spring上下文获取到所有注册的Dubbo服务接口和实现类
  2. 根据interfaceClass/method/{aliasName}规则,生成服务URL
  3. 根据方法参数生成请求参数

这里有一个问题,就是Swagger的文档注解是写在接口上,还是实现类上?

个人认为在一个公司内部,可以写在接口上,方便公司内部阅读。如果是对外使用,强烈不建议污染服务接口,应该写在实现类上。swagger-dubbo没有限制注解的位置,既可以写在接口上,也可以写在实现类上。

Swagger和HTTP模拟调用

HTTP模拟调用和Swagger两者之间本身不该有任何依赖和关联。
上文中提到的别名和请求方法是http模拟调用与Swagger的唯一依赖关系,如果不考虑自定义请求方法和重载方法的别名,可以不写任何swagger注解,就可以模拟HTTP调用,因为写swagger注解应该只是为了文档,而不该越权去定义HTTP请求。

但是,现在这些唯一的依赖关系,可能是最简单的实现。

NameTrending命名趋势

编写代码,离不开命名。

在阅读源码前,不如阅读一下所有的命名。我们对两个流行的Java Library进行了简单的命名分析,以期能了解这些库的一点知识和找到一点命名趋势。

guava

guava_trending

可以看到,这些命名体现了guava的侧重点。
命名中大多数含有Map、Set、List、Collection,同时拥有61个Immutable字符的名字,体现了guava对集合操作进行了大量的扩展。

62次Abstract表明了Guava中有着大量的抽象类。
173次Test排首位,可以看出测试用例覆盖广泛(我相信一个好的库,Test出现的频率一定也是最高的)。

spring-framework

spring_trending

1804次Test验证了一个好的库,Test出现的频率一定也是最高
370次Factory说明用了大量的工厂,330次Abstract有着不少的抽象类。
Resolve和Annotation表明默认自带了很多注解。
Message、Transaction、Converter、Resource、Interceptor等则体现了某方面的一些特性。

我们通常会考虑什么样的名字才是能准确表达含义,什么样的名字才是业界标准,什么样的名字能反应设计,什么样的名字才是好的名字?

我相信,好名字一定是好设计的前提。

NameTrending提供了简要统计命名趋势的功能
源码:GitHub NameTrending

Collections(六)Map和Set上篇

Map是一个键值对映射的集合,不允许重复的键,允许null的键或者值,关于Map接口的方法,请参考《Java Collections Framework(一)概览》。

前面提到过,Map和Collection是独立的两个接口,Java平台提供了三种通用实现: HashMap,LinkedHashMap和TreeMap,从名称中不难发现,Java没有采用可变数组为基础的实现,比如ArrayMap,或者单纯以链表为基础的实现,比如LinkedMap,这里有个重要的单词:Hash,即散列。

本文将详细讨论这些Map的实现原理。

基础知识:哈希表

在讨论Map之前,我们先来熟悉一下数据结构中提及到的哈希表。

哈希表是一种以常量平均时间进行插入和查找的技术,哈希表实现通常是一个数组,将不同的关键字映射到数组某个下标的位置,所以需要一个哈希函数来计算数组中的位置,如index = f(key)。

理想情况下,哈希函数尝试将不同的Key关联到一个唯一的数组下标index,但是由于数组大小是一定的,这就有可能导致不同的Key映射到同一个index,这就是哈希碰撞。

哈希函数
哈希表的性能取决于选择一个好的哈希函数。如果映射的Key是一个整数,那么我们可以 f(key) = key % size 来映射到index,但是如果这些整数具有一些特征,比如size如果是10,key的值也正好都是10的倍数,那么显然这个哈希函数就是糟糕的。

如果映射的key不是一个整数,而是字符串或者一个对象呢?Java通过hashcode()方法来计算这些对象的int类型散列码,然后再处理这个散列码,使其成为数组的下标。我们来看看HashMap中计算哈希函数的源码:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

可以看到,计算散列码的方式是对象的hashCode值与移位后的值异或,默认的hashcode是通过对象存储地址计算而来的,如果类重写了equals方法,没有重写hashcode方法,在计算散列码时,这个类的两个对象是不一致的,我们应该保证 两个对象equals,那么它们的hashcode应该是相同的,两个对象不equals,hashcode没有强制要求,所以如何编写类的hashcode方法就变得有意义了,具体写法《effective Java》里面有过详细描述,许多IDE也可以生成hashcode方法,这里给出String的源码:

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

注意的是,hash是一个类成员,因为String是final的,所以无需每次计算hashcode,使用hash变量来保存第一次的计算结果。

哈希碰撞
当不同的Key映射到同一个数组位置时,我们就需要解决哈希碰撞的问题,算法有开放寻址法(Open addressing)和分离链接法(Separate chaining)等,我们在这里介绍下 分离链接法

把数组的元素看成链表结构的头部,如果不同的key映射到同一个位置,如果当前位置是空,则使用当前位置保存元素,如果不为空,表示出现了哈希碰撞,就会向这个链表尾部追加元素。

我们在查找元素时,也是通过key映射到数组的下标,然后 遍历这个链表查找key值相同,hash值相同的元素

HashMap实现原理

image

HashMap内部正是以一个数组Node<K,V>[] table来维护哈希表的,通过分离链接法解决哈希碰撞,Node结构体描述了每个节点的结构,实现了Map.Entry<K,V>接口:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

从图我们看到,<k0, Sayi>、<k1, Sayi>、<k2, Sayi>三个元素经过哈希函数计算出现了哈希碰撞(即hash0、hash1、hash2都映射到下标2的位置)形成了一个链表,当我们寻找k2元素时,就会依次遍历k0、k1和k2。

备注:key为null的元素总是会存储在数组的第0个位置,因为它的hash返回值是0,取余后为0

我们再来看看如果哈希函数不合理会导致最坏的情况是什么样子的?

image

答案是退化成一个链表,其中k0到k形成了一个单向链表,当我们查询一个元素时,它的时间复杂度是O(n),而一个性能优异的哈希表的时间复杂度则常量级的。

初始容量和增长策略

HashMap也遵循了转换构造函数的设计,提供了一个无参构造函数和一个以Map为参数的构造函数,HashMap默认内部数组容量是由一个常量定义的,值为16:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

为了性能,HashMap提供了设置初始容量的构造函数:

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

注意到:DEFAULT_LOAD_FACTOR(默认值是0.75)是个加载因子,它定义了哈希表何时扩容,即如果当前容量是16,则当元素个数超过16*0.75=12时,就会选择扩容。

我们注意到哈希函数要将整型值映射到一个固定大小数组的某个位置,这是个求余运算,在《Java Collections Framework(四)Deque》介绍过,如果数组大小时2的幂次方,可以用位运算代替求余运算提供效率,所以 HashMap的容量也必须是2的幂次方

我们来看看当我们传任意容量的时候,它是怎么将初始值转化为四舍五入的2的幂次方的,这段代码和在Deque章节中讲到的是同一个算法。

/**
 * Returns a power of two size for the given target capacity.
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

我们现在已经知道, 初始容量是16且必须是2的幂次方,当元素超过容量乘以负载因子 时,就会扩容,我们来看看新增元素的代码:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

我们看到,通过位运算获得数组下标tab[i = (n - 1) & hash]),然后遍历链表,在链表结尾处插入新Node节点,如果遇到key相同的元素,则更改节点值为新值。

计算哈希的方法hash已经在前文讲过,我们直接拉到代码的最后几行:if (++size > threshold) resize();, threshold这个变量就是容量乘以负载因子的值表示阀值的意思,我们进入resize方法,看看具体如何扩容的:

inal Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 略去了扩容后重新设置元素的代码

其中newCap表示新的容量,newThr表示新的阀值,核扩容代码是这一行:

if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
         oldCap >= DEFAULT_INITIAL_CAPACITY)
    newThr = oldThr << 1; // double threshold

newCap和newThr都是原来的大小乘以2,之后我们便可以新分配数组空间newTab重新设置集合元素了。

红黑树

我们回过头再看看putVal代码中没有提及的地方:

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    treeifyBin(tab, hash);

我们知道,如果哈希表退化成链表之后,访问性能会大大降低,HahsMap对其作了一个优化尝试把O(n)的复杂度降低到O(logN), TREEIFY_THRESHOLD的常量值为8,即当碰撞元素大于8的时候(binCount没有包含第一个节点),就会尝试使用红黑树来代替链表。

我们接着看treeifyBin的源码:

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

MIN_TREEIFY_CAPACITY的常量值为64,从源码中我们可以得出结论什么时候会使用红黑树代替链表:

  1. 当哈希碰撞个数超过8的时候,容量小于64的时候,那么就会扩大容量来降低哈希碰撞
  2. 当哈希碰撞个数超过8的时候,容量大于等于64的时候,就会使用红黑树来代替链表

image

如图所示,这个时候节点的数据结构就会从Node变为TreeNode结构体如下,其中TreeNode继承了LinkedHashMap.Entry,而LinkedHashMap.Entry继承了Node结构体:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
}

关于红黑树的细节,请参考专业算法书。

HashMap和视图方法:keySet、valueSet、entrySet

HashMap的遍历是通过这三视图转化为Collection进行迭代遍历的,这三个方法都返回了内部类:

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

public Collection<V> values() {
    Collection<V> vs = values;
    if (vs == null) {
        vs = new Values();
        values = vs;
    }
    return vs;
}
public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

我们注意到values返回的是Collection,因为值可以重复。

三个内部集合类实现了迭代器KeyIterator、ValueIterator和EntryIterator,它们继承了HashIterator。

abstract class HashIterator {
    Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot

    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // advance to first entry
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        if ((next = (current = e).next) == null && (t = table) != null) {
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }
    // 略

HashMap同遵循了fail-fast的设计,核心迭代下一个元素的代码如下,其中赋值和判融为一体,这行代码并不难理解:

if ((next = (current = e).next) == null && (t = table) != null) {
      do {} while (index < t.length && (next = t[index++]) == null);
}

Dubbo 2.5.3+

1. 关于Dubbo、原理、负载均衡

Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。
官网地址 http://dubbo.io
GitHub地址 https://github.com/alibaba/dubbo
Dubbo 是阿里巴巴的一个开源产品,淘宝内部使用的HSF并未开源。
关于Dubbo的架构可以参考官网的示例图

http://dubbo.io/dubbo-architecture.jpg-version=1&modificationDate=1330892870000.jpg

当整个系统越来越庞大的时候,基础的分布式服务越来越多,这些基础服务的负载均衡,服务URL配置管理,服务的协调都显得复杂,dubbo就是这样一个分布式服务的协调和治理框架。

与Hessian比较,dubbo具有如下优势:

  • 服务URL地址的管理,服务自动注册与发现,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的IP地址,并且能够平滑添加或删除服务提供者
  • Dubbo采用全Spring配置方式,透明化接入应用,对应用没有任何API侵入,只需用Spring加载Dubbo的配置即可,Dubbo基于Spring的Schema扩展进行加载。
  • Dubbo提供了多种均衡策略,缺省为random随机调用。

Random LoadBalance随机,按权重设置随机概率。
RoundRobin LoadBalance轮循,按公约后的权重设置轮循比率。。
LeastActive LoadBalance最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
ConsistentHash LoadBalance 一致性Hash,相同参数的请求总是发到同一提供者。

2. Dubbo的几个概念:注册中心、服务提供者、服务消费者、协议(dubbo、http、hessian)

在架构图中,可以看到有以下几个节点:

  • Provider: 暴露服务的服务提供方。
  • Consumer: 调用远程服务的服务消费方。
  • Registry: 服务注册与发现的注册中心。
  • Monitor: 统计服务的调用次调和调用时间的监控中心。
  • Container: 服务运行容器。
  1. 注册中心
    注册中心负责服务地址的注册与查找,相当于目录服务。服务提供者向注册中心注册自己的服务,服务消费者向注册中心发现自己的服务,当服务有变更时,注册中心通知到消费者。消费者发现所需服务列表后,将根据软负载均衡,选择一台提供者进行调用。注册中心,服务提供者,服务消费者三者之间均为长连接。
    注册中心推荐使用Zookeeper,还可以使用Redis、数据库等客户端。如<dubbo:registry address="zookeeper://127.0.0.1:7181" />

  2. 服务提供者
    暴露服务一方,每个接口都应定义版本号,为后续不兼容升级提供可能。由于默认使用hessian二进制序列化,所以接口的参数和返回值必须实现序列化接口。如<dubbo:service interface="com.deepoove.XxxService" version="1.0" />

  3. 服务消费者
    服务消费者向注册中心获取服务提供者地址列表,并根据负载算法直接调用提供者。<dubbo:reference id="xxxService" interface="com.deepoove.XxxService" />

  4. 协议
    http://dubbo.io/dubbo-protocol.jpg-version=1&modificationDate=1331068241000.jpg
    服务调用涉及到网络通讯,缺省协议为dubbo://,dubbo协议采用单一长连接和NIO异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。对于大数据量不建议采用dubbo协议,可以考虑短连接协议,比如rmi、http。dubbo协议默认序列化方式为hessian2,对性能有更高要求可以使用dubbo序列化。
    <dubbo:protocol name=“dubbo” port=“9090” server=“netty” client=“netty” codec=“dubbo” serialization=“hessian2” charset=“UTF-8” threadpool=“fixed” threads=“100” queues=“0” iothreads=“9” buffer=“8192” accepts=“1000” payload=“8388608” />

  • Serialization枚举值:dubbo, hessian2, java, json
    dubbo协议说明:
  • 连接个数:单连接
  • 连接方式:长连接
  • 传输协议:TCP
  • 传输方式:NIO异步传输
  • 序列化:Hessian二进制序列化
  • 适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用dubbo协议传输大文件或超大字符串。
  • 适用场景:常规远程服务方法调用

除了默认dubbo协议,还有rmi、http、hessian(可以与原生hessian服务互操作)等协议。

3. 注册中心Zookeeper

Zookeeper是一个针对分布式应用程序的高性能协调服务,提供的功能有:命名、配置管理、同步、组服务等。Because coordinating distributed systems is a Zoo。
dubbo默认为zkclient,推荐使用Java客户端Curator,中文意思为馆长。

单机安装Zookeeper,下载tar包:

~/sayi/zookeeper/zookeeper-3.5.2-alpha$ cd conf/
~/sayi/zookeeper/zookeeper-3.5.2-alpha/conf$ cp zoo_sample.cfg zoo.cfg
~/sayi/zookeeper/zookeeper-3.5.2-alpha/conf$ vi zoo.cfg
~/sayi/zookeeper/zookeeper-3.5.2-alpha/conf$ cd ../bin/
~/sayi/zookeeper/zookeeper-3.5.2-alpha/bin$ ./zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /home/sayi/zookeeper/zookeeper-3.5.2-alpha/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED

至此,Zookeeper已经启动完毕,其中zoo.cfg内容如下:

# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial 
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between 
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just 
# example sakes.
dataDir=~/sayi/zookeeper/data/zookeeper
# the port at which the clients will connect
clientPort=7181

客户端连接Zookeeper,连接成功后可以通过help查看可用命令:
./zkCli.sh -server 127.0.0.1:7181

Dubbo使用Zookeeper作为注册中心,目录结构如下:

---dubbo
-----|---com.deepoove.XxxService
------------|-----configurators
------------|-----routers
------------|-----consumers
------------|-----providers
----------------------|--dubbo://172.16.11.18:20880/com.deepoove.XxxService?XXX

流程说明:
1.服务提供者启动时
  向/dubbo/com.deepoove.XxxService/providers目录下写入自己的URL地址。
2.服务消费者启动时
  订阅/dubbo/com.deepoove.XxxService/providers目录下的提供者URL地址。
  并向/dubbo/com.deepoove.XxxService/consumers目录下写入自己的URL地址。
3.监控中心启动时
  订阅/dubbo/com.deepoove.XxxService目录下的所有提供者和消费者URL地址。

4. Dubbo Example 一个示例

Dubbo版本为2.5.3,Zookeeper版本3.5.2。服务提供方[项目dubbo_example]采用jetty(tomcat)作为服务容器,服务消费端[项目dubbo_example_consumer]只是一个简单的Main方法,并加载一个简单的Spring容器,用于消费服务,服务接口[项目dubbo_example_api](该接口需单独打包,在服务提供方和消费方共享)不依赖任何jar包。项目基于Gradle进行构建。

1. 服务接口dubbo_example_api

服务接口需要单独打包,供服务提供方和消费方共享。定义一个简单的接口如下:

package com.deepoove.dubboexample.api.service;
import com.deepoove.dubboexample.api.pojo.User;

public interface UserService {
	User getUser(String id);
}

定义简单的类User:

package com.deepoove.dubboexample.api.pojo;
import java.io.Serializable;

public class User implements Serializable{

	private static final long serialVersionUID = -1169812613737118557L;
	private String id;
	private String name;
	private String site;

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getSite() {
		return site;
	}

	public void setSite(String site) {
		this.site = site;
	}
}

2. 服务提供方dubbo_example

项目除了依赖dubbo、spring、Zookeeper、zkclient,还需要加上服务接口的依赖,在settings.gradle文件中,包含服务接口项目:

rootProject.name = 'dubbo_example'

include ':dubbo_example_api'
project(':dubbo_example_api').projectDir = new File(settingsDir, '../dubbo_example_api')

由于采用logback打印日志,同时使用jetty作为容器,所以我也加上了logback的依赖和jetty gradle插件,最终build.gradle文件内容如下:

apply plugin: 'java'
apply plugin: 'war'
apply plugin: 'jetty'

repositories {
    mavenLocal()
    mavenCentral()
    jcenter()
}

dependencies {
    compile project(":dubbo_example_api")
    compile group: 'org.springframework', name: 'spring-webmvc', version: '4.1.5.RELEASE'
    compile group: 'org.springframework', name: 'spring-test', version: '4.0.5.RELEASE'
    compile group: 'javax.servlet', name: 'javax.servlet-api', version: '3.0.1'
    
    compile (group: 'com.alibaba', name: 'dubbo', version: '2.5.3'){
    	exclude group: 'org.springframework'
    }
    compile group: 'org.apache.zookeeper', name: 'zookeeper', version: '3.5.2-alpha'
    compile group: 'com.github.sgroschupf', name: 'zkclient', version: '0.1'

    compile group: 'ch.qos.logback', name: 'logback-core', version: '1.1.7'
    compile group: 'ch.qos.logback', name: 'logback-classic', version: '1.1.7'
    testCompile 'junit:junit:4.12'
}

jettyRun{
	contextPath = "demo"
	httpPort = 8077
}

定义服务接口的实现,实现类如下:

package com.deepoove.dubboexample.provider;

import com.deepoove.dubboexample.api.pojo.User;
import com.deepoove.dubboexample.api.service.UserService;

public class UserServiceImpl implements UserService {

	@Override
	public User getUser(String id) {
		User user = new User();
		user.setId("sayi");
		user.setName("sayi");
		user.setSite("http://www.deepoove.com");
		return user;
	}

}

服务接口实现好之后,接下来就可以配置dubbo,在src/main/resources下新增logback.xml日志配置文件,在src/main/resources/application下新增配置文件remote-provider.xml,同时在web.xml中配置路径。

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:application/*.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

remote-provider.xml中的典型配置如下,我们通过dubbo:application配置提供方应用信息名称,通过dubbo:registry向注册中心暴露服务地址,通过dubbo:protocol配置暴露服务的协议和端口,通过dubbo:service申明要暴露的服务接口。

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="
        http://www.springframework.org/schema/beans     
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://code.alibabatech.com/schema/dubbo
        http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

	<dubbo:application name="dubbo-example-app" />

	<dubbo:registry address="zookeeper://127.0.0.1:7181" />

	<dubbo:protocol name="dubbo" port="20880" />

	<dubbo:service interface="com.deepoove.dubboexample.api.service.UserService"
		ref="userRemoteService" />

	<bean id="userRemoteService" class="com.deepoove.dubboexample.provider.UserServiceImpl" />

</beans>

运行grade jettyrun后,可以通过查看Zookeeper的目录或者lsof -i:20880查看服务提供的接口是否使用来确认服务是否提供成功。

3. 服务消费端dubbo_example_consumer

消费方式一个Main方法,加载了spring容器,同时依赖服务接口项目。定义文件remote-consumer.xml,通过dubbo:application配置消费方应用名,通过dubbo:reference生成远程服务代理,可以和本地bean一样使用,内容如下:

    <dubbo:application name="consumer-of-dubbo-example-app" />
    <dubbo:registry address="zookeeper://115.29.10.121:7181" />
    <dubbo:reference id="userService"
		interface="com.deepoove.dubboexample.api.service.UserService" />

Main方法中加载此xml文件,远程调用服务方提供的接口:

package com.deepoove.dubbo_example_consumer;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.deepoove.dubboexample.api.pojo.User;
import com.deepoove.dubboexample.api.service.UserService;

public class Consumer {

	public static void main(String[] args) throws Exception {
		ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
				new String[] { "remote-consumer.xml" });
		context.start();

		UserService userService = (UserService) context.getBean("userService"); // 获取远程服务代理
		User sayi = userService.getUser("sayi"); // 执行远程方法

		System.out.println(sayi); // 显示调用结果

		System.in.read(); // 按任意键退出
		
		context.close();
	}
}

5. Dubbo:检测服务是否提供成功、验证是否可用

在实际使用中,在服务提供者启动完毕后,通常会检测服务是否发布成功,或者对服务的某个接口进行调用测试。大体上有以下三个方法:

  • 检测服务提供者监听端口号
lsof -i:20880
COMMAND  PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
java    4675  Sai  217u  IPv6 0x1ca3042d017b5851      0t0  TCP *:20880 (LISTEN)

linux上也可以用命令netstat -apn | grep 20880

  • 查看注册中心是否注册服务
    以Zookeeper为例,可以查看在providers目录下是否暴露对应服务,同时,我们也可以提供一些连接Zookeeper的dubbo工具,如dubbo-admin、dubbo-monitor。
[zk: 127.0.0.1:7181(CONNECTED) 0] ls /
[dubbo, zookeeper]
[zk: 127.0.0.1:7181(CONNECTED) 1] ls /dubbo
[com.alibaba.dubbo.monitor.MonitorService, com.deepoove.dubboexample.api.service.UserService]
[zk: 127.0.0.1:7181(CONNECTED) 2] ls /dubbo/com.
com.alibaba.dubbo.monitor.MonitorService            com.deepoove.dubboexample.api.service.UserService   
[zk: 127.0.0.1:7181(CONNECTED) 2] ls /dubbo/com.deepoove.dubboexample.api.service.UserService
[configurators, consumers, providers, routers]
[zk: 127.0.0.1:7181(CONNECTED) 3] ls /dubbo/com.deepoove.dubboexample.api.service.UserService/providers
[dubbo%3A%2F%2F172.16.11.18%3A20880%2Fcom.deepoove.dubboexample.api.service.UserService%3Fanyhost%3Dtrue%26application%3Ddubbo-example-app%26dubbo%3D2.5.3%26interface%3Dcom.deepoove.dubboexample.api.service.UserService%26methods%3DgetUser%26pid%3D4675%26revision%3Ddubbo_example_api%26side%3Dprovider%26timestamp%3D1478840366117]
  • 模拟服务消费者调用服务
    对于http协议可以通过浏览器进行模拟调用,但是由于缺省采用的是dubbo协议,所以我们需要写个简单的Main方法,参加服务消费端dubbo_example_consumer,如果不考虑具体方法的调用,可以采用回声测试:所有服务自动实现EchoService接口,只需将任意服务引用强制转型为EchoService。
    UserService demoService = (UserService) context.getBean("userService"); // 获取远程服务代理
    EchoService echoService = (EchoService) demoService; // 强制转型为EchoService
    Object status = echoService.$echo("OK"); // 回声测试可用性
    assert (status.equals("OK"));

6. Dubbo admin 和 Dubbo monitor

  • Dubbo admin
    获取dubbo-admin.war,修改文件WEB-INF/dubbo.properties文件,配置Zookeeper地址:
dubbo.registry.address=zookeeper://127.0.0.1:7181
dubbo.admin.root.password=root
dubbo.admin.guest.password=guest

服务启动完毕后,浏览器访问控制台:127.0.0.1:8080/,输入密码进入管理界面。

  • Dubbo monitor
    监控中心也是一个标准的dubbo服务,所以也需要配置dubbo.protocol.port端口。
dubbo.container=log4j,spring,registry,jetty
dubbo.application.name=simple-monitor
dubbo.application.owner=
dubbo.registry.address=zookeeper://127.0.0.1:7181
dubbo.protocol.port=7077
dubbo.jetty.port=8099
dubbo.jetty.directory=/Users/Sai/Sayi/source/dubbo/monitordirectory/monitor
dubbo.charts.directory=${dubbo.jetty.directory}/charts
dubbo.statistics.directory=/Users/Sai/Sayi/source/dubbo/monitordirectory/monitor/statistics
dubbo.log4j.file=/Users/Sai/Sayi/source/dubbo/monitordirectory/logs/dubbo-monitor-simple.log
dubbo.log4j.level=WARN

通过命令bin/start.sh启动监控中心服务,浏览器访问127.0.0.1:8099/,进入监控界面。

7. 安全:关于Zookeeper的ACL

Zookeeper使用ACL控制节点的访问,ACL不具有继承性,即对父节点设置的Access Control不应用到子节点上。ACL可以理解成为scheme : id : permission的组合,其中permission为:CREATE、READ、WRITE、DELETE、ADMIN。Zookeeper支持pluggable authentication schemes.

  • world has a single id, anyone, that represents anyone.
  • auth doesn't use any id, represents any authenticated user.
  • digest uses a username:password string to generate MD5 hash which is then used as an ACL ID identity. Authentication is done by sending the username:password in clear text. When used in the ACL the expression will be the username: base64 encoded SHA1 password digest(sha1加密后base64编码) .
  • host uses the client host name as an ACL ID identity. The ACL expression is a hostname suffix. For example, the ACL expression host:corp.com matches the ids host:host1.corp.com and host:host2.corp.com, but not host:host1.store.com.
  • ip uses the client host IP as an ACL ID identity. The ACL expression is of the form addr/bits where the most significant bits of addr are matched against the most significant bits of the client host IP.

下面的示例演示通过zkcli操作ACL,setAcl可以设置权限,getAcl获取权限列表:

[zk: 127.0.0.1:7181(CONNECTED) 14] ls /
[dubbo, zookeeper]
[zk: 127.0.0.1:7181(CONNECTED) 15] create /sayi "hello,data"
Created /sayi
[zk: 127.0.0.1:7181(CONNECTED) 16] get /sayi
hello,data
[zk: 127.0.0.1:7181(CONNECTED) 17] getAcl /sayi
'world,'anyone
: cdrwa
[zk: 127.0.0.1:7181(CONNECTED) 18] addauth digest sayi:123456
[zk: 127.0.0.1:7181(CONNECTED) 19] setAcl /sayi auth:sayi:123456:cdrwa
[zk: 127.0.0.1:7181(CONNECTED) 20] getAcl /sayi
'digest,'sayi:8n7aQg/S4IP6WJGOwf3DGTLg4iY=
: cdrwa
[zk: 127.0.0.1:7181(CONNECTED) 21] 

8. 服务参数校验、服务异常捕获、请求日志、服务文档

Collections(九)Guava Collections

Java Collections Framework基本介绍完了,当我们遇到性能或者有需需要定义自己的集合时,比如希望数据不是保存在内存中而是从数据库迭代,可以扩展JDK提供的明确设计的抽象实现AbstractXXX接口。

Guava提供了一些特别的集合实现和工具类,本文旨在入门Guava Collections,分为三个部分:

  1. 更多的集合类型
  2. 不可变集合
  3. 工具

详细文档请参阅官方文档:https://github.com/google/guava/wiki。

更多的集合类型

Multiset

Multiset是一个元素可以重复的集合,Multiset可以被看做一个无需关心顺序ArrayList,或者一个元素和元素个数的Map。如果没有这个类,我们通常的实现会是这样的:

new ArrayList<E>()
或者
new HashMap<E, Integer>

Multiset 继承了JDK Collection接口,并且提供了一些额外的方法。

方法 作用
int count(@nullable @CompatibleWith("E") Object element) 获取元素的个数
int setCount(E element, int count); 设置元素的个数
int add(@nullable E element, int occurrences); 新增元素
Set elementSet(); 获取不同的元素集合

根据不同特性和实现,Guava提供了以下几种Multiset:

  • HashMultiset
  • LinkedHashMultiset
  • TreeMultiset
  • ConcurrentHashMultiset
  • ImmutableMultiset

因为泛型书写的原因,Guava中创建集合大多数采用了工厂方法,不建议用new的方式,所以我们创建一个Multiset的代码如下:

Multiset<String> set = HashMultiset.<String>create();

Multimap

Multimap是一个key值允许重复的集合,可以看作为Map<K, List>。如果没有这个类,我们的实现可能是这样的:

new HashMap<K, Collection<V>>

Multimap是一个独立的接口,它不是一个Ma,提供了一些基础方法:

方法 作用
Collection get(@nullable K key); 获取key对应的值集合
Multiset keys(); 获取所有可重复的keys
Map<K, Collection> asMap();转换为Map

如果当你希望使用key-collection这样的结构时,就可以考虑使用Multimap,根据valus的实现方式不同,我们更多用的是接口ListMultimap和接口SetMultimap,前者返回的值是一个列表List<V> get(@Nullable K key);,后者返回的是一个set:Set<V> get(@Nullable K key);

我们通过构建器MultimapBuilder进行初始化:

ListMultimap<String, String> map = MultimapBuilder.hashKeys().arrayListValues().build();

BiMap

BiMap是一个双向Map,既可以key-value映射,也可以value-key映射(调用inverse()方法),所以要求value必须是唯一的,实现原理是内部维护了两张哈希表。具体实有以下几个:

  • HashBiMap
  • EnumBiMap
  • EnumHashBiMap
  • ImmutableBiMap
    同样,当我们初始化这些类时,是通过静态方法而不是new:
BiMap<String, Integer> userId = HashBiMap.create();

ClassToInstanceMap

ClassToInstanceMap一个专用的实现,表示类型与实例的映射,继承Map<Class<? extends B>, B>,我们可以看看接口定义:

public interface ClassToInstanceMap<B> extends Map<Class<? extends B>, B> {
  <T extends B> T getInstance(Class<T> type);
  <T extends B> T putInstance(Class<T> type, @Nullable T value);
}

Guava提供了两个实现MutableClassToInstanceMap和ImmutableClassToInstanceMap,同样通过静态方法初始化:

MutableClassToInstanceMap.create();

不可变集合

顾名思义,该集合不支持修改操作,比如:

  • ImmutableList
  • ImmutableSet
  • ImmutableMap
  • ImmutableMultiset
  • ImmutableMultimap
  • ImmutableBiMap

通过前面文了解过,JDK提供了一个包装方法返回一个不可修改的集合:Collections.unmodifiableXXX,那么为什么Guava提供了新的实现方式呢?主要基于以下几点来考虑:

  1. 需要一个集合的常量的场景是广泛的,如果都要通过包装方法,显得冗余且厌烦
  2. 包装方法通常需要先实例化一个集合,而这个集合的引用如果继续被操作将影响到不可变的集合
  3. 包装方法是低效的,背后调用了一个具体集合的方法,里面可能包含了很多在不可变集合中无用的代码,所以我们需要针对不可变写一个高效的代码。

注意:所有不可变集合不支持null值,因为google觉得大多数场景下都不会使用null值。
构造不可变集合的方式也是通过工厂方法或者构建器:

Builder<String> builder = ImmutableList.<String>builder();
builder.add(str);
ImmutableList<String> list = builder.build();

// of
ImmutableList<String> list2 = ImmutableList.of("hi");

// copy of
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add("hi");
ImmutableList<Object> copyOf = ImmutableList.copyOf(arrayList);

工具

正如JDK提供了Collections工具类一样,Guava也提供了一系列工具类。

  • Collections2 区别于JDK工具类
  • Lists
  • Sets
  • Maps

这里不作介绍,可以自行查看官方文档,值得注意的是,这些工具类提供的静态构造方式Lists.newArrayList()已经不建议使用了,官方建议jdk1.7以后使用<>语法来代替这些工厂方法。

装饰器模式

Guava提供了一系列ForwardingXXX类为我们对集合进行装饰提供了便捷,我们只需要继承这些类,然后实现抽象方:

protected abstract List<E> delegate();

接下来,你只需要重写对应的方法进行装饰了。

抽象迭代器

我们知道,实现Iterator接口需要实现几个方法,guava为我们封装了一个实现Iterator接口的抽象类AbstractIterator方便我们继承,我们只需要方法computeNext即可,下面这段代码实现了一个迭代字符串的列表。

package com.deepoove.datastructure;

import java.util.AbstractList;
import java.util.Iterator;
import java.util.Objects;

import com.google.common.collect.AbstractIterator;

public class CharacterList extends AbstractList<Character> {

  private transient String str;

  public CharacterList(String str) {
      Objects.requireNonNull(str);
      this.str = str;
  }

  @Override
  public Character get(int index) {
      return str.charAt(index);
  }

  @Override
  public int size() {
      return str.length();
  }

  @Override
  public Iterator<Character> iterator() {
      return new AbstractIterator<Character>() {

          int i = 0;

          @Override
          protected Character computeNext() {
              if (i == CharacterList.this.size()) return endOfData();
              return CharacterList.this.str.charAt(i++);
          }
      };

//        return new Iterator<Character>() {
//
//            int i = 0;
//
//            @Override
//            public boolean hasNext() {
//                return i < CharacterList.this.size();
//            }
//
//            @Override
//            public Character next() {
//                return CharacterList.this.str.charAt(i++);
//            }
//        };
  }

}

总结

虽然JDK也能完某些任务,但是Guava Collections提供了很大的便利性。

这是这个系列文的最后一篇,熟读并不是需要你记得这些内容,而是更好的使用它。

swagger-diff:比较swagger文档

Swagger围绕着OpenAPI规范,提供了一套设计、构建、文档化rest api的开源工具,在实践中,它也是前后端沟通的桥梁。

当我们服务多次发布,Swagger API Doc也随之变化,我们称之为接口的升级。

swagger-diff致力于输出这些变化,本文对基本原理作个介绍,使用文档参见官网Document

swagger-diff

基本原理其实很简单,Swagger API Doc是以JSON格式组织的,我们先看看一个接口的JSON结构:

"/pet/findByStatus": {
  "get": {
    "tags": [
      "pet"
    ],
    "summary": "Finds Pets by status",
    "description": "Multiple status values can be provided with comma separated strings",
    "operationId": "findPetsByStatus",
    "produces": [
      "application/xml",
      "application/json"
    ],
    "parameters": [
      {
        "name": "status",
        "in": "query",
        "description": "Status values that need to be considered for filter",
        "required": true,
        "type": "array",
        "items": {
          "type": "string",
          "enum": [
            "available",
            "pending",
            "sold"
          ],
          "default": "available"
        },
        "collectionFormat": "multi"
      }
    ],
    "responses": {
      "200": {
        "description": "successful operation",
        "schema": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/Pet"
          }
        }
      },
      "400": {
        "description": "Invalid status value"
      }
    },
    "security": [
      {
        "petstore_auth": [
          "write:pets",
          "read:pets"
        ]
      }
    ]
  }
}

从结构体中可以得到以下几点:

  • 请求路径/pet/findByStatus
  • 请求方法get
  • 描述description
  • 请求参数parameters
  • 返回值responses
    ......

所以差异化的核心就是围绕这些字段作差异化输出,算法的核心也简化成对Map的比较,对object里面每个属性的的比较。

Map比较

比较两个Map的Key,输出共有的Key、增加的元素和减少的元素。

private Map<K, V> increased;
private Map<K, V> missing;
private List<K> sharedKey;

public static <K, V> MapKeyDiff<K, V> diff(Map<K, V> mapLeft,
        Map<K, V> mapRight) {
    MapKeyDiff<K, V> instance = new MapKeyDiff<K, V>();
    if (null == mapLeft && null == mapRight) return instance;
    if (null == mapLeft) {
        instance.increased = mapRight;
        return instance;
    }
    if (null == mapRight) {
        instance.missing = mapLeft;
        return instance;
    }
    instance.increased = new LinkedHashMap<K, V>(mapRight);
    instance.missing = new LinkedHashMap<K, V>();
    for (Entry<K, V> entry : mapLeft.entrySet()) {
        K leftKey = entry.getKey();
        V leftValue = entry.getValue();
        if (mapRight.containsKey(leftKey)) {
            instance.increased.remove(leftKey);
            instance.sharedKey.add(leftKey);

        } else {
            instance.missing.put(leftKey, leftValue);
        }

    }
    return instance;
}

object比较

对象比较其实就是每个属性比较,属性作为新对象我们会接着去递归比较。

我们通过.符号输出更深层次的比较,比如新增user.address.num参数,可以理解成user对象里面的address对象新增加了一个属性num。具体算法参考源码。

优化输出:diff文档

swagger-diff提供了HTML和Markdown两种格式的输出文档。可以用模板引擎去实现这个功能,比如freemarker,后续有时间会朝这个方向优化,目前使用j2html去生成html,字符串拼接markdown。

String html = new HtmlRender("Changelog",
    "http://deepoove.com/swagger-diff/stylesheets/demo.css")
            .render(diff);

String md = new MarkdownRender().render(diff);

image

CLI命令行工具

为了使这个工具更加易用,提供了CLI命令行交互,采用了POSIX风格。

java -jar swagger-diff.jar \
-old http://petstore.swagger.io/v2/swagger.json \
-new http://petstore.swagger.io/v2/swagger.json \
-v 2.0 \
-output-mode html > diff.html

最后

如果不是早就厌倦了java swing,我想我还有激情开发一个桌面版本,欢迎提交PR。

最后, 😄 也许,在你每次服务迭代的时候,把这样的一份diff文档发给测试,发给产品,才会明白深藏功与名的道理。

第三方平台移动应用的签名

无论是微信的开发者中心,还是微博的移动应用认证,都需要你的android签名。
1.手机安装了你的已签名的apk,然后下载签名工具,输入包名
2.通过java自带的命令行

  keytool -list -v -keystore ~/Sayi//debug.keystore -storepass passowrd

为你的Github项目加上徽章

程序员是有趣的,他们喜欢简简单单的字母,有时候也想变得好玩点,他们使用emoji,并且为他们的项目加上了徽章。

image

徽章就是如上图的东西,每个徽章通过色彩和数字简要的说明了项目的某种特征,更加直观的多维度了解这个项目。

Travis CI徽章

Travis CI是一个持续集成平台,通过CI我们可以第一时间知道我们的项目有没有打包成功,获得徽章,我们参照官网的指南就可以了。

  1. 使用GitHub登录,激活github 仓库
  2. 在项目根目录下,编写.travis.yml文件
  3. 提交代码会自动触发build

接下来我们就可以在travis-ci上获得这个徽章,我们可以选择Markdown语法,然后写到项目的Readme里。

Sonar 徽章

打开https://sonarcloud.io/,同样使用GitHub登录,按照说明我们获取到token,然后在Github项目根目录下执行命令:

mvn sonar:sonar \
>   -Dsonar.organization=sayi-github \
>   -Dsonar.host.url=https://sonarcloud.io \
>   -Dsonar.login=XXXXXX

执行好后,在sonarcloud上的项目目录下,就可以领取徽章了。

Coveralls

对于普通的Java项目,需要两步:

  1. Add your repository to Coveralls.
    打开https://coveralls.io/,使用GitHub账号登录,将你需要测试的代码项目开关打开。

  2. Configure your build to install the Coveralls library for the programming language you’re using.
    Coveralls与Travis是自动集成的,修改Travis的.travis.yml文件,最后加上这一行:

after_success:
  - mvn clean cobertura:cobertura org.eluder.coveralls:coveralls-maven-plugin:report

同时在项目的POM.xml文件里面,引入响应的插件:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>cobertura-maven-plugin</artifactId>
    <version>2.7</version>
    <configuration>
    <format>xml</format>
    <maxmem>256m</maxmem>
    <!-- aggregated reports for multi-module projects -->
    <aggregate>true</aggregate>
    </configuration>
</plugin>
<plugin>
    <groupId>org.eluder.coveralls</groupId>
    <artifactId>coveralls-maven-plugin</artifactId>
    <version>4.3.0</version>
</plugin>

至此,当触发CI的时候,就会上传覆盖率的报告,我们可以去coveralls领徽章了。

更多徽章

我们可以去gitter领社交徽章、也可以接着去领取license徽章,等等。

但是程序员的世界你不懂,当我们不断的领徽章的时候,有一部分程序员提供了徽章服务,你完全可以自定义自己的徽章。打开下面这个链接,你将看到一个GoodGame徽章:

https://img.shields.io/badge/goodgame-100%25-green.svg

服务是由https://shields.io/提供。

poi-tl处理Word表格(Table)的最佳实践

⚠️ 这里是旧版1.2.0文档,最新文档参见http://deepoove.com/poi-tl/

表格对于页面的布局具有重大的意义,正因为其灵活性,所以用模板引擎处理word中的Table时,显得并不是那么简单,本文将讨论如何利用poi-tl(1.2.0版本)提供的工具来简化表格处理。

表格模板

poi-tl默认实现了N行N列的样式(如下图),同时提供了当数据为空时,展示一行空数据的文案(如下图中的No Data Descs)。
image

在poi-tl的1.2.0版本中,表格模板语法是#,数据结构是com.deepoove.poi.data.TableRenderData。

  1. 表格头使用headers[]定义,支持设置背景色
  2. 表格数据使用datas[]定义,不同列的数据在datas中使用分号隔开
  3. 宽度使用width定义
  4. 无数据文案使用noDatadesc定义
{
  "datas": [
    "beijing;beijing",
    "zhejiang;hangzhou"
  ],
  "headers": [
    {
      "style": {
        "color": "1E915D",
        "fontSize": 0
      },
      "text": "province"
    },
    {
      "style": {
        "color": "1E915D",
        "fontSize": 0
      },
      "text": "city"
    }
  ],
  "noDatadesc": "no datas",
  "width": 0
}

具体Java代码参考:

@Test
public void testTable() throws Exception {
  Map<String, Object> datas = new HashMap<String, Object>() {
    {
      // 有表格头 有数据
      put("table", new TableRenderData(new ArrayList<RenderData>() {
        {
          add(new TextRenderData("1E915D", "province"));
          add(new TextRenderData("1E915D", "city"));
        }
      }, new ArrayList<Object>() {
        {
          add("beijing;beijing");
          add("zhejiang;hangzhou");
        }
      }, "no datas", 0));
    }
  };
  XWPFTemplate template = XWPFTemplate.compile("src/test/resources/table.docx").render(datas);

  FileOutputStream out = new FileOutputStream("out_table.docx");
  template.write(out);
  out.flush();
  out.close();
  template.close();
}

表格的宽度怎么定义的

是一个点的二十分之一,或者是1440分之一英寸。官方解释如下:

dxa - Specifies that the value is in twentieths of a point (1/1440 of an inch).
首先1英寸=2.54厘米,A4纸大小为21cm*29.7cm。
如果这个width设置成5670,则表示这个表格的宽度是10cm。

抛开对这个单位理解的难度,我们最常见的应该是宽度自适应和宽度最大。
如果在poi-tl中设置了width=0,则表格是宽度自适应的。
以A4纸为例,页面宽度为21cm,左右页边距各位3.17cm,则表格的width=(21-3.172)/2.541440,大约为8310。

合并单元格

但是,很多业务场景并不仅限于如此简单的表格布局,产品需求总是丰富多彩的。poi-tl对XWPFDocument进行了封装,增强实现了一些基本功能。在com.deepoove.poi.NiceXWPFDocument中提供了合并的功能。

/**
 * 合并行单元格
 * @param table
 * @param row
 * @param fromCol
 * @param toCol
 */
public void mergeCellsHorizonal(XWPFTable table, int row, int fromCol,
    int toCol)

/**
 * 合并列单元格
 * @param table
 * @param col
 * @param fromRow
 * @param toRow
 */
public void mergeCellsVertically(XWPFTable table, int col, int fromRow,
    int toRow)

自定义表格之新建表格

我们完全可以从无到有去创建一个新的表格。

  1. 无需事先创建表格,在docx中,直接输入{{table}}
  2. 默认{{table}}是文本模板,我们需要通过registerPolicy设置此模板为自定义模板。
XWPFTemplate template = XWPFTemplate.compile("src/test/resources/complex.docx");
template.registerPolicy("table", new MyTableRenderPolicy());
  1. 新建MyTableRenderPolicy.java,实现RenderPolicy接口
@Override
public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
  NiceXWPFDocument doc = template.getXWPFDocument();
  RunTemplate runTemplate = (RunTemplate) eleTemplate;
  XWPFRun run = runTemplate.getRun();
  if (null == data) return;

  //doc.insertNewTable(run, row, col);
  //doc.mergeCellsHorizonal(table, 1, 0, 1);
  //...
  runTemplate.getRun().setText("", 0);
}

至此,我们持有了NiceXWPFDocument和XWPFRun对象,可以插入表格,合并单元格等操作。

自定义表格之动态处理已有表格

对于事先已知道部分表格样式,我们只需要处理剩余部分的表格可以采用此方式。

比如下图,我们在模板中设计好表格头和表格未的样式,表格中间的数据则可以动态处理。
image

  1. 定义如图的模板,在表格内输入模板元素{{table}}
  2. 通过registerPolicy设置此模板为自定义模板
  3. 新建MyTableRenderPolicy.java,继承DynamicTableRenderPolicy
public class MyTableRenderPolicy extends DynamicTableRenderPolicy {

  @Override
  public void render(XWPFTable table, Object data) {
      //table.getRow(1).getCell(0)
      //XWPFTableRow row = table.insertNewTableRow(1);
      //table.removeRow(1);
  }
}

至此,我们可以通过XWPFTable对象对表格进行删除行列、增加行列、设置文字等操作。

More

有时间的话,会对表格的API作一次优化。

  • 重新设计TableRenderData,对数据结构进行一次重构
  • 单元格文字样式处理,文字对齐处理。

运行时扫描Java注解(三)之java.lang.reflect包

java.lang.reflect包是在JDK1.1时引入的,主要提供了一些获取关于类、对象的反射信息的类和接口。通常,它与java.lang.Class一起使用。

反射并不神秘,本文对其使用作一个简单的介绍。

java.lang.Class类

JDK1.0时引入此类,1.1开始,引入了大量反射的方法。Class类封装一个对象和接口运行时的状态,当装载类时,Class类型的对象自动创建。

forName 通过类的字符串名加载该类

这是获取Class实例的方法之一,通过forName可能加载某一个Class对象,如果找不到该字符串表示的类类,则抛出ClassNotFoundException。如果记得java jdbc编程,最开始会有一行加载驱动的代码:

Class.forName("com.mysql.jdbc.Driver");

这行代码就会加载这个驱动类,并且执行了该类的static区块。

static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}

isInstance、instanceof、isAssignableFrom

isInstance和instanceof功能是一样的,后者是操作符,前者使用代码的方法解放了对操作符的依赖。
isInstance是针对实例的,判断是否是一个类或者其子类的实例。
isAssignableFrom是针对Class的,判断是否是和参数相同的类(接口)或者是其超类(接口)。

newInstance

除了new一个对象,我们可以使用newInstance创建一个对象。

//Class.java
public T newInstance() throws InstantiationException, IllegalAccessException
//Constructor.java
public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException

注解、枚举、数组、原生类型、Void

这些都代表着某种Class对象。

//注解是一种接口。
public boolean isAnnotation() {
    return (getModifiers() & ANNOTATION) != 0;
}
//枚举是一种类
public boolean isEnum() {
    // An enum must both directly extend java.lang.Enum and have
    // the ENUM bit set; classes for specialized enum constants
    // don't do the former.
    return (this.getModifiers() & ENUM) != 0 &&
    this.getSuperclass() == java.lang.Enum.class;
}
//每一个数组(相同元素个数,相同类型)都对应着一个Class
public native boolean isArray();
//每种原生类型对应着Class 
public native boolean isPrimitive();
//void也同样对应着Class

//输出
//interface com.deepoove.example.Time
//class com.deepoove.example.Day
//class [Ljava.io.File;
//int
//void
System.out.println(Time.class);//注解
System.out.println(Day.class);//枚举
System.out.println(new File[]{}.getClass());
System.out.println(int.class);
System.out.println(void.class);

java.lang.reflect一览

这个包包含了反射的类和接口。Field封装了属性,Method封装了方法、Constructor封装了构造器、Parameter封装了参数信息、ParameterizedType封装了泛型、Array提供操作数组的一系列静态方法、Proxy则提供了动态代理的功能、WildcardType代表通配符等。

----|- reflect
-------- AccessibleObject.java
-------- AnnotatedArrayType.java
-------- AnnotatedElement.java
-------- AnnotatedParameterizedType.java
-------- AnnotatedType.java
-------- AnnotatedTypeVariable.java
-------- AnnotatedWildcardType.java
-------- Array.java
-------- Constructor.java
-------- Executable.java
-------- Field.java
-------- GenericArrayType.java
-------- GenericDeclaration.java
-------- GenericSignatureFormatError.java
-------- InvocationHandler.java
-------- InvocationTargetException.java
-------- MalformedParameterizedTypeException.java
-------- MalformedParametersException.java
-------- Member.java
-------- Method.java
-------- Modifier.java
-------- Parameter.java
-------- ParameterizedType.java
-------- Proxy.java
-------- ReflectAccess.java
-------- ReflectPermission.java
-------- Type.java
-------- TypeVariable.java
-------- UndeclaredThrowableException.java
-------- WeakCache.java
-------- WildcardType.java

java.lang.reflect.Type类与泛型

Type是所有类型的父接口, 如原始类型(raw types 对应 Class)、 参数化类型(parameterized types 对应 ParameterizedType)、 数组类型(array types 对应 GenericArrayType)、 类型变量(type variables 对应 TypeVariable )和基本(原生)类型(primitive types 对应 Class)。

在基于泛型的编程中,我们有时候需要知道某个属性的泛型类型、某个方法参数的泛型类型、某个方法返回值的泛型类型,这些可以通过反射获取到:

field.getGenericType();
method.getGenericParameterTypes();
method.getGenericReturnType();
if(returnType instanceof ParameterizedType){
    ParameterizedType type = (ParameterizedType) returnType;
    Type[] typeArguments = type.getActualTypeArguments();
    for(Type typeArgument : typeArguments){
        Class typeArgClass = (Class) typeArgument;
        System.out.println("typeArgClass = " + typeArgClass);
    }
}

我们是否可以获取到一个对象的泛型类型?比如List list = new ArrayList();
能获取到泛型类型String吗?

答案是否定的,我们无法获取到String类型,因为类型擦除机制,String只是定义在变量声明的地方,在Class层面只能获取到泛型参数T或者E等变量。

访问控制权限:public、default、private

getMethod、getField方法默认只能获取到类或者父类中public的方法和属性,getDeclaredMethod、getDeclaredField则获取当前类声明的public、default、private方法和属性。

通过反射设置private属性值、调用private方法会抛出java.lang.IllegalAccessException。我们可以通过临时设置权限绕过默认的访问控制。

public void setAccessible(boolean flag) throws SecurityException

场景:运行时扫描Java注解

下面通过代码示例,运行时获取Java Runtime类型的注解。

//获取所有方法
public List<Member> getMethods(Class cls) {
    List<Member> methods = Lists.newArrayList();
    methods.addAll(Arrays.asList(cls.getDeclaredMethods()));
    methods.addAll(Arrays.asList(cls.getDeclaredConstructors()));
    return methods;
}

//获取类注解
aClass.getDeclaredAnnotations();

//获取属性注解
field.getDeclaredAnnotations()

//获取方法注解
nnotation[] annotations =
                method instanceof Method ? ((Method) method).getDeclaredAnnotations() :
                method instanceof Constructor ? ((Constructor) method).getDeclaredAnnotations() : null;

//获取参数注解
Annotation[][] annotations =
                method instanceof Method ? ((Method) method).getParameterAnnotations() :
                method instanceof Constructor ? ((Constructor) method).getParameterAnnotations() : null;

更多More

Guava reflection对反射提供了若干支持。

  • java中,泛型在运行时会被擦除。通过TypeToken可以获取真实泛型类型。
TypeToken<List<String>> stringListToken
  = new TypeToken<List<String>>() {};
TypeToken<List<Integer>> integerListToken
  = new TypeToken<List<Integer>>() {};
TypeToken<List<? extends Number>> numberTypeToken
  = new TypeToken<List<? extends Number>>() {};
 
assertFalse(stringListToken.isSubtypeOf(integerListToken));
assertFalse(numberTypeToken.isSubtypeOf(integerListToken));
assertTrue(integerListToken.isSubtypeOf(numberTypeToken));
  • Reflection.java 提供了生成动态代理的简便方法
 public static <T> T newProxy(Class<T> interfaceType, InvocationHandler handler) ;
  • Invokable:对MethodConstructor的封装,流式API
Method getMethod = List.class.getMethod("get", int.class);
Invokable<List<String>, ?> invokable = new TypeToken<List<String>>() {}.method(getMethod);
assertEquals(TypeToken.of(String.class), invokable.getReturnType()); // Not Object.class!
assertEquals(new TypeToken<List<String>>() {}, invokable.getOwnerType());

下一篇文章,我们将深入字节码技术,从字节码的角度看Class。

依赖注入(一)实现一个简单的DI框架

无论你对依赖注入这个概念是否了解,你或许早就驾轻就熟了。甚至当你在一个复杂项目中,因为没有使用依赖注入而感到烦劳(在我第一个Android项目中,因为没有使用依赖注入而感到彷徨无助)。

依赖注入并不是一门新的技术,也不是为了解决一个新的课题诞生的,它是一种模式,它告诉我们如何优雅的创建对象以及对象的依赖,姑且把这种模式想象成一个新的new操作符吧。

Think of Dependency Injection as the new new.

本系列文章分为三篇:

  • 第一篇介绍下依赖注入的背景以及规范,手动写一个实验性质的DI框架
  • 第二篇对Spring DI进行深度分析。
  • 第三篇对Guice DI进行深度分析。

为什么需要依赖注入

Java为我们提供了new操作符来创建对象。我们的代码可能看起来是这样的:

class Stopwatch {
  final TimeSource timeSource;
  Stopwatch () {
    timeSource = new AtomicClock(...);
  }
  void start() { ... }
  long stop() { ... }
}

在初始化Stopwatch类时,我们在构造器中创建对AtomicClock的依赖关系。

当TimeSource有多个实现时,为了扩展性,工厂模式可以很好的解决这个问题。

Stopwatch () {
    timeSource = DefaultTimeSource.getInstance();
  }

此时,工程师要做的就是维护好工厂的代码,由工厂来决定依赖关系。

那么这段代码该如何进行单元测试了?我们需要对工厂提供的对象进行Mock:

void testStopwatch() {
  TimeSource original = DefaultTimeSource.getInstance();
  DefaultTimeSource.setInstance(new MockTimeSource());
  try {
    // Now, we can actually test Stopwatch.
    Stopwatch sw = new Stopwatch();
    ...
  } finally {
    DefaultTimeSource.setInstance(original);
  }
}

Factory和Mock似乎解决了我们所有的问题,但是它会带来新的问题:

  1. 在单元测试阶段,我们必须小心翼翼的使用Mock,因为工厂保存了全局对象,如果没有清理掉Mock对象,那么工厂返回的实例都将是一个测试对象。
  2. 依赖关系隐藏在构造器内部实现中,依赖关系的隐藏为后续的维护会带来额外的开销,我们必须为构造器的代码中维护工厂和依赖的调用。
  3. 随着系统的复杂度,会出现大量的工厂。

为了解决以上问题,依赖注入模式应运而生了,它用容器替代了工厂,我们不要再新建工厂类,工程师只需要处理依赖关系即可,对象的创建Constructor、对象之间的装配dependency和对象的销毁destroy都交由容器来处理,所以依赖注入又被称为控制反转

依赖注入即控制反转,Service Locator则提供了另一种解耦的模式(本文不作探讨,https://www.martinfowler.com/articles/injection.html)。

控制反转或者依赖注入其实遵循了以下两个法则:

  • 好莱坞法则:不要打电话给我们,我们会打给你
    “我们”就可以理解为容器,打电话的时机交由容器来控制。

  • 迪米特法则(最少知识原则):不要和陌生人说话
    对象应该依赖接口,而不是具体实现,对其它对象要尽可能少的了解。

JSR 330: Dependency Injection for Java

JSR 330定义了Java实现依赖注入的规范,javax.inject包指定了以可重用性,可测试性和可维护性的方式获取对象的方法。
image

  1. @Inject
    标识可注入的构造器、域、方法,通过此注解实现依赖的注入,类似于Spring的@Autowired
public class Car {
  // Injectable constructor
  @Inject public Car(Engine engine) { ... }

  // Injectable field
  @Inject private Provider<Seat> seatProvider;

  // Injectable package-private method
  @Inject void install(Windshield windshield, Trunk trunk) { ... }
}

实际使用依赖注入,还会发现更多的问题,比如当Engine有多个实现的时候,该注入哪一个实现呢?规范提供了@Qualifier@Named来解决这类问题。

  1. @Qualifier
    通过别名标识具体实现。可以实现一个新的以 @qualifier, @retention(RUNTIME), @documented来注释的注解,如@bmw,这个新的注解将和具体的依赖接口共同标识具体的实现类。
@java.lang.annotation.Documented
@java.lang.annotation.Retention(RUNTIME)
@javax.inject.Qualifier
public @interface BMW {
}

// 注入宝马发动机
@Inject public Car( @BMW Engine engine) { ... }
  1. @Named
    通过基于字符串名称的别名标识具体实现。@Qualifier的方式需要实现新的注解,而@nAmed提供了基于字符串和依赖接口确定具体实现的方式。
@Inject public Car( @Named("BMW") Engine engine) { ... }
  1. @Scope
    标识对象的会话周期。可以实现一个新的以 @scope, @retention(RUNTIME), @documented来注释的注解,这个新的注解定义了对象会话的范围。
    默认提供了@singleton注解标识对象是单例的。

  2. @Singleton
    单例对象,应用中只存在一个对象。

  3. Provider<T>
    提供T的实例,定义了提供具体实现类对象的抽象接口。

public interface Provider<T> {
  T get();
}

实现一个DI框架

接下来将会基于JSR330实现一个DI框架,注入方式为Field注入,命名为lite-inject。

  1. Injector接口提供一个门面:统一获得具体Bean对象
    通过接口类型获得实现类的实例,或者通过@Named的字符串值和接口类型获取实现类的实例。
package com.deepoove.liteinject;

import com.deepoove.liteinject.exception.NoSuchInstanceException;

public interface Injector {

  <T> T getInstance(Class<T> clazz) throws NoSuchInstanceException;

  <T> T getInstance(String name, Class<T> clazz) throws NoSuchInstanceException;

}

在暴露了门面后,Injector的实现DefaultInjector内部定义了容器数据结构,其中Key、Value都是一些绑定信息,获取实例的流程就是通过Key获取Value,再通过Value获得实例或者初始化实例。

private LinkedHashMap<Key<?>, Value> container = new LinkedHashMap<>();

Key包含了三个属性,来制定具体待绑定的类型:

private Class<T> type;
private String named;
private Class<? extends Annotation> qualifierType;

Value包含了实例化的策略和绑定的实现类:

private Class<?> type;
private Class<? extends Provider<?>> provider;

private Object instance;
private boolean singleton;
  1. 暴露绑定的配置:Key到Value的绑定
    任何实现InjectModule的类实现cofigure方法,通过bind方法开始配置对象实例化的方式,所以初始化容器DefaultInjector的步骤,就是对Module里面绑定的Key和Value进行解析,生成数据存储到container对象中。
package com.deepoove.liteinject;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

public abstract class InjectModule {

  public abstract void cofigure();

  public <T> Binder<T> bind(Class<T> clazz) {
    Binder<T> binder = new Binder<>(Key.get(clazz));
    binders.add(binder);
    return binder;
  }
}
  1. 绑定Binder的具体功能
    可以绑定某个接口到实现,也可以绑定别名。
package com.deepoove.liteinject;

import java.lang.annotation.Annotation;

import javax.inject.Provider;
import javax.inject.Scope;
import javax.inject.Singleton;

public class Binder<T> {

  private Key<T> key;
  private Value value;

  // 绑定类型
  public Binder(Key<T> key) {
    this.key = key;
    value = new Value();
  }

  // @Named别名
  public Binder<T> named(String name) {
    key.setNamed(name);
    return this;
  }

  // @Qualifier
  public Binder<T> annotatedWith(Class<? extends Annotation> annotationType) {
    key.setQualifierType(annotationType);
    return this;
  }

  // 绑定实现类
  public Binder<T> to(Class<? extends T> clazz) {
    value.setType(clazz);
    return this;
  }

  // 绑定Provider实例化方式
  public Binder<T> toProvider(Class<? extends Provider<T>> clazz) {
    value.setProvider(clazz);
    return this;
  }

  // 是否单例
  public Binder<T> inScope(Class<? extends Annotation> clazz) {
    value.setSingleton(Singleton.class.equals(clazz));
    return this;
  }
}
  1. 获取Bean
    通过Key获取Value值,进而获得实例化Bean的策略,实例化的原理是通过Java反射,因为是通过Field注入,所以构造一个对象可以简单的使用Class.newInstance(我们姑且认为所有的类都有一个空的构造方法)。
if (null != provider) {
  return provider.newInstance().get();
} else {
  return type.newInstance();
}

在实例化对象后,就可以考虑通过反射注入域了:

void inject(Key key, Object obj) {
  if (currentlyInCreation.contains(key)) throw new RuntimeException("BeanCurrentlyInCreationException:" + key);
  currentlyInCreation.add(key);
  Class<?> type = obj.getClass();
  Field[] declaredFields = type.getDeclaredFields();
  for (Field field : declaredFields) {
    Inject annotation = field.getAnnotation(Inject.class);
    Named namedAnnotation = field.getAnnotation(Named.class);
    if (null != annotation) {
      field.setAccessible(true);
      try {
        field.set(obj, null == namedAnnotation
            ? this.getInstance(Key.get(field.getType()))
            : this.getInstance(Key.get(namedAnnotation.value(), field.getType())));
      } catch (IllegalArgumentException | IllegalAccessException
          | NoSuchInstanceException e) {
        e.printStackTrace();
      }
    }
  }
  currentlyInCreation.remove(key);
}

currentlyInCreation是为了抛出循环依赖的问题,所谓循坏依赖,就是A的初始化需要依赖B,B的初始化也需要A,这样就产生了一个无限循环。关于如何解决循环依赖的问题,将在后续文章中详细介绍。

至此,一个实验性质的DI框架已经完成了,我们可以写个单元测试验证下依赖注入的功能:

@Test
public void testInject() {
  Injector injector = new DefaultInjectorCreator().addModule(new InjectModule() {
            
            @Override
            public void cofigure() {
              bind(UserService.class).named("lite").toProvider(UserServiceProvider.class);
              bind(UserService.class).to(UserServiceImpl.class);
              bind(LoginServiceImpl.class).to(LoginServiceImpl.class);
              bind(LoginService.class).to(LoginServiceImpl.class).inScope(Singleton.class);
            }
        }).build();
  UserService accountService = injector.getInstance("lite", UserService.class);
  Assert.assertTrue(accountService.get("").getName().equals("Sayi"));

  UserService userService = injector.getInstance(UserService.class);
  Assert.assertTrue(userService.get("").getName() == null);

  LoginService loginService1 = injector.getInstance(LoginService.class);
  LoginService loginService2 = injector.getInstance(LoginService.class);
  Assert.assertTrue(loginService1 == loginService2);

}

Dependency Injection

一个成熟的DI框架支持的注入方式通常有三种:构造器注入、方法注入和域注入,在Bean的生命周期中,提供优雅的扩展点,同时解决了循环依赖的问题,这类优秀的框架有Spring、Guice等,待续。

并发(七)线程池

本文介绍如何结构化执行任务、线程池技术和Future的设计。

任务和线程执行分离 :永远不推荐手动调用thread.start()方法去启动线程,线程的执行应该统一去管理,开发者仅仅提供任务的实现即可。

Executor

Executor是一个定义了如何提交任务的接口,任务通过Runnable接口表示。

public interface Executor {
  void execute(Runnable command);
}

我们来看看Executor的类继承关系图:

image

ExecutorService

ExecutorService继承了Executor接口,实现了更多的功能,shutdown方法可以禁止继续提交任务去执行,已经提交的任务还可以继续执行,shutdownNow方法会尝试停止已经提交的任务,invokeAll和invokeAny支持批量执行任务,invokeAny方法当有一个任务成功执行结束时会返回这个任务执行的结果。

相比于Executor提交任务的方式,ExecutorService提供了更多的方式,即一系列重载submit的方法:

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

这三个方法都可以获取异步任务执行的结果,而Future就是为了获取异步结果而设计的。

image

Future有两个很重要的方法,get()方法可以阻塞当前线程,直到获取到异步任务执行结束后返回的结果,cancle(boolean)方法可以取消一个提交的任务。

设计: Runnable是一个没有返回值的任务,Callable是JDK1.5新增的有返回值的任务,在设计层面,我们可以把Runnable看成是一个返回NULL的Callable,代码统一通过Callable.call()来运行,所以需要一个适配器类将Runnable接口适配成Callable接口。JDK的Executors类提供了这样的适配方法:

public static <T> Callable<T> callable(Runnable task, T result) {
  if (task == null)
    throw new NullPointerException();
  return new RunnableAdapter<T>(task, result);
}
public static Callable<Object> callable(Runnable task) {
  if (task == null)
    throw new NullPointerException();
  return new RunnableAdapter<Object>(task, null);
}
static final class RunnableAdapter<T> implements Callable<T> {
  final Runnable task;
  final T result;
  RunnableAdapter(Runnable task, T result) {
    this.task = task;
    this.result = result;
  }
  public T call() {
    task.run();
    return result;
  }
}

AbstractExecutorService

AbstractExecutorService是一个抽象实现,我们重点看下submit方法的源码实现:

public Future<?> submit(Runnable task) {
  if (task == null) throw new NullPointerException();
  RunnableFuture<Void> ftask = newTaskFor(task, null);
  execute(ftask);
  return ftask;
}

public <T> Future<T> submit(Runnable task, T result) {
  if (task == null) throw new NullPointerException();
  RunnableFuture<T> ftask = newTaskFor(task, result);
  execute(ftask);
  return ftask;
}

public <T> Future<T> submit(Callable<T> task) {
  if (task == null) throw new NullPointerException();
  RunnableFuture<T> ftask = newTaskFor(task);
  execute(ftask);
  return ftask;
}

任务task和Future返回值都封装成了同一个对象RunnableFuture,它既是一个可执行的任务,也代表了一个Future对象,获取异步任务执行的结果,继承了Runnable和Future接口:

public interface RunnableFuture<V> extends Runnable, Future<V> {
  void run();
}

execute(ftask)方法留作子类实现,我们进入newTaskFor方法:

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
  return new FutureTask<T>(runnable, value);
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
  return new FutureTask<T>(callable);
}

newTaskFor被修饰为protected,它可以被子类重写,子类可以重写这两个方法来定义自己的RunnableFuture实现 ,FutureTask是默认实现,在ThreadPoolExecutor原理中我们会深入分析其代码。

ThreadPoolExecutor

ThreadPoolExecutor是抽象类AbstractExecutorService的实现,它实现了最重要的方法public void execute(Runnable command)

ThreadPoolExecutor支持提交一系列任务,并且支持有限个或者无限个工作线程去执行任务,这些工作线程的管理和执行我们称之为:线程池技术。如果线程池是有限的,那么多余的任务将会排队,等待被执行。

线程池里面的工作线程是可以被复用的,当某个任务来了之后可以交给已经创建好的线程执行,而不用再去创建线程。当没有任何任务的时候,这些工作线程将会被阻塞,所以排队的队列是一个阻塞队列BlockingQueue。

线程池里面的工作线程也是可以被销毁的,尤其当线程个数太多而没有继续提交任务时,销毁线程可以释放资源。

初始化

我们先来看看ThreadPoolExecutor的构造函数,它提供了线程池的一些配置参数:

/**
 * Creates a new {@code ThreadPoolExecutor} with the given initial
 * parameters.
 *
 * @param corePoolSize the number of threads to keep in the pool, even
 *    if they are idle, unless {@code allowCoreThreadTimeOut} is set
 * @param maximumPoolSize the maximum number of threads to allow in the
 *    pool
 * @param keepAliveTime when the number of threads is greater than
 *    the core, this is the maximum time that excess idle threads
 *    will wait for new tasks before terminating.
 * @param unit the time unit for the {@code keepAliveTime} argument
 * @param workQueue the queue to use for holding tasks before they are
 *    executed.  This queue will hold only the {@code Runnable}
 *    tasks submitted by the {@code execute} method.
 * @param threadFactory the factory to use when the executor
 *    creates a new thread
 * @param handler the handler to use when execution is blocked
 *    because the thread bounds and queue capacities are reached
 * @throws IllegalArgumentException if one of the following holds:<br>
 *     {@code corePoolSize < 0}<br>
 *     {@code keepAliveTime < 0}<br>
 *     {@code maximumPoolSize <= 0}<br>
 *     {@code maximumPoolSize < corePoolSize}
 * @throws NullPointerException if {@code workQueue}
 *     or {@code threadFactory} or {@code handler} is null
 */
public ThreadPoolExecutor(int corePoolSize,
              int maximumPoolSize,
              long keepAliveTime,
              TimeUnit unit,
              BlockingQueue<Runnable> workQueue,
              ThreadFactory threadFactory,
              RejectedExecutionHandler handler) {
  if (corePoolSize < 0 ||
    maximumPoolSize <= 0 ||
    maximumPoolSize < corePoolSize ||
    keepAliveTime < 0)
    throw new IllegalArgumentException();
  if (workQueue == null || threadFactory == null || handler == null)
    throw new NullPointerException();
  this.corePoolSize = corePoolSize;
  this.maximumPoolSize = maximumPoolSize;
  this.workQueue = workQueue;
  this.keepAliveTime = unit.toNanos(keepAliveTime);
  this.threadFactory = threadFactory;
  this.handler = handler;
}

理解每个参数的含义,才能真正用好线程池,才能有资格使用线程池。

  1. corePoolSize 核心工作线程数
    线程池的初始工作线程个数为0,当提交任务后,就会创建工作线程,最多创建corePoolSize个工作线程,后续提交的任务才会进入队列排队而不会去创建线程

  2. maximumPoolSize 最大工作线程数
    当排队失败,无法入队时(比如到达队列容量上限) ,就会创建最大不超过maximumPoolSize大小的工作线程,否则会被丢弃,丢弃策略见第6点。

  3. keepAliveTime & unit 额外工作线程允许空闲时间

  • 超过corePoolSize且小于maximumPoolSize这部分数量的工作线程在空闲了指定keepAliveTime时间后,将会被销毁。销毁哪些工作线程不是根据创建顺序,而是根据空闲时间,所以可能一个工作线程一直被复用,也有可能被复用后又被销毁

  • 如果一直没有任务提交,那么工作线程个数将会维持在corePoolSize个。核心数目的工作线程也可以指定空闲时间后被销毁,开关方法为:allowsCoreThreadTimeOut()

  • 如果maximumPoolSize和corePoolSize相同,并且没有允许核心数目线程可以被销毁,那么keepAliveTime将毫无意义。

  1. workQueue 任务阻塞队列
    任务排队的BlockingQueue,它可以指定队列的容量,比如new LinkedBlockingQueue()表示可以提交无限的任务。

  2. threadFactory 创建工作线程的工厂,下文会看到默认的实现

  3. handler 当线程个数和阻塞队列容量到达边界,无法处理任务时候的策略

  • ThreadPoolExecutor.AbortPolicy:是JDK默认策略,提交任务时将会抛出一个RejectedExecutionException异常
  • ThreadPoolExecutor.DiscardPolicy:直接被丢弃,不作任何处理
  • ThreadPoolExecutor.DiscardOldestPolicy:队列第一个元素将会被丢弃
  • ThreadPoolExecutor.CallerRunsPolicy:提交任务的线程自己执行这个任务

原理:execute()方法

我们知道初始化参数的含义,其实已经弄清楚了原理,我们直接看execute()方法源码:

/**
 * Executes the given task sometime in the future.  The task
 * may execute in a new thread or in an existing pooled thread.
 *
 * If the task cannot be submitted for execution, either because this
 * executor has been shutdown or because its capacity has been reached,
 * the task is handled by the current {@code RejectedExecutionHandler}.
 *
 */
public void execute(Runnable command) {
  if (command == null)
    throw new NullPointerException();
  /*
   * Proceed in 3 steps:
   *
   * 1. If fewer than corePoolSize threads are running, try to
   * start a new thread with the given command as its first
   * task.  The call to addWorker atomically checks runState and
   * workerCount, and so prevents false alarms that would add
   * threads when it shouldn't, by returning false.
   *
   * 2. If a task can be successfully queued, then we still need
   * to double-check whether we should have added a thread
   * (because existing ones died since last checking) or that
   * the pool shut down since entry into this method. So we
   * recheck state and if necessary roll back the enqueuing if
   * stopped, or start a new thread if there are none.
   *
   * 3. If we cannot queue task, then we try to add a new
   * thread.  If it fails, we know we are shut down or saturated
   * and so reject the task.
   */
  int c = ctl.get();
  if (workerCountOf(c) < corePoolSize) {
    if (addWorker(command, true))
      return;
    c = ctl.get();
  }
  if (isRunning(c) && workQueue.offer(command)) {
    int recheck = ctl.get();
    if (! isRunning(recheck) && remove(command))
      reject(command);
    else if (workerCountOf(recheck) == 0)
      addWorker(null, false);
  }
  else if (!addWorker(command, false))
    reject(command);
}

ctl是一个原子整型变量,采用位操作用一个整型变量记录了线程池的运行状态runState和工作线程数量workerCount。workerCountOf(c)方法就是具体的位操作获得工作线程数:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }

execute()方法从注释中或者通过一个if语句和一个if-else语句可以看出,做了三步事:

  1. if (workerCountOf(c) < corePoolSize)如果当前工作线程小于核心线程,就会通过addWorker(command, true)直接创建线程执行任务,这个任务就是这个工作线程的第一个任务。
  2. 第一步当中创建工作线程失败(比如工厂创建线程失败或者因为某些异常),或者工作线程大于核心线程数,就会入队workQueue.offer(command),入队后会再次检查线程池状态,如果线程池不是运行状态,则出队且丢弃任务,如果是运行状态且当前线程池工作线程个数为0,则addWorker(null, false)创建一个空任务的工作线程。
  3. 如果入队失败,或者线程池不在运行状态,就会尝试创建工作线程,如果当前工作线程已经达到maximumPoolSize个数上限或者线程池不在运行状态就会创建失败,丢弃任务,这里判断线程池不在运行状态代码有点重复了。

接下来,我们深入addWork方法(用来创建带有第一个执行任务的工作线程):

private boolean addWorker(Runnable firstTask, boolean core) {
  retry:
  for (;;) {
    int c = ctl.get();
    int rs = runStateOf(c);

    // Check if queue empty only if necessary.
    if (rs >= SHUTDOWN &&
      ! (rs == SHUTDOWN &&
         firstTask == null &&
         ! workQueue.isEmpty()))
      return false;

    for (;;) {
      int wc = workerCountOf(c);
      if (wc >= CAPACITY ||
        wc >= (core ? corePoolSize : maximumPoolSize))
        return false;
      if (compareAndIncrementWorkerCount(c))
        break retry;
      c = ctl.get();  // Re-read ctl
      if (runStateOf(c) != rs)
        continue retry;
      // else CAS failed due to workerCount change; retry inner loop
    }
  }

  boolean workerStarted = false;
  boolean workerAdded = false;
  Worker w = null;
  try {
    w = new Worker(firstTask);
    final Thread t = w.thread;
    if (t != null) {
      final ReentrantLock mainLock = this.mainLock;
      mainLock.lock();
      try {
        // Recheck while holding lock.
        // Back out on ThreadFactory failure or if
        // shut down before lock acquired.
        int rs = runStateOf(ctl.get());

        if (rs < SHUTDOWN ||
          (rs == SHUTDOWN && firstTask == null)) {
          if (t.isAlive()) // precheck that t is startable
            throw new IllegalThreadStateException();
          workers.add(w);
          int s = workers.size();
          if (s > largestPoolSize)
            largestPoolSize = s;
          workerAdded = true;
        }
      } finally {
        mainLock.unlock();
      }
      if (workerAdded) {
        t.start();
        workerStarted = true;
      }
    }
  } finally {
    if (! workerStarted)
      addWorkerFailed(w);
  }
  return workerStarted;
}

这段代码首先会判断线程池运行状态,然后根据参数boolean core来决定是要判断是比较核心线程数还是最大工作线程数:

if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
    return false;

接着会尝试将工作线程数加一compareAndIncrementWorkerCount(c),如果成功累加,则会创建线程并启动,这时候创建线程有可能会失败,则调用addWorkerFailed(w)方法。创建线程的代码封装在Worker类中,如下面这段缩减了多余代码所示:

Worker w = null;
w = new Worker(firstTask);
final Thread t = w.thread;
t.start();

Worker类封装了一个线程thread,通过这个线程来启动执行任务,Worker就是整个工作线程的核心,也是最难理解的一部分。

Worker工作线程

Worker类包含了第一个任务firstTask和一个线程对象thread,Worker类的thread启动后,会拿第一个任务执行,然后循环从阻塞队列中take任务执行,如果当前线程超过核心工作线程数,那么当take任务阻塞时间超过keepAliveTime后,就会返回NULL,进而销毁这个线程。

Worker类

private final class Worker
  extends AbstractQueuedSynchronizer
  implements Runnable
{
  /**
   * This class will never be serialized, but we provide a
   * serialVersionUID to suppress a javac warning.
   */
  private static final long serialVersionUID = 6138294804551838833L;

  /** Thread this worker is running in.  Null if factory fails. */
  final Thread thread;
  /** Initial task to run.  Possibly null. */
  Runnable firstTask;
  /** Per-thread task counter */
  volatile long completedTasks;

  /**
   * Creates with given first task and thread from ThreadFactory.
   * @param firstTask the first task (null if none)
   */
  Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
  }

  /** Delegates main run loop to outer runWorker  */
  public void run() {
    runWorker(this);
  }

  // Lock methods
  //
  // The value 0 represents the unlocked state.
  // The value 1 represents the locked state.

  protected boolean isHeldExclusively() {
    return getState() != 0;
  }

  protected boolean tryAcquire(int unused) {
    if (compareAndSetState(0, 1)) {
      setExclusiveOwnerThread(Thread.currentThread());
      return true;
    }
    return false;
  }

  protected boolean tryRelease(int unused) {
    setExclusiveOwnerThread(null);
    setState(0);
    return true;
  }

  public void lock()    { acquire(1); }
  public boolean tryLock()  { return tryAcquire(1); }
  public void unlock()    { release(1); }
  public boolean isLocked() { return isHeldExclusively(); }

  void interruptIfStarted() {
    Thread t;
    if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
      try {
        t.interrupt();
      } catch (SecurityException ignore) {
      }
    }
  }
}

上面是Worker的源码,我们来一点点分解来看,我们先来看看定义:

private final class Worker extends AbstractQueuedSynchronizer implements Runnable

Worker继承了AQS类(下文会提到用法),实现了Runnable接口。接着看构造器,初始化了一个任务和使用线程工厂创建一个线程:

Worker(Runnable firstTask) {
  setState(-1); // inhibit interrupts until runWorker
  this.firstTask = firstTask;
  this.thread = getThreadFactory().newThread(this);
}

创建线程的任务是当前Worker对象,我们接着看Worker.run方法:

/** Delegates main run loop to outer runWorker  */
public void run() {
  runWorker(this);
}

工作线程循环执行:runWorker

其中runWorker(this)方法就是循环从队列获取任务进行执行:

final void runWorker(Worker w) {
  Thread wt = Thread.currentThread();
  Runnable task = w.firstTask;
  w.firstTask = null;
  w.unlock(); // allow interrupts
  boolean completedAbruptly = true;
  try {
    while (task != null || (task = getTask()) != null) {
      w.lock();
      // If pool is stopping, ensure thread is interrupted;
      // if not, ensure thread is not interrupted.  This
      // requires a recheck in second case to deal with
      // shutdownNow race while clearing interrupt
      if ((runStateAtLeast(ctl.get(), STOP) ||
         (Thread.interrupted() &&
          runStateAtLeast(ctl.get(), STOP))) &&
        !wt.isInterrupted())
        wt.interrupt();
      try {
        beforeExecute(wt, task);
        Throwable thrown = null;
        try {
          task.run();
        } catch (RuntimeException x) {
          thrown = x; throw x;
        } catch (Error x) {
          thrown = x; throw x;
        } catch (Throwable x) {
          thrown = x; throw new Error(x);
        } finally {
          afterExecute(task, thrown);
        }
      } finally {
        task = null;
        w.completedTasks++;
        w.unlock();
      }
    }
    completedAbruptly = false;
  } finally {
    processWorkerExit(w, completedAbruptly);
  }
}
  • runWorker方法可以看到,在线程执行前后提供了两个钩子方法:beforeExecuteafterExecute,下文会再次提到

  • Worker实现了AQS类,在任务执行前后加了不可重入的排它锁,为什么实现AQS,接着看下文中

w.lock();
...
w.unlock();

runWorker方法中从阻塞队列取任务的核心代码是:while (task != null || (task = getTask()) != null),当getTask方法返回NULL时就会退出循环,最终执行线程销毁工作processWorkerExit(w, completedAbruptly),那么什么时候返回NULL呢?

阻塞队列获取任务:getTask

我们进入getTask方法:

private Runnable getTask() {
  boolean timedOut = false; // Did the last poll() time out?

  for (;;) {
    int c = ctl.get();
    int rs = runStateOf(c);

    // Check if queue empty only if necessary.
    if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
      decrementWorkerCount();
      return null;
    }

    int wc = workerCountOf(c);

    // Are workers subject to culling?
    boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

    if ((wc > maximumPoolSize || (timed && timedOut))
      && (wc > 1 || workQueue.isEmpty())) {
      if (compareAndDecrementWorkerCount(c))
        return null;
      continue;
    }

    try {
      Runnable r = timed ?
        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
        workQueue.take();
      if (r != null)
        return r;
      timedOut = true;
    } catch (InterruptedException retry) {
      timedOut = false;
    }
  }
}

getTask方法的核心作用就是取任务元素,还有一个作用就是当allowCoreThreadTimeOut==true 或者核心线程数超过corePoolSize的时候返回NULL,进入线程销毁流程。

NOTE:如果符合销毁条件,是否一个工作线程会在keepAliveTime时间内一定被销毁呢?
不一定,因为workQueue.poll或者workQueue.take都会抛出中断异常,所以他们是可中断的,中断后又会接着执行for循环重新阻塞获取任务,比如ThreadPoolExecutor.setCorePoolSize()方法就会导致工作线程中断,因为配置被更改,需要中断这些线程来更新配置。

NOTE:我们现在就可以来解释为什么Worker需要实现AQS这个问题?
原因一 是不希望正在执行任务的工作线程被中断,阻塞住的线程可以被中断,我们来看看setCorePoolSize方法调用的中断线程的代码:

private void interruptIdleWorkers(boolean onlyOne) {
  final ReentrantLock mainLock = this.mainLock;
  mainLock.lock();
  try {
    for (Worker w : workers) {
      Thread t = w.thread;
      if (!t.isInterrupted() && w.tryLock()) {
        try {
          t.interrupt();
        } catch (SecurityException ignore) {
        } finally {
          w.unlock();
        }
      }
      if (onlyOne)
        break;
    }
  } finally {
    mainLock.unlock();
  }
}

当我们更改了线程池的配置时,如果尝试获取锁成功w.tryLock(),那些阻塞住的线程会被中断(所以执行任务的前后都会加锁和释放锁),然后更新配置,重新阻塞。

原因二 是我们可以通过isLock()方法来判断哪些工作线程是在执行任务,哪些工作线程是在阻塞等待。ThreadPoolExecutor.getActiveCount()方法的源码就是这个原理。

FutureTask作为一个Task:执行

综上,我们知道了线程池如何创建线程,对任务进行排队,从阻塞队列取线程以及销毁线程的原理,通过AbstractExecutorService我们也知道,这些Runnable任务其实一个FutureTasK类型,接下来我们深入FutureTask的实现。

public class FutureTask<V> implements RunnableFuture<V> {

  public FutureTask(Callable<V> callable) {
    if (callable == null)
      throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;     // ensure visibility of callable
  }
  public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;     // ensure visibility of callable
  }
  public void run() {
    if (state != NEW ||
      !UNSAFE.compareAndSwapObject(this, runnerOffset,
                     null, Thread.currentThread()))
      return;
    try {
      Callable<V> c = callable;
      if (c != null && state == NEW) {
        V result;
        boolean ran;
        try {
          result = c.call();
          ran = true;
        } catch (Throwable ex) {
          result = null;
          ran = false;
          setException(ex);
        }
        if (ran)
          set(result);
      }
    } finally {
      // runner must be non-null until state is settled to
      // prevent concurrent calls to run()
      runner = null;
      // state must be re-read after nulling runner to prevent
      // leaked interrupts
      int s = state;
      if (s >= INTERRUPTING)
        handlePossibleCancellationInterrupt(s);
    }
  }
  // 略
}

FutureTask作为Runnable属性就是执行任务,然后将结果保存,具体方法是set(result):

protected void set(V v) {
  if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
    outcome = v;
    UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
    finishCompletion();
  }
}

set(V)方法会将当前任务的执行结果标记为COMPLETING,然后通过finishCompletion()方法唤醒那些通过Future.get获取异步结果的线程。

private void finishCompletion() {
  // assert state > COMPLETING;
  for (WaitNode q; (q = waiters) != null;) {
    if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
      for (;;) {
        Thread t = q.thread;
        if (t != null) {
          q.thread = null;
          LockSupport.unpark(t);
        }
        WaitNode next = q.next;
        if (next == null)
          break;
        q.next = null; // unlink to help gc
        q = next;
      }
      break;
    }
  }
  done();
  callable = null;    // to reduce footprint
}

waiters是一个等待队列的链表,LockSupport.unpark(t)执行了唤醒操作,接下来我们就来看看Future.get的源码。

FutureTask作为一个Future

Future.get原理

Future.get首先都会判断FutureTask的状态标记是否为COMPLETING

public V get() throws InterruptedException, ExecutionException {
  int s = state;
  if (s <= COMPLETING)
    s = awaitDone(false, 0L);
  return report(s);
}

awaitDones就是将当前线程加入等待队列的代码,这段代码相当有技巧性,通过一个for循环不断来处理每个if-else语句:

private int awaitDone(boolean timed, long nanos)
  throws InterruptedException {
  final long deadline = timed ? System.nanoTime() + nanos : 0L;
  WaitNode q = null;
  boolean queued = false;
  for (;;) {
    if (Thread.interrupted()) {
      removeWaiter(q);
      throw new InterruptedException();
    }

    int s = state;
    if (s > COMPLETING) {
      if (q != null)
        q.thread = null;
      return s;
    }
    else if (s == COMPLETING) // cannot time out yet
      Thread.yield();
    else if (q == null)
      q = new WaitNode();
    else if (!queued)
      queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                         q.next = waiters, q);
    else if (timed) {
      nanos = deadline - System.nanoTime();
      if (nanos <= 0L) {
        removeWaiter(q);
        return state;
      }
      LockSupport.parkNanos(this, nanos);
    }
    else
      LockSupport.park(this);
  }
}

首先会创建一个节点q = new WaitNode();,然后将节点加入等待队列queued = UNSAFE.compareAndSwapObject(this, waitersOffset,q.next = waiters, q);,最后都会通过LockSupport.park阻塞当前线程,直到任务执行结束或者被中断。

report(s)的代码相对简单:

private V report(int s) throws ExecutionException {
  Object x = outcome;
  if (s == NORMAL)
    return (V)x;
  if (s >= CANCELLED)
    throw new CancellationException();
  throw new ExecutionException((Throwable)x);
}

其中有个判断标记是否大于CANCELLED,这些状态有:

private static final int CANCELLED    = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED  = 6;

Future.cancel原理

public boolean cancel(boolean mayInterruptIfRunning) {
  if (!(state == NEW &&
      UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
        mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
    return false;
  try {  // in case call to interrupt throws exception
    if (mayInterruptIfRunning) {
      try {
        Thread t = runner;
        if (t != null)
          t.interrupt();
      } finally { // final state
        UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
      }
    }
  } finally {
    finishCompletion();
  }
  return true;
}

其中runner变量表示当前工作的线程,在FutureTask.run()执行中会设置。

cancel的原理就是如果mayInterruptIfRunning是true,则会中断工作线程,最终都会设置任务执行状态的标记为:INTERRUPTEDINTERRUPTING 或者 CANCELLED,这样在get时候就会如同report(s)方法所示抛出CancellationException。

问题:一个FutureTask执行了cancle(true),那么这个任务是立即终止的吗?
答案是不一定。因为即使参数是true,调用线程的t.interrupt()方法,也只是设置了中断标记而已,或者让wait、sleep等方法抛出中断异常,任务中断逻辑还是需要自己代码实现

Hook钩子

线程池提供了三个钩子,这些钩子可以用来监控和观察线程执行情况。

protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }
protected void terminated() { }

ThreadFactory

JDK提供了创建工作线程工厂的默认实现:Executors.defaultThreadFactory()

static class DefaultThreadFactory implements ThreadFactory {
  private static final AtomicInteger poolNumber = new AtomicInteger(1);
  private final ThreadGroup group;
  private final AtomicInteger threadNumber = new AtomicInteger(1);
  private final String namePrefix;

  DefaultThreadFactory() {
    SecurityManager s = System.getSecurityManager();
    group = (s != null) ? s.getThreadGroup() :
                Thread.currentThread().getThreadGroup();
    namePrefix = "pool-" +
            poolNumber.getAndIncrement() +
           "-thread-";
  }

  public Thread newThread(Runnable r) {
    Thread t = new Thread(group, r,
                namePrefix + threadNumber.getAndIncrement(),
                0);
    if (t.isDaemon())
      t.setDaemon(false);
    if (t.getPriority() != Thread.NORM_PRIORITY)
      t.setPriority(Thread.NORM_PRIORITY);
    return t;
  }
}

BlockingQueue排队阻塞队列:workQueue

阻塞队列通常有三种策略可以选择:

1. 无界阻塞队列UnBounded queues

通常使用LinkedBlockingQueue,当工作线程达到corePoolSize后,所有新来的任务都会进入阻塞队列,所以 无界阻塞队列导致maximumPoolSize参数无效,在使用无界队列时,我们必须考虑到无界会带来的资源衰竭内存耗尽问题。

2. 有界阻塞队列Bounded queues

可以有效防止资源耗尽,但是我们必须在队列容量和线程池的大小上作一个很好的权衡,从而提高效率。同时,我们必须考虑到线程个数和容量到达零界点时丢弃任务的策略。

3. 直接传递Direct handoffs

不保存任何元素的阻塞队列,仅仅是将任务传递给工作线程,比如SynchronousQueue。通常我们使用无界的maximumPoolSizes来创建工作线程以防止任务丢弃,所以我们必须注意过多的线程带来的系统瓶颈。

ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor是一个计划执行任务的线程池,它继承ThreadPoolExecutor类,实现了ScheduledExecutorService接口。

ScheduledExecutorService

ScheduledExecutorService定义了一系列计划执行的方法,包括隔一段时间执行。

public interface ScheduledExecutorService extends ExecutorService {

  public ScheduledFuture<?> schedule(Runnable command,
                     long delay, TimeUnit unit);

  public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                       long delay, TimeUnit unit);

  public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                          long initialDelay,
                          long period,
                          TimeUnit unit);
                          
  public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                           long initialDelay,
                           long delay,
                           TimeUnit unit);

}

初始化

我们直接看构造函数,它调用了ThreadPoolExecutor的构造方法,初始了一个最大线程个数为最大值,阻塞队列为DelayedWorkQueue的线程池。

public ScheduledThreadPoolExecutor(int corePoolSize,
                   ThreadFactory threadFactory,
                   RejectedExecutionHandler handler) {
  super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
      new DelayedWorkQueue(), threadFactory, handler);
}

DelayedWorkQueue是内部基于堆实现的一个可排序阻塞队列。

ScheduledFutureTask

这里所有的任务不是FutureTask,而是ScheduledFutureTask,继承于FutureTask。在《并发(六)Collections》讲DelayQueue集合以及如何实现Delay接口中已经介绍过ScheduledFutureTask了。

Executors工厂类

ThreadPoolExecutor参数这么多,可以使用工厂来构建,JDK提供了一个工具类Executors来创建线程池。

newFixedThreadPool

固定大小nThreads的线程池,使用无界阻塞队列,最多只能有nThreads个线程执行,新来的任务将会排队。

public static ExecutorService newFixedThreadPool(int nThreads) {
  return new ThreadPoolExecutor(nThreads, nThreads,
                  0L, TimeUnit.MILLISECONDS,
                  new LinkedBlockingQueue<Runnable>());
}

newSingleThreadExecutor

只有一个工作线程的线程池,使用无界阻塞队列。和newFixedThreadPool(1)类似,但是最大的不同点是newSingleThreadExecutor返回的线程池无法重新配置。

public static ExecutorService newSingleThreadExecutor() {
  return new FinalizableDelegatedExecutorService
    (new ThreadPoolExecutor(1, 1,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>()));
}

newCachedThreadPool

有需要就会创建工作线程,也会重复利用已经空闲的线程。所以线程的个数最大是Integer.MAX_VALUE,采用传递队列SynchronousQueue。线程空闲时间超过60s就会被销毁,资源被回收,直至工作线程个数为0。

通常使用newCachedThreadPool执行耗时较短的任务,使用时必须考虑线程个数增加带来的可能问题。

public static ExecutorService newCachedThreadPool() {
  return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                  60L, TimeUnit.SECONDS,
                  new SynchronousQueue<Runnable>());
}

newWorkStealingPool

工作窃取线程池ForkJoinPool,会在下一篇Fork/Join文章中介绍。

public static ExecutorService newWorkStealingPool(int parallelism) {
  return new ForkJoinPool
    (parallelism,
     ForkJoinPool.defaultForkJoinWorkerThreadFactory,
     null, true);
}

newScheduledThreadPool

计划线程池。

public static ScheduledExecutorService newScheduledThreadPool(
    int corePoolSize, ThreadFactory threadFactory) {
  return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}

总结

深入线程池原理才能真正用好线程池,推荐使用ThreadPoolExecutor的构造器来创建线程池,因为这样才会对你想使用的线程池有更好的理解。

Collections(一)概览

在编程实践中,容器类库对于面向对象语言来说是最重要的类库,Java Collections Framework是Java设计者提供的容器集合,通过使用这些容器,无须费力就可以完成大量有趣的工作。

某些时候,你必须更多的了解容器以便正确的使用它们。你必须对散列操作有足够的了解,从而能够编写自己的hashcode方法(并且你必需知道何时该这么做),你还必须对不同容器的实现有足够的了解,这样才能够为你的需要进行恰当的选择。--节选自Java编程**》

本系列文章将对Java Collections Framework的一些核心数据结构API进行介绍,并从源码角度分析其实现原理,文章中将更多的探讨为什么这样设计以及性能的分析和优化。

本文对Collections API一些通用概念和设计作一个概览。

类图

image

从类图中可以看出,Java容器核心接口是Collection和Map。Collection是一个通用接口,定义了一些数据的集合,Map是个Key-Value映射集合

  • List表示一个线性(有顺序)的表结构,可以拥有重复元素
  • Queue表示一个队列,FIFO方式
  • Deque表示一个双向队列,同时支持FIFO方式或者LIFO,它集成Queue接口,即实现了队列,又实现了栈
  • Set表示一个数据集合,不允许拥有重复的元素

针对Set和Map还有两个支持排序的接口:

  • SortedSet表示一个排序的Set
  • SortedMap表示一个排序的Map

设计问题:为什么Map要独立于Collection接口?

我想Java的设计者肯定也考虑过让Map继承于Collection接口,比如Collection的元素是EntrySet来满足Map的实现。但是,Map的意义不应该只是一些数据的集合,更应该是键值对映射,它和Collection的结构和用法上有不一样的地方,所以为了更清晰的定义Ma这样的数据结构,分离了Map接口,同时提供了方法entrySet()将Map转化为Collection。

接口Collection

再具体介绍Collection接口的方法前,我们有必要了解一下集合框架中的一个设计:

设计约定:所有通用的集合实现通常提两个标准的构造器,一个无参构造器,一个参数类型为Collection的构造器(public ClazzX(Collection<? extends E> c))。

这种设计方便使用指定集合初始化新集合,允许转化集合的类型,这种构造器称为转换构造函数(conversion constructor)。

1. 基础操作(Basic Operations)

方法 作用
int size() 元素个数
boolean isEmpty() 是否是空集合
boolean contains(Object element) 是否包含元素
boolean add(E element) 增加元素
boolean remove(Object element) 删除指定元素
default boolean removeIf(Predicate<? super E> filter) 删除断言元素
Iterator iterator() 获取迭代器

其中removeIf是接口中的默认方法,在这里先给出实现源码,通过迭代器删除元素,其中Predicate是函数式接口:

default boolean removeIf(Predicate<? super E> filter) {
  Objects.requireNonNull(filter);
  boolean removed = false;
  final Iterator<E> each = iterator();
  while (each.hasNext()) {
    if (filter.test(each.next())) {
      each.remove();
      removed = true;
    }
  }
  return removed;
}

2. 批量操作(Bulk Operations)

方法 作用
boolean containsAll(Collection<?> c) 是否拥有集合元素
boolean addAll(Collection<? extends E> c) 增加集合元素
boolean removeAll(Collection<?> c) 删除集合元素
boolean retainAll(Collection<?> c) 交集
void clear() 清空集合

3. 数组操作(Array Operations)

方法 作用
Object[] toArray() 转化为数组
T[] toArray(T[] a) 转化为指定类型的数组

4. 流方式的聚合操作(Aggregate Operations)

方法 作用
Stream stream()
Stream parallelStream() 并行流
Spliterator spliterator() 分割迭代器

接口Map

Ma允许NULL值,不允许重复的KEY,一个KEY最多对应一个值VALUE。Map也提供了转换构造器,遵守了Collection接口的设计约定接下来我们会对Map接口的一些不同于Collection的方法进行介绍。

1. 基础操作(Basic Operations)

方法 作用
boolean containsKey(Object key) 是否包含KEY
boolean containsValue(Object value) 是否包含VALUE
V get(Object key) 获取Key对应的Value值
V put(K key, V value) 新增键值对
default V putIfAbsent(K key, V value) 如果Key不存在,则新增
default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) 通过Key-Value计算,remappingFunction返回NULL则删除此key(如果key存在),否则新增
default V replace(K key, V value) 替换元素

其中putIfAbsent的默认实现如下:

default V putIfAbsent(K key, V value) {
  V v = get(key);
  if (v == null) {
    v = put(key, value);
  }
  return v;
}

2. 批量操作(Bulk Operations)

方法 作用
void putAll(Map<? extends K, ? extends V> m) 增加MAP
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) 替换MAP元素
void clear() 清空MAP

其中BiFunction是一个函数式接口,提供了通过Key和Value生成替换后的元素:

@FunctionalInterface
public interface BiFunction<T, U, R> {
  R apply(T t, U u);
}

3. MAP视图

方法 作用
Set keySet() key的Set结婚,Map中的key本身不允许重复
Collection values() 值的集合,可能会重复
Set<Map.Entry<K, V>> entrySet() key-value对的Set集合

与Collection通过Iterator遍历不同,Map遍历的方式是通过以上三种MAP视图遍历的。在JDK1.8以后,Map接口提供了foreach方式便捷的迭代Map,默认实现是通过entrySet视图遍历:

default void forEach(BiConsumer<? super K, ? super V> action) {
  Objects.requireNonNull(action);
  for (Map.Entry<K, V> entry : entrySet()) {
    K k;V v;
    try {
      k = entry.getKey();
      v = entry.getValue();
    } catch(IllegalStateException ise) {
      // this usually means the entry is no longer in the map.
      throw new ConcurrentModificationException(ise);
    }
    action.accept(k, v);
  }
}

其中BiConsumer是个函数式接口。

遍历1:Iterable

Collection接口作为容器的一个根接口,它还继承了Iterable接口,所以所有的Collection实现类都提供了forEach遍历的方法。

public interface Iterable<T> {
  Iterator<T> iterator();

  default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
      action.accept(t);
    }
  }

  default Spliterator<T> spliterator() {
    return Spliterators.spliteratorUnknownSize(iterator(), 0);
  }
}

Java语言提供的"for-each loop"语句正是基于接口Iterable实现的,编译器将会优化成iterator方式遍历,对于数组的for-each,编译器将会优化成下标法遍历(The type of the Expression must be Iterable or an array type, or a compile-time error occurs《Java语言规范》)。

正因为Collection继承了Iterable接口,因此我们可以对Collecion实现类进行遍历:

for (Object o : collection)
    System.out.println(o);

Google的Guava Collection设计:由于Google的数据集合并不一定存储在内存中,可能存储在数据库中或者其它数据中心,无法获取所有数据,因此不支持计算size大小,所以倾向于使用Iterable接口。

Whenever possible, Guava prefers to provide utilities accepting an Iterable rather than a Collection

遍历2:Iterator

正如Iterator接口所示,它提供了一个方法,返回iterator对象,我们可以通过iterator进行遍历,接口定义如下:

public interface Iterator<E> {

  boolean hasNext();

  E next();

  default void remove() {
    throw new UnsupportedOperationException("remove");
  }

  default void forEachRemaining(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    while (hasNext())
      action.accept(next());
  }
}

所有Collection实现类根本身的实现原理,创建Iterator对象。典型的遍历方式代码如下:

Iterator<?> it = c.iterator();
while (it.hasNext())
    it.next();

设计**:这有个很重要的**,就是Iterator统一了所有Collection集合的遍历方式,这样我们可以无缝切换集合,但是遍历方式是一样的,这也Java框架易学习的一个设计。

遍历3:Stream

JDK1.8以后,对集合的遍历更倾向于获得Stream流,然后基于进行聚合操作。

c.stream()
  .forEach(e -> System.out.println(e.getName());

相比于Iterator,聚合操作基于流元素,而不是集合元素,它使用内部迭代方式,可以更容易将问题分解为子问题,进行并行计算,支持lambda表达式,Stream的篇幅可以很长,这里不作延伸介绍。

排序:Comparable和Comparator

对数据集合排是一种常见操作,Comparable接口定义了对象的自然排序(natural ordering),实现了此接口的类的所有对象,都将拥有一个排序属性,它提供了一方法来定义顺序,负数表小于,正表示大于,0表示相等:

public interface Comparable<T> {
  public int compareTo(T o);
}

当我们希望重新定义一个对象的顺序时(不使用自然顺序),或者对象不可排序的(没有实现Comparable接口),我们就可以使用函数式接口Comparator。

int compare(T o1, T o2);

更多应用,参见java.util.Arrays.sort(T[], Comparator<? super T>)方法。

性能:Sequential Access和Random Access

性能将选用什么数据结构实现的一个重要考虑点。

RandomAccess是一个接口,实现了此接口的类表示它的元素支持快速随机访问,Java集合框架在处理相关集合的操作上,会针对Sequential Access和Random Access来优化算法,比如实现了RandomAccess的接口的List,通过下标访问法比使用iterator更快,我们来看下Collections工具类提供的一个二分法查找的源码:

public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
    if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
        return Collections.indexedBinarySearch(list, key);
    else
        return Collections.iteratorBinarySearch(list, key);
}

运行时异常:UnsupportedOperationException和ConcurrentModificationException

当Collection某个方法在具体实现类中不支持时,就会抛出运行时异常UnsupportedOperationException。

我们来思考下这个 运行时异常的设计,它提供了方法,却在只有我们运行时才会抛出不支持异常,而仅仅能保证代码正确的方式,就是依赖具体实现类的javadoc注释,你可能觉得有点奇怪,我们为什么不提供新的没有这些方法的接口呢?

我想可能的一个目的是保证Java Collections框架为了接口的数量。

Collection很多实现类不支持并发操作,当我们在迭代集合时,如果对集合结构进行了改变(增加或者删除操作),那么程序未来的行为都是未知的,与保持同步的方式相比,抛出UnsupportedOperationException异常能达到快速失败(fail-fast)的目的,尽可能提前终止程序的运行,注意,这个异常是由iterator抛出的,在接下来的章节中,我们将会看到JDK是如何实现fail-fast机制的,这也是集合框架 设计的一个特征

常用API实现

我们通过一个表格简单的了解下通用数据结构的实现API以及实现原理。

接口 哈希表 可变数组 链表 哈希表+链表
Set HashSet TreeSet LinkedHashSet
List ArrayList LinkedList
Queue PriorityQueue(不允许NULL值)
Deque ArrayDeque LinkedList
Map HashMap TreeMap LinkedHashMap

这些通用实现都是线程不安全的,除了PriorityQueu,都允许NULL元素,并且都支持fail-fast的设计,所有都实现了接口Serializable。

为了性能和一些特殊用途,还有一些专用实现,为了线程安全,还有一些并发的实现,都将在后续章节讨论。

总结:Design Goals

本文讨论了Java Collections框架的一些基础知识,以及通用设计,这个框架最主要的设计目标是保小巧和轻量级的概念。

为了保证接口数量少,它没有提供诸如不可变的集合,不可修改的集合等,为了保证接口内方法数量少,它只包含了那些真正的基础操作以及为了性能,实现类有必要重写的方法。

还有个重要的设计**是集合都是可以互操作的比如转换构造器,Ma视图等,这也包括Array,通过桥接模式,很好的将集合和数组连接在一起。

下一篇文章,我们将会对List进行详细解析,待续。

设计模式的扉页

23种设计模式

7种结构型模式

  1. 外观模式Facade pattern
    掩藏子系统的复杂性,统一由门面对外提供简洁的接口。

  2. 适配器模式Adapter pattern
    将一个不兼容的接口(对象)包装在适配器中,以使它兼容另一个接口(对象)。

  3. 代理模式Proxy pattern
    提供一个对象的替代者。

  4. 装饰器模式Decorator pattern
    为一个对象动态的添加行为。

  5. 组合模式Composite pattern
    针对树形的一种模式,体现部分-整体的关系,像处理单一对象一样处理组合对象。

  6. 桥接模式Bridge pattern
    将抽象部分与实现部分分离,这样抽象部分和实现部分可以多维度的发生变化。

  7. 享元模式Flyweight pattern
    使用共享来有效的支持大量的细粒度对象。

5种创建型模式

  1. 单例模式Singleton pattern
    确保一个类只有一个实例,全局的入口获取这个对象实例。

  2. 建造者模式Builder pattern
    构造器与对象的表示分离,使得相同的构造器可以Build对象的不同表示。

  3. 抽象工厂模式Abstract Factory pattern
    创建一系列相关或者依赖的工厂去初始化不同的对象。扩展工厂类的方式来初始化新增的对象。

  4. 工厂模式Factory Method pattern
    定义一个接口创建对象,不同的子类表示差异的对象。使用工厂来决定子类的初始化。

  5. 原型模式Prototype pattern
    通过克隆原型对象来创建新对象。

11种行为型模式

  1. 模板方法模式Template Method pattern
    定义一些算法骨架,而将某些步骤延迟到子类中实现。

  2. 策略模式Strategy pattern
    封装一系列内部独立变化的算法,使这些算法独立于使用它们的客户端。

  3. 观察者模式Observer pattern
    定义一对多的对象,当这个对象改变时,通知或者更新到其它依赖对象发生改变。

  4. 责任链模式Chain of responsibility pattern
    给予不止一个对象机会去处理请求。

  5. 命令模式Command pattern
    将请求封装为对象,对不同的请求参数化,排队或记录请求,并支持可撤销操作。

  6. 备忘录模式Memento pattern
    在对象之外捕获并且保存对象的某些状态,便于以后恢复。

  7. 状态模式State pattern
    当一个对象内部状态改变时,允许改变它的行为。

  8. 访问者模式Visitor pattern
    在数据结构稳定的情况下,定义新的操作者。

  9. 中介者模式Mediator pattern
    中介者封装许多不同对象的交互。

  10. 解释器模式Interpreter pattern
    用语法代表来解释语句。

  11. 迭代子模式Iterator pattern
    提供一种按顺序访问聚合对象的方法,且不暴露对象的内部表示。Cursor

源码里面的设计模式

StringBuild、StringBuffer
javax.xml.xpath.XPathFactory、javax.xml.parsers.DocumentBuilderFactory
java.lang.reflect.Method#invoke()
java.util.Pattern
java.text.Normalizer
java.util.Iterator
java.util.Enumeration
java.util.Arrays#asList()
java.util.Collections#list()
java.util.Collections#enumeration()
javax.xml.bind.annotation.adapters.XMLAdapter
java.util.logging.Logger#log()
javax.servlet.Filter#doFilter()
java.lang.Runnable
java.awt.Component
java.io.InputStream, java.io.OutputStream, java.io.Reader and java.io.Writer
java.lang.Integer#valueOf(int)
java.util.Observer
java.util.EventListener
java.nio.file.FileVisitor

More:真正的设计

一个Android Markdown编辑器的实现

Markdown,一种易读易写的书写语言。更多标记语法,参考Markdown 语法说明 (简体中文版)
本文所述的markdown语法基于GitHub Flavored Markdown.

本文讨论如何在Android平台实现一个具有简单功能(代码也是极其简单)的Markdown编辑器,界面效果如下图,Android应用参见GitHub博客 APP

Layout布局

直接进入主题,从截图中可以看出,这是一个带有标题、标签、内容的编辑器。整体布局构造如下:

appbar(含有保存功能的菜单)
titlezone(标题区域)
-----|- titleEdit(标题输入框)
-----|- labelView(标签View,点击进入标签选择视图)
toolbar(工具栏)
contentzone(内容区)
-----|- contentEdit(编辑区)
-----|- previewView(预览区)

toolbar工具栏

工具栏提供了markdown的基本功能,包括插入标题、粗体、引用、代码、列表,同时提供了markdown转化为HTML的预览功能。

<android.support.v7.widget.Toolbar
    android:id="@+id/toolbar2"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:layout_gravity="left"
    android:layout_margin="0dp"
    android:background="?attr/colorPrimary"
    android:gravity="left"
    android:padding="0dp"
    app:contentInsetLeft="0dp"
    app:contentInsetStart="0dp"
    app:contentInsetStartWithNavigation="0dp"
    app:popupTheme="@style/AppTheme.PopupOverlay"
    app:titleTextAppearance="@style/Toolbar.TitleText">

    <TextView
        android:id="@+id/switchPreAndEdit"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:background="?android:attr/selectableItemBackgroundBorderless"
        android:clickable="true"
        android:gravity="center"
        android:paddingLeft="16dp"
        android:paddingRight="16dp"
        android:text="@string/blog_preview"
        android:textColor="@color/colorPrimaryDark" />

    <View
        android:layout_width="1px"
        android:layout_height="20dp"
        android:background="@color/divider"
        android:padding="6dp"></View>
</android.support.v7.widget.Toolbar>

通过id=switchPreAndEdit实现预览功能和编辑功能的切换,菜单menu/create.xml如下:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <group android:id="@+id/group">
        <item
            android:id="@+id/action_header"
            android:icon="@drawable/ic_font"
            android:title="预览"
            app:showAsAction="always" />

        <item
            android:id="@+id/action_bold"
            android:icon="@drawable/ic_bold"
            android:title="粗体"
            app:showAsAction="always" />
        <item
            android:id="@+id/action_quote"
            android:icon="@drawable/ic_quote"
            android:title="引用"
            app:showAsAction="always" />
        <item
            android:id="@+id/action_code"
            android:icon="@drawable/ic_code"
            android:title="代码"
            app:showAsAction="always" />
        <item
            android:id="@+id/action_list"
            android:icon="@drawable/ic_list"
            android:title="列表"
            app:showAsAction="always" />
        <item
            android:id="@+id/action_unlist"
            android:icon="@drawable/ic_unlist"
            android:title="无序列表"
            app:showAsAction="always" />

    </group>
</menu>

在Activity中,实现toolbar与菜单的关联:

Toolbar toolbar2 = (Toolbar) findViewById(R.id.toolbar2);
toolbar2.inflateMenu(R.menu.create);
toolbar2.setOnMenuItemClickListener(this);

编辑区与预览区

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="0dp"
        android:background="@null"
        android:fontFamily="monospace"
        android:gravity="top|left"
        android:hint=""
        android:inputType="textMultiLine"
        android:lineSpacingExtra="8dp"
        android:minLines="6"
        android:padding="16dp"
        android:scrollbars="vertical"
        android:textColor="@color/color_666"
        android:textColorHint="@color/color_999"
        android:textCursorDrawable="@drawable/cursor_editor"></EditText>

    <RelativeLayout
        android:id="@+id/preview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="invisible">

        <TextView
            android:id="@+id/progress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="16dp"
            android:text="@string/loading_preview"
            android:textColor="@color/color_666" />

        <WebView
            android:id="@+id/webview"
            android:layout_width="fill_parent"
            android:layout_height="match_parent"
            android:background="#fff"
            android:visibility="invisible" />
    </RelativeLayout>

</RelativeLayout>

编辑区由EditText实现,预览区则由WebView实现。
具体交互效果是点击工具栏的预览按钮,展示loading preview视图,待webview加载完毕,展示预览视图WebView,此时预览按钮变化成返回按钮,点击返回按钮,重新进入编辑区。

功能实现

功能主要分成两个部分,一个部分是如何实现markdown的语法插入,另一个部分是如何实现预览。

markdown语法插入

语法插入分为三种:

第一种为内联插入,即在当前位置插入前后标记,光标定位在中间,比如强调粗体(**粗体**)、内联代码(`code`),注意,如果选中了文字,我们将作为标签内容处理;

private void mdInline(Editable editableText, String character, int selectionStart, int selectionEnd) {
    editableText.insert(selectionEnd, character);
    editableText.insert(selectionStart, character);
    textArea.setSelection(selectionEnd + character.length());
}

如代码所示,在光标选中的前后位置插入特定标记字符,且将光标定位到选中文字之后。

第二种为单标签插入,即在下一行插入标记,光标定位在标记之后,比如标题(###)、引用(> )、列表(* );

private void mdNewSingleLine(Editable editableText, String character, int selectionStart, int selectionEnd) {
    if (selectionEnd == editableText.length() || '\n' == editableText.charAt(selectionEnd)) {
        editableText.insert(selectionEnd, "\n");
    } else {
        editableText.insert(selectionEnd, "\n\n");
    }
    if (selectionStart == 0 || '\n' == editableText.charAt(selectionStart - 1)) {
        editableText.insert(selectionStart, "\n" + character + " ");
        textArea.setSelection(selectionEnd + ("\n" + character + " ").length());
    } else {
        editableText.insert(selectionStart, "\n\n" + character + " ");
        textArea.setSelection(selectionEnd + ("\n\n" + character + " ").length());
    }
}

此段代码中,对光标选中的前后位置进入换行符插入,同时插入标记字符,光标定位在选中文字之后。

第三种为双标签插入,即插入前后标记,光标定位在中间。比如代码块(```)。

private void mdNewDoubleLine(Editable editableText, String character, int selectionStart, int selectionEnd) {
    if (selectionEnd == editableText.length() || '\n' == editableText.charAt(selectionEnd)) {
        editableText.insert(selectionEnd, "\n" + character + "\n");
    } else {
        editableText.insert(selectionEnd, "\n" + character + "\n\n");
    }
    if (selectionStart == 0 || '\n' == editableText.charAt(selectionStart - 1)) {
        editableText.insert(selectionStart, "\n" + character + "\n");
        textArea.setSelection(selectionEnd + ("\n" + character + "\n").length());
    } else {
        editableText.insert(selectionStart, "\n\n" + character + "\n");
        textArea.setSelection(selectionEnd + ("\n\n" + character + "\n").length());
    }
}

双标签插入相较于单标签插入,我们只要在光标选中末尾处,插入结束标记字符即可。至此,工具栏的监听代码已完成。

@Override
public boolean onMenuItemClick(MenuItem item) {
    int itemId = item.getItemId();
    Editable editableText = textArea.getEditableText();
    int selectionStart = textArea.getSelectionStart();
    int selectionEnd = textArea.getSelectionEnd();
    CharSequence selectStr = editableText.subSequence(selectionStart, selectionEnd);

    switch (itemId) {
        case R.id.action_header:
            mdNewSingleLine(editableText, "###", selectionStart, selectionEnd);
            break;
        case R.id.action_bold:
            mdInline(editableText, "**", selectionStart, selectionEnd);
            break;
        case R.id.action_code:
            if (selectionStart == 0 || '\n' == editableText.charAt(selectionStart - 1)) {
                mdNewDoubleLine(editableText, "```", selectionStart, selectionEnd);
            } else {
                mdInline(editableText, "`", selectionStart, selectionEnd);
            }
            break;
        case R.id.action_quote:
            mdNewSingleLine(editableText, ">", selectionStart, selectionEnd);
            break;
        case R.id.action_list:
            mdNewSingleLine(editableText, "1.", selectionStart, selectionEnd);
            break;
        case R.id.action_unlist:
            mdNewSingleLine(editableText, "*", selectionStart, selectionEnd);
            break;
    }

    return false;
}

预览功能

预览功能其实就是两个View的隐藏与展现。功能的重点在于如何实现md到html的转化,以及webview如何加载html数据。

  1. md转化成html有很多开源工具,这里使用了GitHub的开放API:
Request url:
POST https://api.github.com/markdown

Request body:
{
  "text": "### 1. 关于Dubbo、原理、负载均衡 "
}
  1. 获取的html片段,我们通过webview的loadDataWithBaseURL方法进行加载
String template = Template.getInstance().getPreview();
String data = template.replace("{{body}}", response);
mWebView.loadDataWithBaseURL("file:///android_asset/", data, "text/html", "utf-8", null);

更多More

关于markdown的标记,不止本文实现的这些功能,我们可以在工具栏扩展图片插入、链接插入等新功能。当然,在电脑上有ctr+z取消输入,在手机上编辑器也可以增加恢复撤销功能。

最后,目前移动设备上并不适合长文本输入,简短的Markdown输入来的更自由,更方便。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.