JAVA基础-JDK源码

JDK源码

1.ArrayList、LinkedList、Hashtable、HashMap、ConcurrentHashMap、HashSet、CopyOnWrite容器和Queue的实现原理

ArrayList实现原理

ArryList是基于数组实现的。是一个动态数组,默认初始大小为10.
不是线程安全的,多线程环境下应该考虑Collections.synchronizedList(List l),返回一个线程安全的List.或者使用CopyOnWriteArrayList.
实现了Serializable接口,支持序列化传输。实现了RandomAccess接口,支持快速随机访问,即通过下标进行访问。支持Clone.

LinkedList实现原理

基于 双向链表 实现。
实现了List/Deque/Cloneable/Serializable接口.

数据结构
transient int size = 0;
transient Node<E> first; //链表的头指针
transient Node<E> last; //尾指针
//存储对象的结构 Node, LinkedList的内部类
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;
    }
}
add(E e)

此方法是将元素添加至末尾,调用自身的addLast(E e)

public boolean add(E e) {
    linkLast(e);
    return true;
}
/**
* Links e as last element.
*/
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++;
}
add(int x,E e)

在指定位置插入元素,若正好在末尾则调用addLast(E e).否则调用linkBefore(element,node(index))方法进行插入

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

   if (index == size)
       linkLast(element);
   else
       linkBefore(element, node(index));
}
/**
 * Inserts element e before non-null Node succ.
 */
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++;
}
Hashtable

继承Dictionary类,实现了Map,Cloneable,Serializable接口。

成员变量

table是一个Entry[]数组类型。而Entry就是一个单向链表。哈希表的Key-Value键值对都是存储在Entry数组中。
count是HashTable的大小,它是其保存的键值对的数量。
Threshold是阈值,用于判断是否需要调整HashTable的容量。threshold的值=容量*加载因子。
loadFactor就是加载因子。
modCount是用来实现Fail-fast机制的。

put(k,v)

1.判断v是否为空,为空则抛出异常。
2.计算k的hash,由此计算table的index.如果table[index]元素不是空,则进行迭代。如果遇到相同的则直接替换返回旧的v
3.否则,将其插入table[index]位置。

public synchronized V put(K key, V value) {
    // Make sure the value is not null确保value不为null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    //确保key不在hashtable中
    //首先,通过hash方法计算key的哈希值,并计算得出index值,确定其在table[]中的位置
    //其次,迭代index索引位置的链表,如果该位置处的链表存在相同的key,则替换value,返回旧的value
    Entry tab[] = table;
    int hash = hash(key);
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            V old = e.value;
            e.value = value;
            return old;
        }
    }

    modCount++;
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        //如果超过阀值,就进行rehash操作
        rehash();

        tab = table;
        hash = hash(key);
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    //将值插入,返回的为null
    Entry<K,V> e = tab[index];
    // 创建新的Entry节点,并将新的Entry插入Hashtable的index位置,并设置e为新的Entry的下一个元素
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
    return null;
}
遍历方式
//1、使用keys()
Enumeration<String> en1 = table.keys();
    while(en1.hasMoreElements()) {
    en1.nextElement();
}

//2、使用elements()
Enumeration<String> en2 = table.elements();
    while(en2.hasMoreElements()) {
    en2.nextElement();
}

//3、使用keySet()
Iterator<String> it1 = table.keySet().iterator();
    while(it1.hasNext()) {
    it1.next();
}

//4、使用entrySet()
Iterator<Entry<String, String>> it2 = table.entrySet().iterator();
    while(it2.hasNext()) {
    it2.next();
}
HashMap
ConcurrentHashMap
HashSet
CopyOnWrite容器
Queue

2.java中的==、equals()、hashCode()源码

hashCode()

HashCode()方法是基类Object中的方法。返回的是该对象中在jvm中的物理地址。也就是说如果不对该方法进行重写。则返回该对象在jvm中的内存地址。
String底层采用char[]数组进行数据的存储。
使用31是因为

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {  //ascii编码中null对性的hash值是0,所以可以将int  h==0 作为当前字符串是否进行过hash计算的判定条件
        char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];    //31 是一个素数,与素数相乘得到的结果比其他方式更容易产生唯一性。    Java 中如果相乘的数字太大会导致内存溢出问题,从而导致数据丢失。
        }
        hash = h;
    }
    return h;
}
equals()

