4万字聊聊阿里二面,能抗多少?(上)

简介: 我是Leo。今天聊一下阿里二面。

一、Java


1.1 Java重写和重载的区别?

重载: 重载就是可以允许在类中存在多个方法名的函数,他们具有相同的名字,但具有不同的参数和不同的参数个数。也是让类以统一的方式处理不同类型数据的一种手段。

重写: 重写就是父类与子类之间的多态性,对父类的函数进行重写定义,子类既可以继承父类的函数,方法也可以在父类的基础上进行特定的修改。

1.2 Java有哪些数据结构,源码分析?

image.png

①、链表(List)

链表是一种递归的数据结构,它或者为空(null),或者是指向一个结点node的引用,该节点还有一个元素和一个指向另一条链表的引用。

这里主要分析一下ArryList。

优点:查询效率高,使用频率也很高(因为在于内存的连续性,CPU的内部缓存结构会缓存连续的内存⽚段,可以⼤幅降低读取内存的性能开销)缺点:线程不安全,增删效率低(copy数组占用太多时间)

//无参构造ArrayList的话 会初始化空对象
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 底层的数组对象
transient Object[] elementData;
// 默认初始化为10 
private static final int DEFAULT_CAPACITY = 10;
// 默认最大值
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 无参构造方法
public ArrayList() {
    //无惨构造,默认参数赋给数组对象
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 有参构造方法
public ArrayList(int initialCapacity) {
    //有参构造时,会传一个大小,如果大于0 就把传入的参数作为length
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        //如果等于0,就直接用自带的DEFAULTCAPACITY_EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        //如果这个大小小于0肯定是不允许的
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
// add方法
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
// add方法中的ensureCapacityInternal
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// add方法中ensureCapacityInternal的calculateCapacity
// 这个函数其实就是处理 当是无参构造时,他会在add函数里 做+1处理 并且与自带的大小比较,取最大的值。
// 这里就是为什么很多会说 ArrayList初始化默认长度为10的原因(添加数据后是10)
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
// add方法中的ensureCapacityInternal的ensureExplicitCapacity
private void ensureExplicitCapacity(int minCapacity) {
    //modCount就是他的修改次数同时主要处理快速失败的一个作用,官方原话是这么说的
    //If an implementation does not wish to provide fail-fast iterators, this field may be ignored.
    //如果一个实现不希望提供快速失败迭代器,这个字段可以被忽略。  
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
// 这个函数主要处理的是一个 扩容 的一个作用。
// 扩容完之后, 开始处理数据的真实大小。通过Arrays.copyOf 函数 截取当前的 扩容后的大小 也就是15赋值给elementData 底层数组
private void grow(int minCapacity) {
        // 首先取整个链表 数据的长度,   比如 length = 10
        int oldCapacity = elementData.length;
      //10 + 5 = 15   这个 15 就是最新的链表长度 
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // copy 数组,耗时主要是在这个地方
        elementData = Arrays.copyOf(elementData, newCapacity);
}
// 这个函数其实没啥用,主要就是两极的判断, 如果小于0 抛出异常,如果大于最大值 等等
private static int hugeCapacity(int minCapacity) {
    // 
      if (minCapacity < 0)
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}
// 最后处理完之后, 回到add函数的 第二行代码,把新加的数据 写入当前数组对象中

JDK1.7与1.8的区别

1.7 默认构造大小为10

1.8 默认构造大小为空数组,长度为0

//无参构造方法   1.7
public ArrayList() {
        this(10);
}
 public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
}
//无参构造方法   1.8
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

增删慢的原因是

在对源码进行分析的话,如果读懂源码的话,我相信大家对 grow 比较印象深刻。这个函数内会有一个Arrays.copyOf操作。也就是拷贝数组数据。这就是慢的根本原因。

tip: 这里LinkedList 我用的不多,就不做过多介绍了。后续会展开更新的再细一些。

②、哈希表(Hash)

哈希表(HashTable) 也叫散列表,是一种可以通过关键码至 K,V直接访问的数据结构。它最大的特点就是可以快速实现查找、插入和删除。

数组的最大特点就是查找容易,插入和删除困难;而链表正好相反,查找困难,而插入和删除容易。哈希表很完美地结合了两者的优点, Java 的 HashMap 在此基础上还加入了树的优点。

上源码

final float loadFactor; //记录 hashMap 装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 0.75
static final int MAXIMUM_CAPACITY = 1 << 30;  // 1073741824
static final int TREEIFY_THRESHOLD = 8;   //链表长度到8,就转为红黑树
static final int UNTREEIFY_THRESHOLD = 6; // 树大小为6,就转回链表
transient Node<K,V>[] table;  // 哈希桶数组
transient int modCount; //记录 hashMap 发生结构性变化的次数(注意 value 的覆盖不属于结构性变化)
int threshold; //threshold的值应等于 table.length * loadFactor, size 超过这个值时进行 resize()扩容
// 无参构造
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 根据大小参数以及负载因子构造
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
}
// 根据大小参数构造   日常使用中,我们还是首选无参构造以及 根据大小参数构造的。
public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// put 放入函数
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
// hash 函数
static final int hash(Object key) {
        int h;
    /*key 的 hash 值的计算是通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销*/
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// put 函数的内嵌函数
// 参数onlyIfAbsent表示是否替换原值
// 参数evict我们可以忽略它,它主要用来区别通过put添加还是创建时初始化数据的
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)
            // resize()不仅用来调整大小,还用来进行初始化配置
            n = (tab = resize()).length;
      //这里就是看下在hash位置有没有元素,实际位置是hash % (length-1)
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 将元素直接插进去
            tab[i] = newNode(hash, key, value, null);
        else {
            //这时就需要链表或红黑树了
          // e是用来查看是不是待插入的元素已经有了,有就替换
            Node<K,V> e; K k;
            // p是存储在当前位置的元素
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;  //要插入的元素就是p,这说明目的是修改值
            // 若原来元素是红黑树节点,调用红黑树的插入方法:putTreeVal
            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);
                        // 链表比较长,需要树化,
                      // 由于初始即为p.next,所以当插入第8个元素才会树化
                        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;
                }
            }
            // e就是被替换出来的元素,这时候就是修改元素值
            if (e != null) { // 待插入元素在 hashMap 中已存在
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 默认为空实现,允许我们修改完成后做一些操作
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
      // size太大,达到了capacity的0.75,需要扩容
        if (++size > threshold)
            resize();
      // 默认也是空实现,允许我们插入完成后做一些操作
        afterNodeInsertion(evict);
        return null;
}
// 动态扩容
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
      // 如果是第一次resize 肯定是0,无需扩容。 如果是装不下了  扩容的时候 就会选取 oldTab.length
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
      // 判断负载因子是否大于0
        int oldThr = threshold;
        int newCap, newThr = 0;
      // 如果大于0 这里主要是做一个安全措施,如果大于了最大值MAXIMUM_CAPACITY  就会使用更大的上限
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
           // 把上文的threshold 负载因子 扩大一倍  也就是 左移一位  赋值给newCap,与HashMap自带的上限做比较,并且与自带的默认容量比较  如果符合的话  就 计算当前的容量 (扩大一倍)
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
      //当前上文的负载因子大于0,把当前的值赋给 newCap (这个值主要是决定下文Node的大小)
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            // 否则就采用默认的大小 16
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
      //这里主要就是判断 0 的一些保护措施
        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的大小 ,把当前的Node节点给哈希桶数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
      // 判断oldTab  ,这里的oldTab其实就是 HashMap自带的 哈希桶数组在 resize第一行声明了
        if (oldTab != null) {
            // 开始对oldTab进行 循环, 终止值 就是 这个oldTab的大小 上文 判断了 取0 或者 .length了
            // 把 oldTab 中的节点 reHash 到 newTab 中去
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                // 每一次循环 把当前节点赋值给临时变量 e 判断是否为空
                if ((e = oldTab[j]) != null) {
                    // 当前节点归为null
                    oldTab[j] = null;
         // 如果当前只有一个节点,就取当前的hash值与 newCap-1 做二进制 位运算 直接在newTab中进行重定位
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    // 如果当前 e 是树节点 要进行 红黑树的 rehash 操作
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // 若是链表,进行链表的 rehash 操作
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //根据算法 e.hash & oldCap 判断节点位置 rehash 后是否发生改变
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            // rehash 后节点新的位置一定为原来基础上加上 oldCap
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
}
  //这个函数的功能是对红黑树进行 rehash 操作
     final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
             TreeNode<K,V> b = this;
             // Relink into lo and hi lists, preserving order
             TreeNode<K,V> loHead = null, loTail = null;
             TreeNode<K,V> hiHead = null, hiTail = null;
             int lc = 0, hc = 0;
          //由于 TreeNode 节点之间存在双端链表的关系,可以利用链表关系进行 rehash
             for (TreeNode<K,V> e = b, next; e != null; e = next) {
                    next = (TreeNode<K,V>)e.next;
                    e.next = null;
                    if ((e.hash & bit) == 0) {
                        if ((e.prev = loTail) == null)
                            loHead = e;
                        else
                            loTail.next = e;
                        loTail = e;
                        ++lc;
                    }
                    else {
                        if ((e.prev = hiTail) == null)
                            hiHead = e;
                        else
                            hiTail.next = e;
                        hiTail = e;
                        ++hc;
                    }
                }
                //rehash 操作之后注意对根据链表长度进行 untreeify 或 treeify 操作
                if (loHead != null) {
                    if (lc <= UNTREEIFY_THRESHOLD)
                        tab[index] = loHead.untreeify(map);
                    else {
                        tab[index] = loHead;
                        if (hiHead != null) // (else is already treeified)
                            loHead.treeify(tab);
                    }
                }
                if (hiHead != null) {
                    if (hc <= UNTREEIFY_THRESHOLD)
                        tab[index + bit] = hiHead.untreeify(map);
                    else {
                        tab[index + bit] = hiHead;
                        if (loHead != null)
                            hiHead.treeify(tab);
                    }
                }//end if
            }//end split

