java集合类源码探秘(一)

1、java的default关键字

default关键字是java8引入的关键字,也可称为Virtual extension methods--虚拟扩展方法。是指在接口内部包含了一些默认的方法实现(也就是接口中可以包含方法体,这打破了Java之前版本对接口的语法限制),从而使得接口在进行扩展的时候,不会破坏与接口相关的实现类代码。

比如:

public interface I {
  void fun();
  default void defaultFun1() {
    System.out.println("default方法1");
  }
  default void defaultFun2() {
    System.out.println("default方法2");
  }
}

public class IImpl implements I {

  @Override
  public void fun() {
    System.out.println("fun方法");
    defaultFun1();
    defaultFun2();
  }
  @Override
  public void defaultFun2() {
    System.out.println("重写了default方法");
  }

  public static void main(String[] args) {
    IImpl impl = new IImpl();
    impl.fun();
  }

}

接口中的方法使用了default关键字对方法进行了实现,实现类实现含有default方法的接口时,我们可以直接使用接口的default方法,也可以在实现类中重写接口的default方法从而实现自己的方法。

 

2、数组的内部机制

  数组是一种引用数据类型,数组元素(数组内部存储的值)和数组变量在内存里是分开存放的。数组变量只是一个引用,这个引用变量可以指向任何有效的内存,只有当该引用指向有效内存后,才可以通过该数组变量来访问数组元素,因为,引用变量是访问真实对象的根本方式。也就是说,如果我们希望在程序中访问数组,则只能通过这个数组的引用变量来访问它,而实际的数组元素被存储在堆(Heap)内存中,引用变量则被存储在栈(Stack)内存中。

  简单点说,数组就是一个连续数据的存储空间。

(1)基本类型的数组

如:

int[] iArr;

iArr = new int[5];

当执行 int[] iArr; 后,只是在栈内存中定义了一个空引用,这个引用并未指向任何有效的内存,因此也就无法指定数组的长度;当执行  “iArr = new int[5];”  实现动态初始化后,系统将负责为该数组分配5个int型数据所占的连续内存空间,并分配默认的初始值,所有数组元素都被默认赋为0。

(2)对象类型的数组

对象类型的数组和基本类型的数组一样,只不过数组元素是对象,即数组变量指向对象的内存而已。

 

3、ArrayList、LinkedList和HashMap的底层原理

 ArrayList源码

   //对象数组:ArrayList的底层数据结构
    private transient Object[] elementData;
    //elementData中已存放的元素的个数,注意:不是elementData的容量
    private int size;
ArrayList的底层数据结构是一个数组,默认初始化容量为10,也可在构造时传入初始化容量参数,当不断向ArrayList添加元素时,如果数组容量不够,ArrayList会自动扩容,扩容至原来的1.5倍,如下源码:

   int oldCapacity = elementData.length;
   int newCapacity = oldCapacity + (oldCapacity >> 1);

 

LinkedList 底层是由java实现的一个双向链表,其节点为Node对象,item保存值,next和prev分别为指向后一个和前一个节点的指针,其源码如下:

public class LinkedList<E>{

  transient Node<E> first;

  transient Node<E> last;

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

}

 

HashMap底层是一个数组+链表实现

数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端:

  数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;

  链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。

哈希表则综合两者的特性,是一种寻址容易,插入删除也容易的数据结构,哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。HashMap就是一个线性的数组实现的,可以理解为其存储数据的容器就是一个线性数组。这可能让我们很不解,一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有做一些处理。首先HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,HashMap有一个基准数组,每一个数组变量里存放的是一个单向链表,当我们向HashMap中存放数据时,首先获取传入的key的hashCode,再将得到的hashCode对基准数组的长度求余数,根据余数决定该数据放在哪个数组变量里,然后遍历该数组变量里的单向链表,如果发现有相同的key则直接修改该key对应的value,否则用传入的key和value构造一个Entry对象,请该Entry对象放在该链表的首位,并将next指向原来首位的Entry对象。

 

4、System.arraycopy()和Arrays.copyOf()的区别

先来看看System.arraycopy()方法的声明

public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

src         --    源数组。
srcPos   --    源数组中的起始位置。
dest       --    目标数组。
destPos --    目标数据中的起始位置。
length    --    要复制的数组元素的数量。

该方法用了native关键字,说明调用的该方法是其他语言写的底层函数,即本地方法。

再看看Arrays类的copyOf()方法的声明

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
  @SuppressWarnings("unchecked")
  T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength);
  System.arraycopy(original, 0, copy, 0,
  Math.min(original.length, newLength));
  return copy;
}

original      --   要复制的数组
newLength --  要返回的副本的长度
newType    --  要返回的副本的类型
该方法对应不同的数据类型都有各自的重载方法,仔细观察发现,copyOf()内部调用了System.arraycopy()方法。

System.arraycopy()和Arrays.copyOf()方法的区别在于:

  1. System.arraycopy()需要目标数组,将原数组拷贝到你自己定义的数组里,而且可以选择拷贝的起点和长度以及放入新数组中的位置
  2. Arrays.copyOf()是系统自动在内部新建一个数组,调用System.arraycopy()将原数组的内容复制到新数组中去,并且长度为newLength。返回新数组; 即将原数组拷贝到一个长度为newLength的新数组中,并返回该数组。

总结:Array.copyOf()可以看作是受限的System.arraycopy(),它主要是用来将原数组全部拷贝到一个新长度的数组,适用于数组扩容。

5、线程安全的并发容器底层原理

(1) CopyOnWriteArrayList实现原理

CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为"写时复制器",底层数据结构也是一个数组。很多时候,我们的系统应对的都是读多写少的并发场景。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将原容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。

public class CopyOnWriteArrayList<E>{

  

  final transient ReentrantLock lock = new ReentrantLock();

  //指向原容器的引用
  private transient volatile Object[] array;

 

  final Object[] getArray() {
    return array;
  }

  final void setArray(Object[] a) {
    array = a;
  }

  //读(获取)操作无锁

  public E get(int index) {

    return get(getArray(), index);
  }

  //写操作则会上锁,并首先将原容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器

  public E set(int index, E element) {

    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
      Object[] elements = getArray();
      E oldValue = get(elements, index);

      if (oldValue != element) {
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len);
        newElements[index] = element;
        setArray(newElements);
      } else {
        setArray(elements);
      }
      return oldValue;
    } finally {
      lock.unlock();
    }
  }


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

优缺点分析

  了解了CopyOnWriteArrayList的实现原理,分析它的优缺点及使用场景就很容易了。

  优点:

  读操作性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。Java的list在遍历时,若中途有别的线程对list容器进行修改,则会抛出ConcurrentModificationException异常。而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的list容器,所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常了

  缺点:

  缺点也很明显,一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC;二是无法保证实时性,Vector对于读写操作均加锁同步,可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。

 

ConcurrentHashMap的底层原理   具体可见 https://www.cnblogs.com/chengxiao/p/6842045.html

 

6、java的Executor框架和线程池实现原理及源码分析

(1)Executor接口

public interface Executor {

  void execute(Runnable command);

}

(2)ExecutorService接口

public interface ExecutorService extends Executor {

  void shutdown();

  List<Runnable> shutdownNow();

  boolean isShutdown();

  boolean isTerminated();

  boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

  <T> Future<T> submit(Callable<T> task);

  <T> Future<T> submit(Runnable task, T result);

  Future<?> submit(Runnable task);

  <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;

  <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;

  <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;

  <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;

}

7、CAS原理与应用

  在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令。 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成(摘自维基本科)。

  CAS是通过悬锁实现的,CAS的整个过程是原子的,即(1)获取到内存的值V,(2)将内存值V与旧的预期值A比较,(3)将内存值修改为新值B或什么都不做,这三步是同时完成的,不可能被其他线程中断。

  CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。java.util.concurrent包完全建立在CAS之上的,没有CAS就不会有此包,可见CAS的重要性,java.util.concurrent包中借助CAS实现了区别于synchronouse同步锁的一种乐观锁。

  CAS的缺点:CAS存在ABA的问题,意思就是当A被修改为B时,B又被修改为A,CAS就回误以为该值没有被改变过; JVM提供了AtomicStampedReference类,通过版本控制的原理,解决了ABA的问题。

 

8、Java中Unsafe类详解

java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作,主要提供了以下功能:

(1)通过Unsafe类可以分配内存,可以释放内存;

类中提供的3个本地方法allocateMemory、reallocateMemory、freeMemory分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应。

public native long allocateMemory(long l);
public native long reallocateMemory(long l, long l1);
public native void freeMemory(long l);
(2)挂起与恢复

将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。

(3)CAS操作

是通过compareAndSwapXXX方法实现的

 

9、AtomicInteger、AtomicBoolean等原子类的原理

AtomicInteger、AtomicBoolean等并发包java.util.concurrent.atomic下面的原子类都是线程安全的,其内部实现原理是: volatile+CAS(Unsafe类中提供的原子操作方法基于CAS算法)

举例说明,如AtomicInteger部分源码如下:

public class AtomicInteger{

  private static final Unsafe unsafe = Unsafe.getUnsafe();

  private volatile int value; 

  public final int get() {
    return value;
  }

  public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
  }

}

可以看出

  (1) AtomicInteger中存储的值value被volatile关键字修饰,就保证了value的可见性,且获取 AtomicInteger中存储的值是直接返回的,未进行任何控制

  (2) AtomicInteger中存储的值value被修改时调用的是Unsafe类提供的方法,由于Unsafe类提供的方法都是硬件级别的原子操作,所以能保证原子性

其他原子类原理与此相同,由于以上的机制,所以AtomicInteger、AtomicBoolean等并发包java.util.concurrent.atomic下面的原子类都是线程安全的。

 

谈谈可见性:

  可见性发生于多个线程访问共享变量时,一个线程对共享变量做了修改,其他线程能够立即看到。可见性是由于现在的内存设置原因导致的一个问题,计算机中不仅有主内存,而且有高速缓存(高速缓存的设置是因为CPU执行指令的速度与CPU读取内存的速度差别较大,为提高CPU存取数据的速度而设置的)。每个线程操作变量时,会把变量加载进CPU的缓存中,修改后,会立即更新此缓存,但是不会立即刷新主内存的值。此时,如果其他线程访问此变量时,有可能拿的就不是最新的值。而造成不安全的情况。

10、i++不是原子操作

public class ITest {
  private static volatile int number = 0;
  private static volatile int count = 0;
  public static void main(String[] args) {
    for(int i=0; i<5; i++) {
      new Thread(new MyThread()).start();
    }
    while(count < 5) {}
    System.out.println(number);
  }

  static class MyThread implements Runnable {
    @Override
    public void run() {
      for(int i=0; i<100; i++) {
        number++;
        try {
          Thread.sleep(10);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
      count++;
    }
  }
}

-------------------------------------------

多次运行输出结果为:第一次499,第一次498...,如果i++是原子操作的话,预期结果应该是500,由此可见 i++ 不是原子操作

posted @ 2018-03-25 12:21  将王相  阅读(88)  评论(0)    收藏  举报