2021春招Java面试题大全(精华五)

返回首页

66.Mysql的锁机制

在Mysql中的锁可以分为共享锁/读锁(Shared Locks)、排他锁/写锁(Exclusive Locks) 、行锁(Record Locks)、表锁。

共享锁是针对同一份数据,多个读操作可以同时进行,但不能进行写操作。排他锁针对写操作,假如当前写操作没有完成,那么它会阻断其它的写锁和读锁。

而行锁和表锁,是从锁的粒度上进行划分的,

  • 行锁锁定当前数据行:
    • 锁的粒度小,发生锁冲突的概率小,并发度高
    • 加锁慢,容易产生死锁
  • 而表锁则锁的整个表:
    • 粒度大,加锁快,开销小,不容易产生死锁
    • 但是锁冲突的概率大,并发度低

锁冲突:例如说事务A将某几行上锁后,事务B又对其上锁,锁不能共存否则会出现锁冲突。(但是共享锁可以共存,共享锁和排它锁不能共存,排它锁和排它锁也不可以)。
死锁:例如说两个事务,事务A锁住了1-5行,同时事务B锁住了6-10行,此时事务A请求锁住6-10行,就会阻塞直到事务B施放6-10行的锁,而随后事务B又请求锁住1-5行,事务B也阻塞直到事务A释放1~5行的锁。死锁发生时,会产生Deadlock错误。

拓展

从对数据操作的类型(读、写)分:

增加读写锁的语句:lock table 表名1 read(write), 表名2 read(write);

读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会相互影响。

  • 我们为一个表加上读锁以后
    • 我们和其他人依然可以查询这张表
    • 我们插入或者跟更新这个锁定的表都会提示错误
    • 其他人插入或者更新这个锁定的表会一直等待获得锁
    • 在我们没有释放锁之前,我们不能查其他的没有锁定的表(因为我们还没有释放锁,前面加锁的账还没还清)。其他人可以查询其他未锁定的表

注意:

我们锁定一个表以后,一定还要给他的别名加上锁,否则会报错!!!

  • 对actor表获得读锁:

    mysql> lock table actor read; 
    
    Query OK, 0 rows affected (0.00 sec)
    
  • 但是通过别名访问会提示错误:

    mysql> select a.first_name,a.last_name,b.first_name,b.last_name 
    from actor a,actor b 
    where a.first_name = b.first_name and a.first_name = 'Lisa' and a.last_name = 'Tom' 
    and a.last_name <> b.last_name;
    
    ERROR 1100 (HY000): Table ‘a’ was not locked with LOCK TABLES
    
  • 需要对别名分别锁定:

    mysql> lock table actor as a read,actor as b read;
    
    Query OK, 0 rows affected (0.00 sec)
    
  • 按照别名的查询可以正确执行:

    mysql> select a.first_name,a.last_name,b.first_name,b.last_name 
    from actor a,actor b where a.first_name = b.first_name 
    and a.first_name = 'Lisa' and a.last_name = 'Tom' 
    and a.last_name <> b.last_name;
    

写锁(排它锁):当前写操作没有完成前,他会阻断其他写锁和读锁。

  • 我们为一个表加上写锁以后
    • 我们可以对这张表进行插入+更新+查询的操作
    • 其他人对标的查询会被阻塞,需要等待锁被释放

从对数据操作的颗粒度分

  • 表锁(偏读)

    • 偏向MyISAM存储引擎,开销小,加锁快;无死锁;锁定粒度大,发送锁冲突的概率最高,并发度低。
    • 查看表上加过的锁:show open tables;
    • 手动增加表锁:lock table 表名1 read(write), 表名2 read(write);
    • 释放锁:unlock tables;
  • 行锁(偏写)

    • 偏向InnoDB存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

    • 行锁有两种类型

      • 共享锁(s):又称读锁。 允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
      • 排他锁(X):又称写锁。 允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。

      对于排他锁大家的理解可能就有些差别,我当初就犯了一个错误,以为排他锁锁住一行数据后,其他事务就不能读取和修改该行数据,其实不是这样的。排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁。mysql InnoDB引擎默认的修改数据语句:update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。

    • 对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁。 事务可以通过以下语句显式给记录集加共享锁或排他锁:

      • 共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
      • 排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE 悲观锁