③、数组(Array)

这个Array可以参考上文的ArrayList。因为ArrayList的底层就是数组

④、栈(Stack)

提到栈我们的第一印象就是 先进后出, 下面我们看看栈的源码。栈的核心函数 全部加了synchronized 锁, 因为栈的数量,下标不能搞错, 搞错就崩了。

protected int elementCount;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
protected int capacityIncrement; // 如果容量增量小于或等于零,则每次需要增长时,向量的容量都会增加一倍。  
// 构造一个 无参的 栈。他继承于Vector
public Stack() {
}
// 入栈
public E push(E item) {
        addElement(item);
        return item;
}
public synchronized void addElement(E obj) {
    // 入栈时  递增 modCount 记录修改次数
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;
}
private void ensureCapacityHelper(int minCapacity) {
        // 如果
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
}
// 这个函数应该在ArrayLisrt会比较熟悉的, 
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
      // 如果当前容量 大于0 说明不需要增长  直接取当前容量, 否则就取  整个容量的 2倍(这里的2倍主要是从 oldCapacity+oldCapacity 得到的 oldCapacity又代表elementData.length )
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
    // 这里就是一些安全措施
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
    // newCapacity 就是 上文我们 三目运算符计算的 进行对象复制
        elementData = Arrays.copyOf(elementData, newCapacity);
}
// 出栈
public synchronized E pop() {
        E       obj;
        int     len = size();
        obj = peek();
        removeElementAt(len - 1);
        return obj;
}
// 这里直接返回容量的大小
public synchronized int size() {
        return elementCount;
}
public synchronized E peek() {
        int     len = size();
        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
}
// 由上列调用可以看出,取的就是末尾数据 弹出
public synchronized E elementAt(int index) {
    // 这里做了安全措施,如果当前弹出的下标 大于 容量  就抛出异常。 这句代码也验证了 顶部的 ` 因为栈的数量,下标不能搞错, 搞错就崩了。`
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
        }
    // 弹出当前下标的元素  给上文的 obj
    return elementData(index);
}
public synchronized void removeElementAt(int index) {
      // 每次 删除的时候  自增一个 记录修改次数
        modCount++;
      // 如果当前的下标 大于 容量  抛出异常   (这个就是一个安全措施 没啥好说的)
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                     elementCount);
        }
        else if (index < 0) {
            throw new ArrayIndexOutOfBoundsException(index);
        }
        int j = elementCount - index - 1;
      // 复制数组
        if (j > 0) {
            System.arraycopy(elementData, index + 1, elementData, index, j);
        }
      // 复制成功后  数量-1
        elementCount--;
      // 弹出的那个对象 为null 清空回收 结束
        elementData[elementCount] = null; /* to let gc do its work */
}

