Java进阶面试-高频面试题详解(二)

目录​​​​​​​

一、算法面试基础

1.1 算法面试的答题思路

面试课:

算法面试基础

算法面试的答题思路

1.2 复杂度分析

时间复杂度

空间复杂度

怎样能快速分析复杂度

二、排序算法常见面试问题

2.1 选择排序

2.2 插入排序

2.3 如何选择排序算法

三、查找算法常见面试题

3.1 常见查找算法介绍

3.2 二分查找

四、你知道Java中有几种锁吗

4.1 Lock的作用和地位part1

Lock接口

4.2 Lock的作用和地位part2

Lock主要方法介绍

lock()

tryLock()

tryLock(long time, TimeUnit unit)

lockInterruptibly()

unlock()

4.3 锁的分类

Java中有哪几种锁?

4.4 可重入锁

可重入性质:

getHoldCount(): 已获得锁的数量

4.5 乐观锁和悲观锁

悲观锁的劣势:

乐观锁的优势:

典型例子:

开销对比:

两种锁各自的使用场景

4.6 公平锁

公平的情况(以ReentrantLock为例)

对比公平和非公平的优缺点

 4.7 共享锁和排它锁

读写锁的作用

读写锁的规则

代码演示 

4.8 自旋锁

自旋锁和阻塞锁

自旋锁的缺点

自旋锁的适用场景

4.9 面试常见问题之锁篇

五、其他面试常考问题

5.1 ConcurrentHashMap面试常考问题

ConcurrentHashMap的结构

ConcurrentHashMap、HashMap、HashTable区别


一、算法面试基础

1.1 算法面试的答题思路

面试课:

        1. 高频算法面试题

        2. Java中有哪几种锁

        3. ConcurrentHashMap面试常考问题

算法面试基础

        算法面试的答题思路

        复杂度分析

算法面试的答题思路

        1. 不要直接写,要先想清楚思路

        2. 和面试官沟通确认,明确条件和思路

        3. 写代码时:明确变量的含义,包含的范围区间

        4. 写完代码后,用测试用例来测试:从小的简单的数据开始测试,边界条件,特殊情况

        5. 写注释:写方法的含义,写出入参的描述,写支持的数据范围

        6. 如果不会做,别慌张,想类似的题,面试以常规题居多,能讲思路就很好

1.2 复杂度分析

        什么是时间复杂度

        什么是空间复杂度

        复杂度有什么用

        怎样能快速分析复杂度

时间复杂度

        抓主要矛盾

空间复杂度

        占用的存储空间的量,和数据量的关系

        空间来换时间(HashMap)

        HashMap的空间复杂度就是O(n)

怎样能快速分析复杂度

        大多数情况下,一次循环就是O(n)

        双重循环就是O(n^2)

        二分就是 O(logn)

        有序数组查找某一个数就是O(logn)

        需要一次排序就是O(nlogn)

        能折半的就是O(logn)

二、排序算法常见面试问题

2.1 选择排序

        思路非常直观:选出最小的一个数,与第一个位置的数交换

        在剩下的数当中再找最小的,与第二个位置交换

        以此类推

2.2 插入排序

        打扑克牌时,发完牌后,一张张整理牌

        右手摸起来的牌都是乱序的,把牌插入到左手中合适的位置(从右往左比较)

public class InsertionSort {
    public static void main(String[] args) {
        for(int i=1; i<input.length; i++) {
            int key = input[i];
            int j = i - 1;
            while(j>=0 && input[j]>key) {
                input[j+1] = input[j];
                j=j - 1;
            }
            input[j+1] = key;
        }
    }
}

2.3 如何选择排序算法

        数据没有特征? -> 快速排序,平均时间短

        基本有序?  -> 插入

        复杂度:插入 ≈ 冒泡 > 快速排序

public static void main(String[] args) {
    int[] arry = {9,8,7,6,5,4,3,2,1};
    quickSort(arry,0, arry.length-1);
    System.out.println(Arrays.toString(arry));
}
//快速排序算法
public static void quickSort(int[] arry,int left,int right){
    //运行判断,如果左边索引大于右边是不合法的,直接return结束次方法
    if(left>right){
        return;
    }
    //定义变量保存基准数
    int base = arry[left];
    //定义变量i,指向最左边
    int i = left;
    //定义j ,指向最右边
    int j = right;
    //当i和j不相遇的时候,再循环中进行检索
    while(i!=j){
        //先由j从右往左检索比基准数小的,如果检索到比基准数小的就停下。
        //如果检索到比基准数大的或者相等的就停下
        while(arry[j]>=base && i<j){
            j--; //j从右往左检索

        }
        while(arry[i]<=base && i<j){
            i++; //i从左往右检索
        }
        //代码走到这里i停下,j也停下,然后交换i和j位置的元素
        int tem = arry[i];
        arry[i] = arry[j];
        arry[j] = tem;


    }
    //如果上面while条件不成立就会跳出这个循环,往下执行
    //如果这个条件不成立就说明 i和j相遇了
    //如果i和j相遇了,就交换基准数这个元素和相遇位置的元素
    //把相遇元素的值赋给基准数这个位置的元素
    arry[left] = arry[i];
    //把基准数赋给相遇位置的元素
    arry[i] = base;
    //基准数在这里递归就为了左边的数比它小,右边的数比它大
    //排序基准数的左边
    quickSort(arry,left,i-1);
    //排右边
    quickSort(arry,j+1,right);

}