先比较引用地址,地址相等则直接返回true。如果为String,强转后比较长度。长度相等,挨个比较char

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}
Java中String创建原理

常量池概念:

Java运行时会维护一个String Pool(String池), 也叫“字符串缓冲区”。String池用来存放运行时中产生的各种字符串,并且池中的字符串的内容不重复。而一般对象不存在这个缓冲池,并且创建的对象仅仅存在于方法的堆栈区。

String对象的创建很讲究,关键是要明白其原理。
原理1:当使用任何方式来创建一个字符串对象s时,Java运行时(运行中JVM)会拿着这个s在String池中找是否存在内容相同的字符串对象,如果不存在,则在池中创建一个字符串s,否则,不在池中添加。
原理2:Java中,只要使用new关键字来创建对象,则一定会(在堆区或栈区)创建一个新的对象。
原理3:使用直接指定或者使用纯字符串串联来创建String对象,则仅仅会检查维护String池中的字符串,池中没有就在池中创建一个,有则罢了!但绝不会在堆栈区再去创建该String对象。
原理4:使用包含变量的表达式来创建String对象,则不仅会检查维护String池,而且还会在堆栈区创建一个String对象。最后指向堆内存中的对象

publicclass StringTest {
    public static void main(String args[]) {
       // 在池中和堆中分别创建String对象"abc",s1指向堆中对象
       String s1 = new String("abc");
       // s2直接指向池中对象"abc"
       String s2 = "abc";
       // 在堆中新创建"abc"对象,s3指向该对象
       String s3 = new String("abc");
       // 在池中创建对象"ab"  和 "c",并且s4指向池中对象"abc"
       String s4 = "ab" + "c";
       // c指向池中对象"c"
       String c = "c";
       // 在堆中创建新的对象"abc",并且s5指向该对象
       String s5 = "ab" + c;
 
       System.out.println("------------实串-----------");
       System.out.println(s1 == s2); // false
       System.out.println(s1 == s3); // false
       System.out.println(s2 == s3); // false
       System.out.println(s2 == s4); // true
       System.out.println(s2 == s5); // false
    }
}

3.ReentrantLock、AQS的源代码?AtomicInteger的实现原理,主要能说清楚CAS机制并且AtomicInteger是如何利用CAS机制实现的

4.Object类中的方法以及每个方法的作用?

5.Collections.sort方法使用的是哪种排序方法?

该方法有两种重载形式。

static <T extends Comparable<? super T>> void sort(List<T> list)    

public static <T> void sort(List<T> list,Comparator<? super T> c)

第一种方式是将列表中的元素进行升序排序,但是列表要满足以下条件:
1、列表元素实现了Comparable接口,且任意两个列表元素都是可比的。
2、列表必须支持set方法。

降序排序

1、直接使用方法,Collections.reverseOrder()作为第二个参数传入。
2、实现一个Comparator接口。自己设定排序规则。

class Point implements Comparable<Point>{
    
    private int x;
    private int y;
    
    public Point(int x,int y)
    {
        this.x = x;
        this.y = y;
    }
    
    @Override
    //如果该点到原点的距离大于o点到原点的距离,则该点大于o点
    public int compareTo(Point o) {

        int distance1 = (this.x) * (this.x) + (this.y) * (this.y);
        int distance2 = (o.x) * (o.x) + (o.y) * (o.y);
        
        return (distance1 > distance2) ? 1 : ((distance1 == distance2) ? 0 : -1); 
    }
    
    @Override
    public String toString() {
        return "(" + x + ","+  y + ")";
    }
}

//降序排序
Collections.sort(list,Collections.reverseOrder());