InnoDB行锁是通过给索引上的索引项加锁来实现的,所以只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!

一句话:InnoDB的行锁是基于索引实现的,如果不通过索引访问数据,InnoDB会使用表锁。

索引失效了行锁会变为表锁,效率降低

什么是间隙锁:

当我们用范围条件检索数据,并请求共享或排它锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP )”

InnoDB也会对这个“间隙“加锁,这种锁机制就是所谓的间隙锁

InnoDB使用间隙锁的目的,是为了防止幻读,

危害

  • 因为Query执行过程中通过范围查找的话,他会锁定整个范围内所有的索引键值,及时这个键值并不存在。
  • 间隙锁有一个比较致命的缺点,就是当锁定一个范围键值后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害。因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。

如何分析行锁:show status like 'innodb_row_lock%';

67.drop、delete与truncate的区别

Delete Truncate Drop
类型 属于DML 属于DDL 属于DDL
回滚 可回滚 不可回滚 不可回滚
删除内容 表结构还在,删除表的全部或者一部分数据行 表结构还在,删除表中的所有数据 从数据库中删除表,所有的数据行,索引和权限也会被删除
删除速度 删除速度慢,需要逐行删除 删除速度快 删除速度最快

delete删除数据后,重新往表里添加数据,他的自增id值会接着删除点往下自增,而truncate是从1开始自增。

因此,在不再需要一张表的时候,用drop;在想删除部分数据行时候,用delete;在保留表而删除所有数据的时候用truncate。

68.如何定位及优化SQL语句的性能问题?创建的索引有没有被使用到?或者说怎么才可以知道这条语句运行很慢的原因?

MySQL提供了explain命令来查看语句的执行计划。对于查询语句,最重要的优化方式就是使用索引。 而执行计划,就是显示数据库引擎对于SQL语句的执行的详细情况,其中包含了是否使用索引,使用什么索引,使用的索引的相关信息等
一些参数的含义:

  • type:表示表的连接类型

    • ALL:Full Table Scan, MySQL将遍历全表以找到匹配的行

      index: Full Index Scan,index与ALL区别为index类型只遍历索引树

      ref: 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值

  • possible_keys:表示查询时,可能使用的索引

  • key:表示实际使用的索引

  • key_len:索引字段的长度