三、查找算法常见面试题

3.1 常见查找算法介绍

        顺序查找

        二分查找(数据有序)

        哈希表法(条件:先创建Hash表;原理:hash计算,速度快)

        跳跃搜索(数据有序,通过以固定距离向前跳)

3.2 二分查找

        复杂度分析:O(logn)

public class BinarySearch {
    public static init binarySearch(int[] input, int target) {
        int l = 0;
        int r = input.length - 1; //在[l...r]范围内查找
        while(l <= r) {
            int mid = (l + r) / 2;
            if (input[mid] == target) {
                return mid
            }
            if (target > input[mid]) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
    }
}

四、你知道Java中有几种锁吗

4.1 Lock的作用和地位part1

        锁是一种工具,用于控制对共享资源(可以被多线程访问的内容)的访问

        Java基础,易懂难精(种类多,能不能在合适的场景选择适合的锁)

        面试高频考点

Lock接口

        为什么需要Lock?为什么synchonized不够用?

        1. 效率低:锁的释放情况少;试图获取锁时,不能设定超时;不能中断一个正在试图获得锁的线程;

        2. 不够灵活(读写锁更灵活):加锁和释放的时机单一。进入synchronized代码块或者被它修饰的方法,你就获取锁,退出时就释放锁。

        3. 无法知道是否成功获取到锁。无法根据这个结果进行额外处理。

        Lock简介

        Lock和synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是使用上和功能上又有较大的不同。

        Lock并不是用来代替synchronized的,而是当使用synchronized不合适或者不足以满足要求的时候,来提供高级功能的。

4.2 Lock的作用和地位part2

Lock主要方法介绍

        在Lock中声明了四个方法来获取锁

        lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()

        那么这四个方法有什么区别?

lock()

        lock()就是最普通的获取锁。如果锁已被其他线程获取,则进行等待。

        Lock不会像synchronized一样在异常时自动释放锁。

        因此最佳实践是,在finally中释放锁,以保证发生异常时锁一定被释放。

tryLock()

        tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功并返回true。否则返回false,代表获取锁失败。

        相比于lock(),这样的方法显然功能更强大了,我们可以根据是否能获取到锁来决定后续程序的行为。

        该方法会立即返回,即便在拿不到锁时不会一直在那等。

tryLock(long time, TimeUnit unit)

        超时就放弃

lockInterruptibly()

        相当于tryLock(long time, TimeUnit unit) 把超时时间设置为无限。在等待锁的过程中,线程可以被中断。

unlock()

        解锁。

4.3 锁的分类

        这些分类,是从各种不同角度出发去看的

        这些分类并不是互斥的,也就是多个类型可以并存:有可能一个锁,同时属于两种类型

        比如ReentrantLock既是悲观锁,又是可重入锁

        好比一个人可以同时是男人,又是军人,这个不冲突

Java中有哪几种锁?

        1. 可重入锁和非可重入锁(根据可以被同一线程重复获得区分)

        2. 乐观锁和悲观锁(线程需不需要先锁住资源,再进行操作)

        3. 公平锁和非公平锁(在多个线程竞争的时候,是否很好的进行排队)

        4. 共享锁和排它锁(多个线程能否共享同一把锁)

        5. 自旋锁和阻塞锁(没有获取到锁会不停尝试,直到获取到锁 / 没有获取到锁就休息一会再来)

4.4 可重入锁

ReentrantLock使用案例:

public static void main(String[] args) {
    //bookSeat()的逻辑为 获得锁并订票
    new Thread(() -> bookSeat()).start()
    new Thread(() -> bookSeat()).start()
    new Thread(() -> bookSeat()).start()
    new Thread(() -> bookSeat()).start()
}

可重入性质:

        获取一把锁后,还可以再获取锁,而不用释放已有的锁。

        好处:避免死锁(你等我释放锁,我等你释放锁)(情景 递归方法中有获取锁的代码);提升封装性;

getHoldCount(): 已获得锁的数量

4.5 乐观锁和悲观锁

为什么会诞生乐观锁?

        乐观锁出现在悲观锁之后,说明悲观锁有一定的劣势。

悲观锁的劣势:

        阻塞和唤醒带来的性能劣势。悲观锁它会让某一个线程独占这个资源,其他线程如果想获取相应资源的话,必须等待前面线程释放锁之后,才能得到相应的锁。