⑤、队列(Queue)

Queue继承于Collection 包,先进先出。它只是一个接口。旗下大概有6种实现类

  • ArrayBlockingQueue :一个由数组支持的有界队列。
  • LinkedBlockingQueue :一个由链接节点支持的可选有界队列。
  • PriorityBlockingQueue :一个由优先级堆支持的无界优先级队列。
  • DelayQueue :一个由优先级堆支持的、基于时间的调度队列。
  • SynchronousQueue :一个利用 BlockingQueue 接口的简单聚集(rendezvous)机制。
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
//  这里大概的看了一下 感觉都很像,其实好好想想 这三种 都是继承AbstractQueue 而且又实现了Queue 能不像嘛
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();
}
LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue();
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);
}
PriorityBlockingQueue priorityBlockingQue = new PriorityBlockingQueue();
public PriorityBlockingQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityBlockingQueue(int initialCapacity,
                                 Comparator<? super E> comparator) {
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        this.comparator = comparator;
        this.queue = new Object[initialCapacity];
}

这里主要就分析一下第一个队列吧

ArrayBlockingQueue是GUC包下的一个线程安全的阻塞队列,底层使用数组实现。

// 底层存储元素的数组
final Object[] items;
// 出队序号,如果有一个元素出队,那么后面的元素不会向前移动,
// 而是将 takeIndex + 1 表示后面要出队的元素的角标
int takeIndex;
// 入队序号,表示后续插入的元素的角标,putIndex 不一定大于 takeIndex
int putIndex;
// 元素个数
int count;
// 内部锁
final ReentrantLock lock;
// notEmpty 条件
private final Condition notEmpty;
// notFull 条件
private final Condition notFull;
// 这是一个有参构造,因为底层是数组实现的,所以肯定是要初始化数组的,数组的长度肯定是你传入的参数。
// 因为他是线程安全的,所以肯定也是少不了锁的存在。
// 锁这个东西这里就不展开了在下文中也会多次提到GUC的。
public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        // 初始化底层数组
        this.items = new Object[capacity];
        // 默认为非公平锁 非公平锁就是 false  代表 不会按照等待越长就越先执行的规则。 
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
}
public void put(E e) throws InterruptedException {
        checkNotNull(e);
      // 获取锁
        final ReentrantLock lock = this.lock;
      /**
         * lock:调用后一直阻塞到获得锁
         * tryLock:尝试是否能获得锁 如果不能获得立即返回
         * lockInterruptibly:调用后一直阻塞到获得锁 但是接受中断信号(比如:Thread、sleep)
         */
        lock.lockInterruptibly();
        try {
            // 如果队列数组已满,则 notFull 阻塞,当有元素被移除后才唤醒 notFull
            while (count == items.length)
                notFull.await();
            // 元素入队
            enqueue(e);
        } finally {
            // 添加完元素后释放锁,这里一定要注意,千万不要因为忘记放锁,导致程序超时
            lock.unlock();
        }
}
// 首先肯定是要判断是否为空, 如果为空的话抛出异常,这个属于安全措施 没啥好说的
private static void checkNotNull(Object v) {
        if (v == null)
            throw new NullPointerException();
}
// 插入元素的函数,只在加锁状态下调用  而且是插入在尾部
private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
    // 要插入的元素e 放入 items数组的 putIndex的位置  (putIndex 就是当前最新要放入的位置下标)
        items[putIndex] = x;
    // 先自增 再匹配 整个数组的长度  这里的写法真漂亮。 没有无效代码
        if (++putIndex == items.length)
            // 下次放入的位置为0  因为是先进先出嘛,所以就导致了 每次放入的位置都是0
            putIndex = 0;
      //元素个数
        count++;
       // 唤醒 notFull
        notEmpty.signal();
}
private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        // 元素置 null
        items[takeIndex] = null;
        // 如果出队的是数组中的最后一个元素,则重置 takeIndex 为 0
        if (++takeIndex == items.length)
            takeIndex = 0;
      // 出队一个 肯定是要递减的呀
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        // 唤醒 notFull
        notFull.signal();

搞完队列,栈啥的 才发现好简单,HashMap的源码是真的复杂,不过学到的东西也是特别多的。

⑥、图(Graph)

不是特别常用

⑦、堆(Heap)

⑧、树(Tree)

  • TreeMap
  • TreeSet

这里主要介绍一下TreeSet,因为TreeSet是在TreeMap的基础上 扩展出的一个有序集合,底层的存储也是一颗红黑树。