69.Comparable和Comparator

  • 自然排序:java.lang.Comparable (实现Compareable接口,重写compareTo()方法)

    • Comparable接口会强行对实现它的每个类的对象进行整体排序,这种排序被称为类的自然排序。例如String类、包装类都实现了Comparable接口重写了CompareTo()方法,而且默认都是从小到大排列的。
    //实现Compareable接口,重写compareTo()方法
    public class Good implements Comparable<Good>{
           
           
        public String name;
        public double price;
    
        public Good() {
           
           
        }
    
        public Good(String name, double price) {
           
           
            this.name = name;
            this.price = price;
        }
    
        @Override
        public String toString() {
           
           
            return "Good{" +
                    "name='" + name + '\'' +
                    ", price=" + price +
                    '}';
        }
    
    //重写compareTo方法
        @Override
        public int compareTo(Good good) {
           
           
            if (this.price>good.price){
           
           
                return 1;
            }else if(this.price<good.price){
           
           
                return -1;
            }else{
           
           
                return 0;
            }
        }
    }
    
    public class CompareTest {
           
           
        public static void main(String[] args) {
           
           
            Good[] goods =new Good[4];
            goods[0]=new Good("huaweiPhone",5000);
            goods[1]=new Good("vivoPhone",4000);
            goods[2]=new Good("oppoPhone",3000);
            goods[3]=new Good("xiaomiPhone",2000);
            Arrays.sort(goods);
            System.out.println(Arrays.toString(goods));
        }
    }
    
  • 定制排序:java.util.Comparator (实现Comparator接口重写compare(Object o1,Object o2)方法)

    • 当对象的类型没有实现Comparable接口而又不方便修改代码,或者实现了Comparable接口,但排序规则不适合当前的操作,那么可以考虑使用Comparator强行对多个对象进行整体排序。
    public class Grade {
           
           
        public String name;
        public int score;
    
        public Grade(String name, int score) {
           
           
            this.name = name;
            this.score = score;
        }
    
        @Override
        public String toString() {
           
           
            return "Grade{" +
                    "name='" + name + '\'' +
                    ", score=" + score +
                    '}';
        }
    }
    
    public class GradeComparator implements Comparator<Grade> {
           
           
    
        @Override
        public int compare(Grade grade1, Grade grade2) {
           
           
            if(grade1.score>grade2.score){
           
           
                return 1;
            }else if(grade1.score<grade2.score){
           
           
                return -1;
            }else{
           
           
                return 0;
            }
        }
    }
    
    public class CompareTest {
           
           
        public static void main(String[] args) {
           
           
            Grade[] grades=new Grade[4];
            grades[0]=new Grade("Tom",72);
            grades[1]=new Grade("Marry",66);
            grades[2]=new Grade("Jack",86);
            grades[3]=new Grade("Luck",56);
    
            Arrays.sort(grades,new GradeComparator());
            System.out.println(Arrays.toString(grades));
        }
    }
    

总结:

(1)使用Comparable进行排序时,需要对象本身实现Comparable接口并重写compareTo()方法,耦合性较高;而使用Comparator进行排序时,可以在外部实现Comparator接口重写compare()方法,耦合性较低;
(2)如果实现类没有实现Comparable接口,又想对两个类进行比较或者实现类实现了Comparable接口,但是对compareTo方法内的比较算法不满意,那么就可以实现Comparator接口,自定义一个比较器,写比较算法;
(3)Comparable和一个具体类绑定在一起,使用比较固定;而Comparator使用比较灵活,可以被用于各个需要比较功能的类;
(4)如果一个类需要进行多种比较排序,只能使用Comparator,而不能使用Comparable。

70.fail-fast和 fail-safe

它是Java集合的一种错误检测机制

fail-fast机制

fail-fast机制在遍历一个集合时,当集合结构被修改,会抛出Concurrent Modification Exception。

fail-fast抛出异常的两种情况

  • 单线程环境,在遍历他的时候修改了结构(remove()方法会让expectModcount和modcount 相等,所以是不会抛出这个异常。)
  • 多线程环境,当一个线程在遍历这个集合,而另一个线程对这个集合的结构进行了修改。

fail-fast机制是如何检测的?

迭代器在遍历过程中是直接访问内部数据的,因此内部的数据在遍历的过程中无法被修改。为了保证不被修改,迭代器内部维护了一个标记 “modeCount” ,当集合结构改变(添加删除或者修改),标记"modeCount"会被修改,而迭代器每次的hasNext()和next()方法都会检查该"modeCount"是否被改变,看modeCount是否等于expectedModCount,当检测到被修改时,抛出ConcurrentModificationException。

fail-safe机制

fail-safe任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出ConcurrentModificationException。他有一个问题就是需要复制集合,产生大量的无效对象,开销大。

Fail Fast Iterator Fail Safe Iterator
Throw ConcurrentModification Exception Yes No
Clone object No Yes
Memory Overhead No Yes
Examples HashMap,Vector,ArrayList,HashSet CopyOnWriteArrayList, ConcurrentHashMap

