这篇3万字的Java后端面试总结,面试官看了瑟瑟发抖(一)

2022-05-05 16:54:59 浏览数 (1)

HashMap源码

❝问:「HashMap」底层原理,为什么线程不安全。❞

代码语言:javascript复制
hashmap:
数组  链表   红黑树
初始长度 = 16
扩容因子 = 0.75
索引确定:
index  = hashCode(key) % length
hashCode(key) 高8位与低8位异或 & (length - 1)

「关于线程不安全」

HashMap会进行resize操作,在resize操作的时候会造成线程不安全。下面将举两个可能出现线程不安全的地方。

put的时候导致的多线程数据不一致。这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。

另外一个比较明显的线程不安全的问题是HashMap的get操作可能因为resize而引起死循环(cpu100%)。

❝问:HashMap与Hashtable的区别❞

1、继承的父类不同

Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。

2、线程安全性不同

javadoc中关于hashmap的一段描述如下:此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。

Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。

4、key和value是否允许null值

其中key和value都是对象,并且不能包含重复key,但可以包含重复的value。

通过上面的ContainsKey方法和ContainsValue的源码我们可以很明显的看出:

Hashtable中,key和value都不允许出现null值。但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常,这是JDK的规范规定的。

HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。

ConcurrentHashMap源码

❝问:ConcurrentHashMap底层原理,如何保证线程安全的❞

这里只讨论JDK1.8的ConcurrentHashMap

采用了「数组 链表 红黑树」的实现方式来设计。

采用Node节点保存key,value及key的hash值。如下:

代码语言:javascript复制
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    ...
 }

为保证线程安全,采用synchronized CAS HashEntry 红黑树

无锁化保证线程安全。

我们看到put方法调用了casTabAt方法。

代码语言:javascript复制
private static final sun.misc.Unsafe U;

////
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT)   ABASE, c, v);
}

////
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

会后调用的是本地方法。

❝问:CAS底层原理❞

引用来源:https://youzhixueyuan.com/concurrenthashmap.html

CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。

AQS原理

❝问:AQS底层以及相关的类❞

见文章:Java并发编程初探-AQS

❝问:ThreadLocal底层,软引用和弱引用❞

引用来源:使用ThreadLocal怕内存泄漏?那你应该来看看这篇文章

线程池

❝问:线程池组成原理,线程池的拒绝策略❞

见文章:手写线程池

❝问:如何理解多线程,高并发❞

引用来源:https://www.cnblogs.com/cheyunhua/p/10530023.html

高并发可以通过分布式技术去解决,将并发流量分到不同的物理服务器上。但除此之外,还可以有很多其他优化手段:比如使用缓存系统,将所有的,静态内容放到CDN等;还可以使用多线程技术将一台服务器的服务能力最大化。

「多线程是指从软件或者硬件上实现多个线程并发执行的技术」,它更多的是解决CPU调度多个进程的问题,从而让这些进程看上去是同时执行(实际是交替运行的)。

这几个概念中,「多线程解决的问题是最明确的,手段也是比较单一的,基本上遇到的最大问题就是线程安全」。在JAVA语言中,需要对JVM内存模型、指令重排等深入了解,才能写出一份高质量的多线程代码。

❝问:如果有个线程4,要等前面线程1,2,3都执行完才能执行,你要怎么做❞

示例:

代码语言:javascript复制
/**
 * Description:倒计数器
 *
 * @author Lvshen
 * @version 1.0
 * @date: 2020/4/16 14:28
 * @since JDK 1.8
 */
public class CountDownLatchDemo {
    static final int COUNT = 20;

    static CountDownLatch cdl = new CountDownLatch(COUNT);

    public static void main(String[] args) throws Exception {
        new Thread(new Teacher(cdl)).start();
        Thread.sleep(1);
        for (int i = 0; i < COUNT; i  ) {
            new Thread(new Student(i, cdl)).start();
        }
        synchronized (CountDownLatchDemo.class) {
            CountDownLatchDemo.class.wait();
        }
    }

    static class Teacher implements Runnable {

        CountDownLatch cdl;

        Teacher(CountDownLatch cdl) {
            this.cdl = cdl;
        }

        @Override
        public void run() {
            System.out.println("老师发卷子。。。");
            try {
                cdl.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("老师收卷子。。。");
        }

    }

    static class Student implements Runnable {

        CountDownLatch cdl;
        int num;

        Student(int num, CountDownLatch cdl) {
            this.num = num;
            this.cdl = cdl;
        }

        @Override
        public void run() {
            System.out.println(String.format("学生(%s)写卷子。。。",num));
            //doingLongTime();
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(String.format("学生(%s)写卷子。。。",num));
            cdl.countDown();
        }

    }
}

public class ConcurrentTestDemo {