TreeMap<Integer,String> tree = new TreeMap<Integer,String>();
//定义NavigableMap接口类型m
private transient NavigableMap<E,Object> m;
//申请类型为Object的PRESENT,作为key-value值中的value值,将我们需要保存的值放入key中
private static final Object PRESENT = new Object();
// The comparator used to maintain order in this tree map, or null if it uses the natural ordering of its keys.
private final Comparator<? super K> comparator;
//多态,m其实为一个TreeMap的实现类
TreeSet(NavigableMap<E,Object> m) {
    this.m = m;
}
//无参构造函数,申请一个TreeMap作为参数,由此可见TreeSet依赖于TreeMap进行实现
public TreeSet() {
    this(new TreeMap<E,Object>());
}
//指定比较器的构造函数
public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}
// 添加函数 源码
public V put(K key, V value) {
    // 声明一个 主节点 root
        Entry<K,V> t = root;
    // 如果是第一次插入的话 肯定是空,因为他没有父亲,他的主节点就是他自己
        if (t == null) {
            //使用此TreeMap的正确比较方法比较两个键。   下午已经把源码贴出来了
            compare(key, key); // type (and possibly null) check
            // 创建一个父节点
            root = new Entry<>(key, value, null);
            // 因为只有一个节点 size设为 1
            size = 1;
            modCount++;
            return null;
        }
      // 如果走到了这里说明 root 不为空,当前存在节点
        int cmp;
      // 声明一个临时 父亲节点
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                // 当前比较的两个节点 不为空时, 就把之前的root 覆给 临时父节点
                parent = t;
                // 比较当前的key值的顺序
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
}
// Compares two keys using the correct comparison method for this TreeMap.
// 使用此TreeMap的正确比较方法比较两个键。  
final int compare(Object k1, Object k2) {
        return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
            : comparator.compare((K)k1, (K)k2);
}

树是有点麻烦了


1.3  JVM的双亲委派机制说一下

class文件是通过类加载器 (ClassLoader) 装载到JVM中的。为了防止存在多个字节码,使用了双亲委派机制。双亲委派机制一定分为3/4层

  • 根加载器(Bootstrap loader)
  • 扩展加载器(ExtClassLoader)
  • 系统加载器(AppClassLoader)
  • 用户自定义加载器

在处理字节码相同的时候,并没有自己判断,而且把这个工作返回给上一层,由上一层判断,上一层会继续返回上一层,如果到了跟加载器还是没有发现这个类名(字节码)的话就判定为不存在这个类名(字节码)。

如何破坏双亲委派的话,我们可以举一个例子,比如我创建一个类名为String的类。你看看能不能使用嘛。就算能使用,那你看看他运行后会不会报错嘛。

这个例子就是完全的诠释了双亲委派原则。


1.4 JDK源码除了数据结构你还阅读过哪些?

比如一开始常用的集合包,JUC包,线程等

JUC (java.util.concurrent)

  • ReentrantLock
  • ThreadPoolExecutor
  • AQS

这里介绍一下ReentrantLock。首先介绍一下AQS。AQS的全称是AbstractQueuedSynchronizer 。因为AQS是JUC包下的基类。是比较核心的一个类。

CountDownLatch,ReentrantLock,信号量等 都依靠AQS来实现。他可以实现公平锁,也可以实现非公平锁,也是互斥锁以及是可重入锁。