71.SpringBoot常用注解

  • @SpringBootApplication

    • 包含@Configuration、@EnableAutoConfiguration、@ComponentScan,通常用在主类上。
    • @EnableAutoConfiguration:让 Spring Boot 根据应用所声明的依赖来对 Spring 框架进行自动配置
  • 在启动类中打开事务扫描器:@EnableTransactionManagement。然后在Service层对应的类或者进行DML的语句的方法上加上@Transactional注解

  • @Reposity:DAO组件

  • @Service:业务层组件

  • @RestController:控制层组件

    • 包含@Controller和@ResponseBody
  • @ResponseBody

    • 表示该方法的返回结果直接写入HTTP response body中。 一般在异步获取数据时使用,在使用@RequestMapping后,返回值通常解析为跳转路径,加上@responsebody后返回结果不会被解析为跳转路径,而是直接写入HTTP response body中。比如异步获取json数据,加上@responsebody后,会直接返回json数据。
  • @ComponentScan:组件扫描。如果扫描到有@Component @Controller @Service等这些注解的类,则把这些类注册为bean。

  • @Configuration:原来xml配置文件中的信息可以写在这里。

  • @Bean:相当于XML中的<bean> </bean>,放在方法的上面,而不是类,意思是产生一个bean,并交给spring管理。

  • @AutoWired:按类型自动注入属性。

  • @Resource(name=“name”,type=“type”):没有括号内内容的话,默认byName。与@Autowired干类似的事。

  • @RequestMapping:是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。

72.抽象类和接口的区别

  • 抽象类要被子类继承extends,接口要被类实现implements。
  • 抽象类有构造方法,接口没有抽象方法。但是他们都不能被实例化
  • 抽象类单继承,接口可多实现
  • 接口里定义的变量只能是公共的静态的常量,即PSF,抽象类中的变量是普通变量。
  • 接口中的方法都是public abstract的,没有方法体(1.8以后有了default和静态方法可以有方法体,1.9以后有private方法),而抽象类中有private方法,非abstract的方法可以有实现体。

73.什么是红黑树?

首先说一下二叉查找树,就是左子树上所有结点的值均小于或等于它的根结点的值。右子树上所有结点的值均大于或等于它的根结点的值。这种查询速度很快,但是插入元素时具有缺陷。容易让树变为瘸子,查找效率降低了。为了解决二叉查找树多次插入新节点导致的不平衡,我们就引入了红黑树。他是一个自平衡的二叉查找树,它符合二叉查找树的基本特征,并且具有一些新的特性:

  • 根节点是黑色,每个叶子节点都是黑色的空节点,每个红色节点的两个子节点都是黑色。
  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
  • 红黑树从根到叶子节点的最长路径不会超过最短路径的2倍。
  • 当插入和删除节点时,红黑树的规则可能会被打破,这就需要进行一定的调整。
    • 左旋:逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子。
    • 右旋:顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子。

74.HashMap为什么不使用AVL树而使用红黑树?

AVL树和红黑树有几点比较和区别:
(1)AVL树是更加严格的平衡,因此可以提供更快的查找速度,一般读取查找密集型任务,适用AVL树。
(2)红黑树更适合于插入修改密集型任务。
(3)通常,AVL树的旋转比红黑树的旋转更加难以平衡和调试。

红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。

平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。

75.为什么HashMap链表长度超过8会转成树结构

因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。

还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

76.HashMap在jdk8 和 jdk7的底层实现方面的区别

  1. new HashMap():底层没有创建一个长度为16的数组

  2. jdk8底层的数组是:Node[],而非Entry[]

  3. 首次调用put()方法时,底层创建长度为16的数组

  4. jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。

77.HashMap底层实现

HashSet的底层实现:

HashSet的底层数据结构:就是HashMap

那为什么HashMap一次要放两个值,而HashSet只放一个值

首先1.7之前HashMap的底层是数组+链表,之后做了一个优化变为了数组+链表+红黑树

放值的过程

HashMap 在 put 键值对的时候会调用 hash() 方法计算 key 的 hash 值,hash() 方法会调用 Object 的 native 方法 hashCode() 并且将计算之后的 hash 值高低位做异或运算,以增加 hash 的复杂度,减少hash碰撞,接下来用这个hash值和当前的数组长度-1做与运算得到他应该放在数组的那个位置上。