    public static void main(String[] args) {

        //并发数
        int currency = 20;

        //循环屏障
        CyclicBarrier cyclicBarrier = new CyclicBarrier(currency);

        for (int i = 0; i < currency; i  ) {
            new Thread(() -> {
                OrderServiceImplWithDisLock orderService = new OrderServiceImplWithDisLock();
                System.out.println(Thread.currentThread().getName()   "====start====");
                //等待一起出发
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                orderService.createOrder();
            }).start();
        }
    }
}

❝问:i 要线程安全,你有几种方法,这几种哪种性能最好❞

volatile sychronized/lock.lock , AtomicInteger高并发下lock.lock性能要好,atomicInteger.incrementAndGet()底层用了「CAS」lock.lock底层也用了「CAS」

代码语言:javascript复制
public class VolatileAtomicTest {
    public static volatile int num = 0;

    public synchronized static void increase() {
        num  ;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i  ) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j  ) {
                    increase();
                }
            });

            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        System.out.println(num);
    }
}

❝问:线程间的共享怎么实现❞

Callable的call方法有返回值;volatile关键字能实现线程变量的可见

代码语言:javascript复制
public static void main(String[] args) throws ExecutionException, InterruptedException {
    Callable<String> callable = () -> {
        log.info("当前线程:{}", Thread.currentThread().getName());
        return "Lvshen";
    };

    //MyFutureTask<String> myFutureTask = new MyFutureTask(callable);

    FutureTask<String> myFutureTask = new FutureTask<>(callable);
    new Thread(myFutureTask).start();

    System.out.println(String.format("当前线程:[%s],取出的值:[%s]", Thread.currentThread().getName(), myFutureTask.get()));
}

如上代码:Main线程获取到[Thread-0]线程的值。

❝问:ThreadPoolExecutor参数❞

代码语言:javascript复制
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {

corePoolSize:核心线程数maximumPoolSize:最大线程数keepAliveTime:当没有任务时,多余核心线程数的线程存活时间

Java锁

❝问:synchronized与Lock的区别❞

见文章:Java锁-synchronized底层原理

❝问:volatile关键字原理,以及原子自增类❞

见文章:Java关键字——volatile底层原理分析

❝问:JUC并发包❞

JDK并发工具类是JDK1.5引入的一大重要的功能,体现在java.util.concurrent包下。java.util.concurrent包主要包含了「并发集合类,线程池」「信号量」三组重要工具类,还包括了java.util.concurrent.atomic以及java.util.concurrent.locks两个子包。一般来说,我们称这个包为J.U.C。

JVM

❝问:Java虚拟机内存划分,GC回收算法。❞

关于内存划分:

可以看看这篇文章:https://mp.weixin.qq.com/s/fit90VdZUa2pG9lbET0i7w

关于GC回收算法:

见文章:GC回收算法

❝问:内存溢出如何排查❞

见文章:https://mp.weixin.qq.com/s/7XGD-Z3wrThv5HyoK3B8AQ

❝问:虚拟机调优❞

见视频:https://ke.qq.com/user/index/index.html#/plan/cid=2770807&term_id=102879437

❝问:类加载❞

算法

❝问:雪花算法,原理知道吗,有没有缺点。❞

代码语言:javascript复制
long id2Long = ((nowTimestamp - baseTimestamp) << TIMESTAMP_LEFT_SHIFT) | workId | sequence;

ip 端口 时间戳 。 跟机器时间有关,如果机器时间回调,会生成重复的ID。

❝问:说说二叉树,与B Tree的区别❞

见文章:MySQL为什么选择B Tree做索引

❝问:红黑树和哈希表使用场景❞

「Hash:」

hash表使用场景:bitmap的布隆过滤器使用的是hash表。在那些需要一次一次遍历,去寻找元素的问题中,可以将问题转化为根据元素的内容去寻找索引,哈希表在这方面的时间效率是贼高的;在一些字符串词频统计问题、数独问题等问题中,可以利用哈希函数来计算某个元素出现的次数,作为算法的辅助工具;还有些问题,可以利用散列函数的思路,让几个不同的元素获得同样的结果,从而实现一个聚类。

举个用于消息摘要例子,银行的数据库中是不能保存用户密码的原文的,只能保存密码的hash值。在这种应用场景里,对于抗碰撞和抗篡改能力要求极高,对速度的要求在其次。一个设计良好的hash算法,其抗碰撞能力是很高的。

「红黑树:」

  • 「epoll」的事件管理模块
  • Java中的TreeMap
  • 适用增删比较多的情况
  • 「AVL」适用查询比较多的情况
  • 相对于跳表,红黑树不适用范围性的查找

未完待续~

0 人点赞