这是ReentrantLock源码的变量 以及 AQS的变量
// 排它锁标识
static final Node EXCLUSIVE = null;
// 如果带有这个标识,证明是失效了
static final int CANCELLED =  1;
// 具有这个标识,说明后继节点需要被唤醒
static final int SIGNAL    = -1;
// Node对象存储标识的地方
volatile int waitStatus;
// 指向上一个节点
volatile Node prev;
// 指向下一个节点
volatile Node next;
// 当前Node绑定的线程
volatile Thread thread;
// 返回前驱节点,如果前期节点为null  就抛出空节点异常
final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
}
// 加锁入口
public void lock() {
    // sync 分为公平和非公平锁
    sync.lock();
}
// 非公平锁
final void lock() {
    // 通过CAS的方式 尝试把state从 0 修改 成1  如果返回true 代表成功,如果返回false  代表失败
    // CAS 是轻量级锁的实现方式
    if (compareAndSetState(0, 1))
        // 将一个属性设置为当前线程,这个属性是AQS的父类提供的  (AbstractOwnableSynchronizer)
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
}
// 核心函数
public final void acquire(int arg) {
    //tryAcquire 再次尝试获取锁资源,如果尝试成功 返回true
        if (!tryAcquire(arg) &&
            // 获取锁资源失败后,需要将当前线程封装成一个node 追加到AQS的队列中
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}
// tryAcquire 函数进入后 
final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
            final Thread current = Thread.currentThread();
    // 获取AQS的state的值
            int c = getState();
    // 如果 占有锁资源的人 已经释放了 我们就可以尝试获取锁资源
            if (c == 0) {
    // CAS尝试修改state 从0 修改成 1  如果成功 设置 exclusiveOwnerThread 属性为当前线程
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
    // 当前占有锁资源的线程是否是当前线程
            else if (current == getExclusiveOwnerThread()) {
                // 将state +1
                int nextc = c + acquires;
                // 如果加1 小于0 
                if (nextc < 0) // overflow
                    // 抛出超时锁可重入的最大值
                    throw new Error("Maximum lock count exceeded");
                // 如果没问题 继续修改 0 -1 
                setState(nextc);
                // 返回成功
                return true;
            }
            return false;
}
// 从 acquire 函数的 addWaiter
// 说明前面获取锁资源失败,放到锁队列中等待
private Node addWaiter(Node mode) {
    // 创建node类, 并且设置thread 为当前线程,设置为排他锁
        Node node = new Node(Thread.currentThread(), mode);
        // 获取AQS中队列的尾部节点
        Node pred = tail;
      //如果tail 不为null  说明现在队列有人排队
        if (pred != null) {
            // 把当前节点的prev 设置为刚才的尾部节点
            node.prev = pred;
            // 基于CAS的方式 将tail 节点设置为当前节点
            if (compareAndSetTail(pred, node)) {
                // 将之前的节点为next 设置为当前节点
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
}
// 如果当前没有人排队等待 就进入了enq函数  如果CAS失败,也会进入到这个位置重新往队列的尾部插入
private Node enq(final Node node) {
    // 死循环
        for (;;) {
            // 重新获取当前的tail节点为 t
            Node t = tail;
            if (t == null) { 
                // 如果当前tail节点为null  说明现在没人排队,我是第一个
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 如果现在有人排队,那我就把尾部插入,继续排队等待
                node.prev = t;
                // 基于CAS的方式 将tail节点设置为当前节点
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}
// 已经将node 加入到双向队列中,然后执行当前方法
final boolean acquireQueued(final Node node, int arg) {
    // 标识
        boolean failed = true;
        try {
            // 标识
            boolean interrupted = false;
            for (;;) {
                // 获取当前节点的 上一个节点 
                final Node p = node.predecessor();
                // 如果 p是头,说明就是null  那我就执行 利用 tryAcquire函数 再次获取锁资源
                // 获取成功之后 state从 0改成1 返回true 
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 保证上一个节点是 -1  才会返回true,才会将线程阻塞,等待唤醒获取锁资源
                if (shouldParkAfterFailedAcquire(p, node) &&
                    // 基于Unsafe类的 park方法,挂起线程
                    parkAndCheckInterrupt())  // 针对fail属性,这里是唯一可能出现异常的地方,JVM                         内部出现问题时。
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}
// 如果没有拿到锁资源 执行这个函数 node是当前节点,pred 是上一个节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获取上一个节点的状态 state
        int ws = pred.waitStatus;
    // 如果上一个节点状态为 SIGNAL 一切正常
        if (ws == Node.SIGNAL)
            return true;
    // 如果上一个节点已经失效了  (这里失效了我解释一下)
        if (ws > 0) {
            do {
               // 将当前节点的prev指针指向了上一个的上一个
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);  // 一致找到小于等于0的
            // 将重新标识好的最近的有效节点的next
            pred.next = node;
        } else {
            // 小于等于0 ,不等于-1,将上一个有效节点状态修改为 -1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
}
// 上文中 finally 代码块的 cancelAcquire 函数
// 如果当前为null 结束,健壮性判断
private void cancelAcquire(Node node) {
        if (node == null)
            return;
      // 当node的线程置为 null,竞争锁资源跟我没关系了。
        node.thread = null;
        // node的上一个节点
        Node pred = node.prev;
      // node上一个节点 大于0
        while (pred.waitStatus > 0)
            // 找到前面节点中 非失效的节点
            node.prev = pred = pred.prev;
      // 将第一个不是失效节点的下一个节点 声明出来
        Node predNext = pred.next;
    // 当前节点置为失效节点 给别的node看的
        node.waitStatus = Node.CANCELLED;
    // 如果当前节点是尾节点,将尾结点设置为最近的有效节点(如果当前是尾节点)
        if (node == tail && compareAndSetTail(node, pred)) {
            // 用CAS方式将尾节点的next 设置成null
            compareAndSetNext(pred, predNext, null);
        } else {
            int ws;
            // 中间节点操作
            // 如果上一个节点不是头节点
            if (pred != head &&
                // 获取上一节点状态,是不是有效
                ((ws = pred.waitStatus) == Node.SIGNAL || //
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }
            node.next = node; // help GC
        }
}


1.5 说一下jvm的内存模型?

聊到JVM内存模式,首先聊一下java文件的加载流程吧

  1. 通过javac  编译 .java 文件
  2. 通过 java  编译成 .class  文件
  3. 通过类装载子系统 加载到运行时数据区(内存模型) 如下图

image.png

堆: 存放的是局部变量信息

栈: 存放的是声明的对象信息,

本地方法栈: 本地方法栈可以通过Thread来查看,start的底层是没有实现的,他的前面加了一个 native。 他是通过C,C++实现的。

方法区: 方法区存放的是 .class文件  ,常量,静态变量,类信息。有一些静态变量他也可能说是一种对象如下代码

public static User user = new User(); 

user会存放在方法区中,但是User会存放在堆上, 在底部会放上最终的流程图,没搞明白的可以去查看一下

程序计数器: 每个线程都有一个程序计数器。通过字节码执行引擎在执行.class文件的同时调用程序计数器计数。

字节码执行引擎: 用于执行加载进来的.class文件。

我们就举一个main线程的例子。在main函数中执行,整个线程主要包含

  1. 程序计数器
  2. 栈帧

栈帧又分成 子函数栈帧(类似于调一个自定义函数)main函数栈帧

在每一块栈帧中,又包含 局部变量,操作数栈,动态链接,方法出口 如下图

  • 局部变量就是我们程序中定义的 int,变量等。
  • 操作数栈就是我们在计算一些信息时,存放的区域。
  • 方法出口就是当调用一个子函数时,有返回信息时,就把返回信息存入方法出口中。从compute栈帧退出到main栈帧后继续执行主函数。

image.png

从方法出口出来之后,会到main函数栈帧中,main函数栈帧也是有同样的局部变量,这个局部变量存放的一些对象信息,存的其实不是对象,他存的只是对象在堆中的引用地址

image.png


1.6 List、HashMap和HashSet的使用区别?

ArrayList优缺点

  1. 有序
  2. 可重复
  3. 当链表过大时,查询速度慢时间复杂度为O(n)
  4. 底层做插入,删除操作时,涉及到copy数据,所以当链表过大,插入删除的性能也很慢

HashMap优缺点

  1. 超级快速的查询速度
  2. 动态的可变长存储数据(和数组相比较而言)
  3. 需要额外计算一次hash值,如果处理不当会占用额外的空间

HashSet优缺点

  1. HashSet基于HashMap实现, 以HashSet的值作为HashMap的一个key, 以一个Object对象常量作为HashMap的值。
  2. 不允许重复,无序。一般用HashSet要重写hashCode和equals方法


1.7 gcroot都有哪些?老年代和新生代的区别?

gcroots也就是垃圾收集器的对象,gc会收集那些不是gc roots且没有被gc roots引用的对象。一个对象可以属于多个root。

  1. class:  由系统类加载器加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,.
  2. Thread:  活着的线程
  3. Stack Local:  Java方法的local变量或参数
  4. JNI Local:  JNI方法的local变量或参数
  5. JNI Global:  全局JNI引用
  6. Monitor Used:  用于同步的监控对象

将GC Roots 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。

GC Roots根节点:线程栈的本地变量,静态变量,本地方法栈的变量等等。类似于一个树结构

老年代与新生代的区别

  1. 存活寿命的不同,老年代的存活寿命大于新生代的存活寿命
  2. 新生代采用的是复制算法,老年代采用的是标记删除算法
  3. 新生代每次使用的空间不超过90%,主要存放新生的对象。老年代主要存放的是声明周期长的对象。
  4. 新生代分为Eden,ServivorFrom,ServivorTo三个区。老年代没有具体的细分区域

Eden:Java新对象的出生地,如果新创建的对象过大会直接分配到老年代。当Eden区内存不够时,就会触发一次MinorGC,对新生代进行一次垃圾回收

ServivorTo:保存Eden区经过一次MintorGC后的幸存者。

ServivorFrom:上一次MintorGC的幸存者,作为这一次的被扫描者。

MintorGC采用的是复制算法,把Eden区,ServivorFrom区域中存活的对象复制到ServivorTo,并且把他们的年代 +1。(如果ServivorTo位置不够用了就会转移部分到老年代,或者年代大于等于15也会转移到老年代)。

然后清空Eden与ServivorFrom的对象,ServivorTo与ServivorFrom互换,原ServivorTo成为下一次GC时的ServivorFrom区了。

老年代的对象比较稳定,MajorGC不会频繁执行,在执行MajorGC前一般都会先执行MintorGC,这样做的好处是使新生代的对象晋升到老年代,导致空间不够用时,再触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。

因为老年代的对象比较稳定,所以不会采用复制算法,MajorGC会采用标记删除算法,首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。Major的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出OOM异常。


1.8 Heap是什么?和Stack的区别

Heap他是一个堆,Stack是一个栈。

  1. 栈的空间是由操作系统自动分配和释放的,堆的空间是手动申请和释放的,也就是我们程序中的new关键字。所以JVM调优绝大多数调优的区域都是在堆中。
  2. 存储方式的不同。因为栈的空间是有限的,堆的空间是有很大的自由区,所以我们在使用时,创建的对象,存在栈中往往的引用堆的内存地址,存在堆中才是实际的对象信息。
  3. 堆空间用完后,一定要free掉,以防堆溢出,栈就不存在这种问题(如果栈要有这个问题的话,我估计出栈入栈就已经卡住了)


1.9 创建一个对象的过程是什么?

public class Leo {
    public static void main(String[] args) throws CloneNotSupportedException {
        // 通过new关键字创建对象
        VipInfo vipInfo = new VipInfo();
        // 通过clone方式创建对象
        VipInfo vipInfo1 = (VipInfo)vipInfo.clone();
        // 通过 .class.newInstance() 创建对象
        try {
            VipInfo info = VipInfo.class.newInstance();
        }catch (Exception e) {
        }
    }
}

创建对象的方式大概有四种,我的话只了解三种。

  • 通过new
  • 通过clone方式
  • 通过.class方式(类似反射的一种,这里不论述了会在下面的反射那个板块里讲解)

创建流程

当对象被创建时,虚拟机会分配内存来存放对象。这里声明一下从父类,超类继承过来的实例变量也会被分配内存空间。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。

在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序员的意志进行初始化。(这个位置才真正的赋构造的值)在Java对象初始化过程中,分别是 实例变量初始化实例代码块初始化构造函数初始化

image.png

按照程序员的意志进行初始化:这里也可以说是 "半初始化"


1.10 对eaquels的理解?

聊到eaquels函数不得不把 == 聊一下

==:判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型比较的是值,引用数据类型比较的是内存地址)。

eaquels

  • 没有重写 equals()  方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
  • 重写了 equals() 方法。一般,我们都重写 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)


1.11 HashMap和ConcurrentHashMap的区别?

源码分析的话在数据结构那里已经分析过了,这里简单总结一下区别

  1. HashMap线程是不安全的,ConcurrentHashMap是基于CAS的,所以线程是安全的。
  2. HashMap允许Key与Value为空,ConcurrentHashMap不允许
  3. HashMap不允许通过迭代器遍历的同时修改,ConcurrentHashMap允许。并且更新可见
  4. 他们俩的底层数据结构都是一样的(数组+链表+红黑树)


1.12 HashMap为什么用红黑树?

不是用红黑树来管理HashMap的,而是在hash相同的情况下,且重复数量大于8。用红黑树来管理数据。 红黑树相当于排序数据,可以自动的使用二分法进行定位,性能较高。一般情况下,hash值做的比较好的话基本上用不到红黑树。

红黑树牺牲了一些查找性能 但其本身并不是完全平衡的二叉树。因此插入删除操作效率略高于AVL树。 AVL树用于自平衡的计算牺牲了插入删除性能,但是因为最多只有一层的高度差,查询效率会高一些。


1.13 JVM调优参数有哪些?随便说几个

  • -Xms256m:初始化堆大小为 256m;
  • -Xmx2g:堆最大内存为 2g;
  • -Xmn50m:新生代的大小50m;
  • -XX:+PrintGCDetails 打印 gc 详细信息
  • -XX:+HeapDumpOnOutOfMemoryError  在发生OutOfMemoryError错误时,来dump堆快照
  • -XX:NewRatio=4    设置年轻的和老年代的内存比例为 1:4;
  • -XX:SurvivorRatio=8 设置新生代 Eden 和 Survivor 比例为 8:2;
  • -XX:+UseSerialGC   新生代和老年代都用串行收集器 Serial + Serial Old
  • -XX:+UseParNewGC 指定使用 ParNew + Serial Old 垃圾回收器组合;
  • -XX:+UseParallelGC  新生代使用Parallel Scavenge,老年代使用Serial Old
  • -XX:+UseParallelOldGC:新生代ParallelScavenge + 老年代ParallelOld组合;
  • -XX:+UseConcMarkSweepGC:新生代使用ParNew,老年代的用CMS;
  • -XX:NewSize;新生代最小值;
  • -XX:MaxNewSize:新生代最大值
  • -XX:MetaspaceSize 元空间初始化大小
  • -XX:MaxMetaspaceSize 元空间最大值


1.14 反射是什么?

反射是把Java类中的各个成分映射成一个个的Java对象。在运行状态中

  • 对于任意一个类,都能够知道这个类的所有属性和方法
  • 对于任意一个对象,都能够调用它的任意一个属性和方法

优点

  • 在程序运行过程中可以操作类对象,增加了程序的灵活性;
  • 解耦,从而提高程序的可扩展性,提高代码的复用率,方便外部调用;
  • 对于任何一个类,当知道它的类名后,就能够知道这个类的所有属性和方法;而对于任何一个对象,都能够调用它的一个任意方法。

缺点

  • 性能问题:Java 反射中包含了一些动态类型,JVM 无法对这些动态代码进行优化,因此通过反射来操作的方式要比正常操作效率更低。
  • 安全问题:使用反射时要求程序必须在一个没有安全限制的环境中运行,如果程序有安全限制,就不能使用反射。
  • 程序健壮性:反射允许代码执行一些平常不被允许的操作,破坏了程序结构的抽象性,导致平台发生变化时抽象的逻辑结构无法被识别。


1.15 Java中创建多线程的方式有哪些?线程池有哪些参数?

创建方式

  1. 继承Thread类,重写run方法
  2. 实现Runnable接口,重写run方法
  3. 通过Callable和FutureTask创建线程
  4. 通过线程池创建线程

常用参数如下

  • 线程池的线程个数 nThreads
  • 创建新线程时要使用的工厂 threadFactory
  • 线程池中的核心线程数量,即使这些线程没有任务干,也不会将其销毁 corePoolSize
  • 当线程池中的线程数量大于corePoolSize时,多余的线程等待新任务的最长时间  keepAliveTime
  • keepAliveTime的时间单位。 unit
  • 在线程池中的线程还没有还得及执行任务之前,保存任务的队列 workQueue
  • 当线程池中没有多余的线程来执行任务,并且保存任务的多列也满了(指的是有界队列),对仍在提交给线程池的任务的处理策略 handlerRejectedExecutionHandler

线程池的好处

  1. 降低资源消耗:可以重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控


1.16 执行流程和拒绝策略是什么?

执行流程

  1. 当有新任务提交到线程池时,当线程数<核心线程数,创建线程
  2. 如果当前线程数>=核心线程数,且任务队列未满时,将任务放入任务队列
  3. 如果当前线程数>=核心线程数,且任务队列已满时,根据拒绝策略进行相应的处理
  4. 如果当前线程数<最大线程数,创建线程
  5. 如果当前线程数=最大线程数,调用拒绝执行处理程序

image.png

源码分析

/**
 * 在将来的某个时间执行给定的任务。 这个任务可以在新线程中执行,也可以在现有的线程池中执行。  
 * 如果任务不能提交执行,可能是因为这个原因“执行者”已经被关闭,或者因为它的容量已经达到,  
 * 当前任务就由RejectedExecutionHandler 进行处理
 */
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * 主要分三步走
     * 1. 如果运行的线程少于corePoolSize,则尝试用给定的命令作为第一个命令启动一个新的线程
     * 任务。 调用addWorker会自动检查runState和workerCount,这样可以防止误报 
     * 线程,当它不应该返回false。
     *
     * 2. 如果一个任务可以成功排队,那么我们仍然需要再次检查我们是否应该添加一个线程  
     * (因为现有的已经在上次检查后死亡)或其他进入此方法后池关闭。 所以我们重新检查状态,
     * 如果有必要,回滚排队停止,或启动一个新的线程(如果没有)。  
     *
     * 3. 如果不能对任务进行排队,则尝试添加一个新的线程。 如果它失败了,我们知道我们已经关闭或饱和了       * 因此拒绝任务。  
     */
    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);
}

拒绝策略

很清晰的看到,当前共有四中实现类实现这个接口。

image.png

AbortPolicy:该策略是直接将提交的任务抛弃掉,并抛出RejectedExecutionException异常。

/**
 * A handler for rejected tasks that throws a
 * {@code RejectedExecutionException}.
 */
public static class AbortPolicy implements RejectedExecutionHandler {
    /**
     * Creates an {@code AbortPolicy}.
     */
    public AbortPolicy() { }
    /**
     * Always throws RejectedExecutionException.
     *
     * @param r the runnable task requested to be executed
     * @param e the executor attempting to execute this task
     * @throws RejectedExecutionException always
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() +
                                             " rejected from " +
                                             e.toString());
    }
}

DiscardPolicy:该策略也是将任务抛弃掉(对于提交的任务不管不问,什么也不做),不过并不抛出异常。

/**
 * A handler for rejected tasks that silently discards the
 * rejected task.
 */
public static class DiscardPolicy implements RejectedExecutionHandler {
    /**
     * Creates a {@code DiscardPolicy}.
     */
    public DiscardPolicy() { }
    /**
     * Does nothing, which has the effect of discarding task r.
     *
     * @param r the runnable task requested to be executed
     * @param e the executor attempting to execute this task
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    }
}

DiscardOldestPolicy : 该策略是当执行器未关闭时,从任务队列workQueue中取出第一个任务,并抛弃这第一个任务,进而有空间存储刚刚提交的任务。使用该策略要特别小心,因为它会直接抛弃之前的任务。

/**
 * A handler for rejected tasks that discards the oldest unhandled
 * request and then retries {@code execute}, unless the executor
 * is shut down, in which case the task is discarded.
 */
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    /**
     * Creates a {@code DiscardOldestPolicy} for the given executor.
     */
    public DiscardOldestPolicy() { }
    /**
     * Obtains and ignores the next task that the executor
     * would otherwise execute, if one is immediately available,
     * and then retries execution of task r, unless the executor
     * is shut down, in which case task r is instead discarded.
     *
     * @param r the runnable task requested to be executed
     * @param e the executor attempting to execute this task
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            e.getQueue().poll();
            e.execute(r);
        }
    }
}

CallerRunsPolicy:该策略并没有抛弃任何的任务,由于线程池中已经没有了多余的线程来分配该任务,该策略是在当前线程(调用者线程)中直接执行该任务。

/**
 * A handler for rejected tasks that runs the rejected task
 * directly in the calling thread of the {@code execute} method,
 * unless the executor has been shut down, in which case the task
 * is discarded.
 */
public static class CallerRunsPolicy implements RejectedExecutionHandler {
    /**
     * Creates a {@code CallerRunsPolicy}.
     */
    public CallerRunsPolicy() { }
    /**
     * Executes task r in the caller's thread, unless the executor
     * has been shut down, in which case the task is discarded.
     *
     * @param r the runnable task requested to be executed
     * @param e the executor attempting to execute this task
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
}


1.17 Java的序列化和反序列化?

Java序列化指将Java对象转换为字节序列的过程,反序列化指将字节序列转换为目标对象的过程;

当Java对象需要网络传输或者持久化到磁盘上时,才需要进行序列化。


相关文章
|
6月前
|
缓存 NoSQL 应用服务中间件
万字攻略,社招腾讯天美C++后台面经,面试题整理(上)
万字攻略,社招腾讯天美C++后台面经,面试题整理
|
6月前
|
设计模式 算法 网络协议
社招offer-腾讯T9-70W年薪(面试经验分享)(上)
社招offer-腾讯T9-70W年薪(面试经验分享)
|
9月前
|
存储 缓存 前端开发
【大厂面试合集】每日一刷——1. 字节跳动2021抖音客户端开发工程师秋招真题
【大厂面试合集】每日一刷——1. 字节跳动2021抖音客户端开发工程师秋招真题
62 1
|
5月前
|
算法 安全 Java
二面头条、三面拼多多、五面蚂蚁分享面经总结,助你拿大厂offer
蚂蚁金服、头条、拼多多的面试总结 文章有点长,请耐心看完,绝对有收获!不想听我BB直接进入面试分享: 准备过程 蚂蚁金服面试分享 拼多多面试分享 字节跳动面试分享 总结
|
5月前
|
消息中间件 缓存 架构师
复习这份美团架构师的Java核心面试宝典,我四面阿里拿下offer
怎样才能拿到大厂的offer,没有掌握绝对的技术,那么就要不断的学习 他是如何拿下阿里等大厂的offer的呢,今天分享他的秘密武器,美团资深架构师整理的Java核心知识点,面试时面试官必问的知识点,篇章包括了很多知识点,其中包括了有基础知识、Java集合、JVM、多线程并发、spring原理、微服务、Netty 与RPC 、Kafka、日记、设计模式、Java算法、数据库、Zookeeper、分布式缓存、数据结构等等。
|
6月前
|
存储 Kubernetes 并行计算
万字攻略,社招腾讯天美C++后台面经,面试题整理(下)
万字攻略,社招腾讯天美C++后台面经,面试题整理
|
6月前
|
存储 应用服务中间件 nginx
社招offer-腾讯T9-70W年薪(面试经验分享)(下)
社招offer-腾讯T9-70W年薪(面试经验分享)
社招offer-腾讯T9-70W年薪(面试经验分享)(下)
|
6月前
|
监控 算法 测试技术
携程2023算法开发岗 一面 二面 面经
携程2023算法开发岗 一面 二面 面经
47 0
|
7月前
|
缓存 算法 架构师
2023年秋招成功斩获offer秘籍:九大核心知识+1000道大厂面试真题
前言 如何在金九银十成功斩获Offer!BATJ大厂面经(成功关键:吃透九大核心知识+狂刷大厂面试真题) 第一轮:团队面试 第一轮基本上是你的团队成员面试你,是和你同级或者高你一个P的师兄来面你,我的话基本没问什么特别的,主要还是讲自己简历上的做的项目,这里需要你很熟悉自己的项目才行。 我个人觉得这里你要把项目里你的角色做了什么没做什么讲清楚,然后最好能把自己做的那部分重点展开来讲,然后面试官会从你讲的内容里挑一些具体来问你,这部分就因人而异了,知道的说不知道的别瞎说。 一些什么TCP/UDP啊、网络模型啊之类的基础知识。我是觉得这一面只要你项目靠谱、不吹牛逼、底子扎实,过一面应该都
|
8月前
|
Java
阿里淘系 七面 0经验拿下offer 只因面试前死磕了Java核心面试笔记
前几天一位粉丝朋友反馈,在九月份参加了阿里(淘系)面试,经过和面试官激励的七次博弈顺利拿下offer!
52 0