map.put(key1,value1);

得到数据应该放在数组的存放位置后。

如果此位置上的数据为空,此时的key1-value1添加成功 ------情况1

如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:

​ 如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。------情况2

​ 如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)比较:

​ 如果equals()返回fasle:此时key1-value1添加成功。(JDK7新添加的元素将放在链表的头部,即:头插法。头插法会造成死链的问题,JDK8变为尾插法)------情况3

​ 如果equals()返回true:使用value1替换value2。(相当于是把添加过程变为了修改过程 )

我们在添加元素的过程中可能存在一个hash碰撞的过程,如果发生了hash碰撞,那么就在后面追加链表,或者树。

补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。

在不断的添加过程中,会涉及到扩容问题,当超出临界值时(比如初始16,元素值为12时不会扩容,在添加一个时会扩容)(且要存放数组位置非空)扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。

扩容

数组扩容时再哈希(re-hash)的理解

re-hash 再哈希问题:HashMap 扩容的时候会重新计算每一个元素的索引

当 HashMap 在 put 元素的时候,如果触发扩容机制,HashMap 会调用 resize() 方法来重新计算数组容量,这个resize的具体过程是hash&(newCapcity-1),因为容量的2的倍数,所以newCapcity-1之后是全1的,所以如果hash的高位为1,那么新的位置就是原来旧数组的位置+原数组长度,如果hash的高位是0,那么在新数组的位置就是原来数组中的位置,这样直接判断高位就可以计算出位置了,可以减少rehash的过程。

扩容以后元素在桶中就分布的更均匀了,链表长度也就缩短了,提高了查找效率。

树化

当数组的某一个索引位置上的元素以链表的形式存在的数组个数 > 8 且当前数组的长度 > 64时(且的后面的判断是为了避免数组太短而树形结构太多,比如:如果链表的个数>8,按理说需要形成红黑树了,但是他先判断此时的数组长度是不是小于64,如果当前数组容量小于64,则扩容数组,而不是先生成红黑树(因为扩容在一定程度上也可以减少链表长度)),此时此索引位置上的所有数组改为使用红黑树存储。

时间复杂度
HashMap的查询复杂度最快可到O(1)(这种情况是直接查找到我们数组就找到了元素值),不好的时候是O(longN)(这种情况是有可能发生了hash碰撞,需要采用链表或者树的形式存储值,我们找到这个元素刚好在某个链表上,那查询的复杂度会是O(n),在1.8以后达到了链表长度得到了8以后就会转为树,这时查询速度就升到了O(longn))。TreeMap的时间复杂度是O(longN)

78.Synchronized和Lock的底层的区别

  • 原始构成
    • synchronized是关键字属于JVM层面的
    • Lock是具体的类
  • 使用方法
    • synchronized不需要用户去手动释放锁,当synchronized代码执行完成后系统会自动让线程释放对锁的占用
    • ReentrantLock则需要用户去手动释放锁若没有主动释放锁,就有可能导致出现死锁现象。(需要lock和unlock方法配合try/finally语句块来完成)
  • 等待是否可中断
    • synchronized不可中断,除非抛出异常或者正常运行完成
    • ReentrantLock可中断:
      • 设置超时方法trylock(long timeout, TimeUnit unit)
      • lockInterruptibly()放代码块中,调用interrupt()方法可中断
  • 加锁是否公平
    • synchronized非公ping锁
    • ReentrantLock两者都可以,默认非公平锁,构造方法可以传入boolean值,true为公平锁
  • 锁绑定多个条件Condition
    • synchronized没有
    • ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

79.怎么理解多线程

多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。

80.为什么要用 Redis

高性能:

假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

高并发:

直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

猜你喜欢

转载自blog.csdn.net/ysf15609260848/article/details/114386039