//降序排序
Collections.sort(list,new Comparator<Point>() {

    @Override
    //当 o1 < o2 时返回正数
    public int compare(Point o1, Point o2) {
        int distance1 = (o1.getX()) * (o1.getX()) + (o1.getY()) * (o1.getY());
        int distance2 = (o2.getX()) * (o2.getX()) + (o2.getY()) * (o2.getY());
        return (distance1 < distance2) ? 1 : ((distance1 == distance2) ? 0 : -1); 
    }
    
});

6.泛型,异常,反射?

7.ConcurrentHashMap,CopyOnWrite,线程池,CAS,AQS,虚拟机优化等?

CopyOnWriteArrayList
1、基本思路

最开始大家都共享同一个集合,但有人想要修改时,将其copy形成一个新的内容进行修改。也就是说,当我们往容器中添加元素时,不直接添加,而是先将当前容器进行copy,复制出一个新的容器,然后向新的绒里中添加元素,添加完成之后,再将引用指向新容器。这样可以保证对原容器并发进行读操作。但是写的时候,也是有加锁的。

2、实现原理

add()

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

因为写时复制,所以如果多个线程同时进行写操作,会copy出多个副本,会发生冲突。
读的时候不进行加锁,但是因为引用尚未指向新的容器,所以还是会读取到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。
读写分离:读在就容器上,写在复制的新容器上。

读取

private transient volatile Object[] array;

volatile关键字修饰,所以能保证在读的时候能读到最新的数据,但是在读取过程中其他线程修改的结果,是无法读取到的。在循环遍历的过程中,其他线程新加入的数据也是无法读取到的。

3、使用场景
其适用于读多写少的并发场景。因为读未加锁,写加锁。
4、注意事项
减少扩容,尽量初始化其大小。
使用批量添加,因为每次添加都会复制容器。
5、缺点
内存占用问题
因为会复制原始对象,所以如果原始对象过于大,可能会造成频繁的pullGC,导致服务响应时间变长。
数据一致性问题
copyOnWrite只能保证数据的最终一致性。不能保证数据的实时一致性。
6、优点
对其实例进行修改操作时会在副本容器进行修改,所以不会产生ConcurrentModifucationException错误。

CAS机制 Compare and Swap 比较替换

背景
因为多线程下操作共享变量,会造成线程安全问题。也可以使用synchronized关键字进行修饰,使没有抢到锁资源的线程都进入block状态,而后争夺到资源再恢复到runnable,这个过程涉及到操作系统用户模式和内核模式的转换,性能和资源的消耗是很大的。
CAS机制实现的类
实现该机制的类主要在java.util.concurrent.atomic包下,也就是各种原子类。
实现原理
cas机制主要有三个基本操作数:内存地址V,原始数据值A,要修改的新值B
1、内存地址V中存储着值为10的变量
2、线程1想把10变为11。此时,A=10,B=11
3、在线程1提交之前,线程2将V中的值先变为11
4、线程1提交之前,会将其A与V中实际值进行比较,发现A!=V,提交失败。
5、A重新读取V中的当前值,并重新修改B。此时线程1的A=11,B=12.这个重新尝试的过程叫做自旋。
6、直到线程1中A的值跟V中实际值相等时,才会进行提交操作。
总结
从思想上来说,synchronized属于悲观锁,也就是认为在修改过程中,一定有其他线程对数据进行了修改。但是CAS属于乐观锁,认为没有其他线程进行修改,所以让线程不断尝试。
缺点
1、CPU开销过大。
在并发量高的情况下,如果许多线程同时反复尝试更新同一个变量,却一直更新失败,会给cpu带来很大压力。
2、不能保证代码块的原子性
CAS机制保证的只是一个变量的原子性操作,但是不能保证整个代码块的原子性。如果对多个变量同时进行原子性更新,就只能使用synchronized
3、ABA问题

底层实现
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}
private volatile int value;   //volatile关键字保证获取到的数值为内存中的最新的值
public final int get() {
    return value;
}

无限循环就是自旋。其中主要实现了:
1、获取当前值
2、当前值+1,就算出目标值
3、进行cas操作,成功则跳出循环

ABA问题