        永久阻塞。一直在等待,迟迟等不到。

乐观锁的优势:

        没有阻塞和唤醒

乐观锁:

        从是否锁住资源的角度分类。某部分数据可能有其他线程在同时操作,但是乐观锁在操作此部分数据时先假定不会有其他线程来干扰,所以也不需要锁住被操作对象。也不会要求别人不准接触此部分数据。乐观锁在更新操作的时候,是会对比在我修改期间这个数据有没有被其他人修改过,如果没有修改过,则正常修改数据就可以了。如果此时数据和我一开始拿到的不一样,说明有其他线程在此期间修改过数据,为了确保数据一致性,则刚才的更新操作失败了,放弃操作。

悲观锁:

        为了保证结果正确性,每次进行真正修改之前,先把数据锁住。

典型例子:

        悲观锁 synchronized和Lock接口

        乐观锁 用version控制数据库(实际开发中用的广泛)

开销对比:

        悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸

        相反,虽然乐观锁一开始的开销比悲观锁小,但是如果不停重试,那么消耗的资源也会越来越多

两种锁各自的使用场景

        悲观锁:锁竞争激烈;持有锁时间较长;

        乐观锁:适合并发写入少,大部分是读取的场景,不加锁 能让读取性能大幅提高

4.6 公平锁

        公平指的是按照线程请求的顺序,来分配锁;非公平指的是,不完全按照请求的顺序,在一定情况下,可以插队。

        什么时机可以插队?在我们还没有准备好的时候,允许让别人很快的执行一个事情。相当于提升了整体工作量,又没有耽误自己的工作。

        为什么要有非公平锁?凭什么默认策略是非公平?Java设计者这样设计的目的是为了提升效率;避免唤醒带来的空档期;

公平的情况(以ReentrantLock为例)

        如果在创建ReentrantLock对象时,参数填写为true,那么这就是个公平锁。

对比公平和非公平的优缺点

 4.7 共享锁和排它锁

        排它锁,又称为独占锁、独享锁。ReentrantLock synchronized

        共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据。

        共享锁和排它锁的典型是“读写锁ReentrantReadWriteLock”,其中读锁是共享锁,写锁是独享锁。

读写锁的作用

        在没有读写锁之前,我们假设使用ReentrantLock,虽然保证了线程安全,但是也浪费了一定的资源。多个读操作同时进行,并没有线程安全问题。

        在读的地方使用读锁,在写的地方使用写锁,灵活控制。如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率。

读写锁的规则

        1. 多个线程只申请读锁,都可以申请到

        2. 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁

        3. 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁

        4. 一句话总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现(要么多读,要么一写)

代码演示 

package com.imooc.lock.readwrite;

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 描述:     电影院对读写锁的应用
 */
public class CinemaReadWrite {

    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(()->read(),"Thread 1").start();
        new Thread(()->read(),"Thread 2").start();
        new Thread(()->write(),"Thread 3").start();
        new Thread(()->write(),"Thread 4").start();
    }
}

4.8 自旋锁

自旋锁和阻塞锁

        阻塞或唤醒线程开销大

        如果同步代码块中的内容过于简单,状态转换时间比代码执行时间还要长

        为了这一小段时间去切换线程,得不偿失

        让后面那个请求锁的线程不放弃CPU的执行,看看持有锁的线程是否很快就会释放锁

        为了让当前线程“稍等一下”,当前线程进行自旋,从而避免切换线程的开销

        阻塞锁和自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞,直到被唤醒

自旋锁的缺点

        如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源

        在自旋的过程中,一直消耗CPU,随着自旋时间的增长,开销也增长

自旋锁的适用场景

        自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高

        另外,自旋锁适用于锁住的范围比较短小的情况,否则如果线程一旦拿到锁,很久以后才会释放,那也是不合适的

4.9 面试常见问题之锁篇

        1. Lock有哪几个常用方法?分别有什么用?finally中解锁

        2. 你知道有几种锁?分别有什么特点?五种

        3. 讲讲悲观锁和乐观锁?

        4. 讲一讲你所知道的公平锁和非公平锁,为什么要非公平?

        5. 讲讲读写锁?

        6. 什么是自旋?自旋的好处和后果是什么呢?

五、其他面试常考问题

5.1 ConcurrentHashMap面试常考问题

ConcurrentHashMap的结构

        JDK7: 分段锁 + HashMap

        JDK8: 数组 + 链表 + 树

ConcurrentHashMap、HashMap、HashTable区别

        没有ConcurrentHashMap之前,有HashTable。HashTable也是线程安全的,多个线程可以同时操作它,但它的效率非常低,内部结构采用synchronized去做的。为了替代HashTable,对HashTable进行升级,ConcurrentHashMap出现了。

        hashMap线程不安全。在多线程下使用可能会导致CPU爆满。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chengbo_eva

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值