当一个值从A变成B,再更新i回A,普通的CAS机制会误判通过检测。这种情况下需要对内存地址中的值,添加版本号的方式,避免ABA问题的发生。
问题描述
三个线程同时操作元素10;
线程A和B想将10->15,可以理解为同时发起两次请求,但是只需要进行一次操作,一次属于误操作,线程C想要将15->10。线程A和B获取到10,C尚未获取到。
A先一步执行完毕,此时值为15。B由于某种原因被阻塞。C在之后获取到了值15.
C执行完毕之后,将值更新回10。此时B重新开始执行。在与内存地址数据比较之后,完成提交,最终数据为15.其实最终数据应该为10。

自旋锁

跟互斥锁相同,一个线程想要访问被自旋锁保护的共享资源,必须先得到锁,在访问玩共享资源后,必须释放锁。
如果再获取自旋锁时,没有线程保持该锁,则立即得到。如果已有保持者,那么获取锁的操作将自旋,直到保持者释放了锁。

优点

自旋锁比较适用于锁使用者保持锁时间比较短的情况,比如一个变量的自增操作。否则多线程环境下可能会造成长时间的多线程自旋,从而增大CPU的压力。
自旋锁的使用时间一般都是非常短,所以才选择自旋而不是睡眠。因此自旋锁的效率高于互斥锁,因为线程的睡眠、唤醒需要操作系统的支持,开销比较大。也正因为时间短,所以不需要线程挂起和睡眠,直接让线程执行一个忙循环,等到自旋锁持有者释放锁后,其他线程就可以获取锁。

缺点

递归死锁
试图递归的获取自旋锁必然会引起死锁,递归程序的持有实例在第二个实例循环,以试图获得相同的自旋锁时,不会释放自旋锁。
过多占用CPU资源
如果不加限制,申请者会一直在循环等待,因此自旋锁在锁定的时候,如果不成功,不会睡眠,会不断的尝试。因此一般自旋锁会有一个参数限定最多持续尝试次数,超出之后,自旋锁放弃当前操作,等待下次机会。

8.hashcode有没有重写过?在什么场景下需要重写?

根据equal方法可以得出,两个对象如果是相等的,hashCode应该相等。但是两个对象如果不相等,hashCode不强制要求不一样。
相等的对象必须有相等的hashCode,但是默认的hashCode方法是针对每一个对象返回一个固定得随机值,所以当我们使用equals方法比较自定义类的对象时,必须override重写hashCode方法。

9.ArrayList,LinkedList的差别,比如一个基于数组,一个基于链表,它们均是线程不安全的,ArrayList的扩容做法等?

10.Set如何实现防重的,比如TreeSet和HashSet等?

HashSet内部维护了一个HashMap,而其add方法底层是调用map的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)
        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;
}

根据p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)),可以得到:
1、如果Hash值不同,说明是一个新的元素,存储
2、如果hash相同,且equal判断相等,说明元素已存在,不存
3、如果hash相同,且equal判断不等,说明是新元素,存储
也就是:如果有元素和传入对象的hash相等,就进行equals判断,如果相等,就认为元素已存在,否则仍然添加。

11.Collection的一些方法,比如比较方法,包装成线程安全的方法等?

12.ArrayList实现队列或堆栈?

1.深入分析了Classloader,双亲委派机制

2.HashTable、ConcurrentHashMap等其他的数据结构,可以比较两种不同的加锁方式?

3.RetreenLock的实现和应用?

4.Java内存模型,Volitale原语,内存栅栏等?

5.红黑树,LRU缓存,HashMap的排序等?

6.字符串常见面试题中的操作?

8.currentHashMap如何扩容?

9.java的异常吗?

10.运行时异常如果不处理会怎么样?应该怎么处理运行时异常??

11.那ConcurrentHashMap内部是如何实现的?每个segment是个什么数据结构?

12.什么是多态?

多态可以理解为“一个接口,多个实现”。多态的出现使得在不同的场合同一个接口可以提供不同的功能。也就是说让变量、函数或者对象能够提供多种功能。
1.编译时多态,主要是方法的重载。
2.运行时多态,主要使用继承或者实现接口

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值