【面经】百度25届秋招提前批Java研发岗面经及解答

1.HashCode()和equals()方法的关系

        equals()方法用于比较两个对象的地址是否相同,在实际应用中,我们往往会改写equals()方法,使其比较两个对象的值是否相同。hashcode()方法用于返回一个hash值,如果hash值相同,证明两个对象是相同的。

       也就是说,如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任一对象的hashCode()方法都必须产生同样的整数结果。如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任一对象的hashCode()方法不一定要产生不同的整数结果。

        我们往往使用equals()和hashcode()来比较Java中的两个对象是否相等,因此也希望两者的返回结果能保持一致,因此,在我们改写了equals()方法后,必须改写hashcode()方法。

        那么,为什么已经有了equals()方法,还需要hashcode()方法呢?因为重写的equals()里一般比较的比较全面比较复杂,这样效率就比较低,而利用hashCode()进行对比,则只要生成一个hash值进行比较就可以了,效率很高。而为什么有了hashcode()还需要使用equals()呢?因为hashCode()并不是完全可靠,有时候不同的对象他们生成的hashcode也会一样(也就是hash冲突问题)。

        综合而言,有以下几个原则:同一个对象(没有发生过修改)无论何时调用hashCode()得到的返回值必须一样;hashCode()的返回值相等的对象不一定相等,通过hashCode()和equals()必须能唯一确定一个对象;一旦重写了equals()函数(重写equals的时候还要注意要满足自反性、对称性、传递性、一致性),就必须重写hashCode()函数。

2.重载和重写有什么区别?

名称发生范围方法名形参列表返回类型修饰符
重载(overload)本类必须一样类型、个数、顺序至少有一个不同无要求无要求
重写(override)父子类必须一样相同子类重写的方法,返回类型和父类一样或者是父类的子类子类方法不能缩小父类方法的访问范围

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

        数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

        悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制

        乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:乐一般会使用版本号机制或CAS算法实现。

        乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

4.讲一讲CAS原理

        CAS即比较并交换,它是一条 CPU 同步原语。是一种硬件对并发的支持,针对多处理器操作而设计的一种特殊指令,用于管理对共享数据的并发访问。CAS 是一种无锁的非阻塞算法的实现。CAS 包含了 3 个操作数:需要读写的内存值 V;旧的预期值 A;要修改的更新值 。当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作。

5.ABA问题,怎么解决

        要了解什么是ABA问题,首先我们来通俗的看一下这个例子,一家火锅店为了生意推出了一个特别活动,凡是在五一期间的老用户凡是卡里余额小于20的,赠送10元,但是这种活动没人只可享受一次。然后火锅店的后台程序员小王开始工作了,很简单就用cas技术,先去用户卡里的余额,然后包装成AtomicInteger,写一个判断,开启10个线程,然后判断小于20的,一律加20,然后就很开心的交差了。可是过了一段时间,发现账面亏损的厉害,老板起先的预支是2000块,因为店里的会员总共也就100多个,就算每人都符合条件,最多也就2000啊,怎么预支了这么多。小王一下就懵逼了,赶紧debug,tail -f一下日志,这不看不知道,一看吓一跳,有个客户被充值了10次!

        假设有个线程A去判断账户里的钱此时是15,满足条件,直接+20,这时候卡里余额是35.但是此时不巧,正好在连锁店里,这个客人正在消费,又消费了20,此时卡里余额又为15,线程B去执行扫描账户的时候,发现它又小于20,又用过cas给它加了20,这样的话就相当于加了两次,这样循环往复肯定把老板的钱就坑没了!

        ABA问题的根本在于cas在修改变量的时候,无法记录变量的状态,比如修改的次数,否修改过这个变量。这样就很容易在一个线程将A修改成B时,另一个线程又会把B修改成A,造成casd多次执行的问题。

        AtomicStampReference在cas的基础上增加了一个标记stamp,使用这个标记可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。它有以下几个参数:

//参数代表的含义分别是 期望值,写入的新值,期望标记,新标记值
public boolean compareAndSet(V expected,V newReference,int expectedStamp,int newStamp);

public V getRerference();

public int getStamp();

public void set(V newReference,int newStamp);

        我们定义了一个money值为19,然后使用了stamp这个标记,这样每次当cas执行成功的时候都会给原来的标记值+1。而后来的线程来执行的时候就因为stamp不符合条件而使cas无法成功,这就保证了每次只被执行一次。

public class AtomicStampReferenceDemo {
 
    static AtomicStampedReference<Integer>  money =new AtomicStampedReference<Integer>(19,0);
 
    public static void main(String[] args) {
 
        for (int i = 0; i < 3; i++) {
 
            int stamp = money.getStamp();
 
            System.out.println("stamp的值是"+stamp);
 
            new Thread(){         //充值线程
 
                @Override
                public void run() {
 
                        while (true){
 
                            Integer account = money.getReference();
 
                            if (account<20){
 
                                if (money.compareAndSet(account,account+20,stamp,stamp+1)){
 
                                    System.out.println("余额小于20元,充值成功,目前余额:"+money.getReference()+"元");
                                    break;
                                }
                            }else {
 
                                System.out.println("余额大于20元,无需充值");
                            }
                        }
                    }
                }.start();
            }
 
 
            new Thread(){
 
                @Override
                public void run() {    //消费线程
 
                    for (int j = 0; j < 100; j++) {
 
                        while (true){
 
                            int timeStamp = money.getStamp();//1
 
                            int currentMoney =money.getReference();//39
 
                            if (currentMoney>10){
                                System.out.println("当前账户余额大于10元");
                                if (money.compareAndSet(currentMoney,currentMoney-10,timeStamp,timeStamp+1)){
 
                                    System.out.println("消费者成功消费10元,余额"+money.getReference());
 
                                    break;
                                }
                            }else {
                                System.out.println("没有足够的金额");
 
                                break;
                            }
                            try {
                                Thread.sleep(1000);
                            }catch (Exception ex){
                                ex.printStackTrace();
                                break;
                            }
 
                        }
 
                    }
                }
            }.start();
 
        }
    }

        这样实现了线程去充值和消费,通过stamp这个标记属性来记录cas每次设置值的操作,而下一次再cas操作时,由于期望的stamp与现有的stamp不一样,因此就会设值失败,从而杜绝了ABA问题的复现。

        以上内容摘自:经典的ABA问题与解决方法-CSDN博客

6.synchroized是什么?jdk8以前和以后有什么区别

        synchronized是Java中的一个关键字,用于实现线程同步和互斥访问共享资源,可以看作是一种对象锁(锁的是对象而非引用)。

        synchronized在JDK 8以前通常被认为是“重量级锁”,因为其实现较为简单,没有太多的优化,导致了较多的上下文切换和线程阻塞,性能相对不高;主要基于JVM内置的Monitor(监视器锁)实现,通过进入与退出Monitor对象来实现同步;锁机制相对单一,没有像JDK 1.6及以后版本那样的锁升级策略(如偏向锁、轻量级锁)。

        在JDK8之后,引入了锁的分层机制和适应性自旋锁等优化技术,极大地提高了synchronized的性能;在JDK 1.8中,进一步的优化使得synchronized的性能更加接近甚至有时超过ReentrantLock,特别是在轻量级锁和偏向锁的使用上;此外,JDK8还采用了锁升级机制,从偏向锁升级到轻量级锁,再到重量级锁,以减少线程挂起和唤醒的开销(【面试】并发常考:请你谈谈 synchronized 锁的升级-CSDN博客)。

7.线程的状态,分别介绍

1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

  • 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
  • 同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
  • 其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时. join()等待线程终止或者超时. 或者I/O处理完毕时,线程重新转入就绪状态。

5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

8.出现SQL慢查询,怎么排查

9.异常处理相关的注解,怎么定义和捕获全局异常处理器

        使用@ExceptionHandler注解。@ExceptionHandler注解可以用来定义一个方法,该方法用于处理特定类型的异常。这个注解可以用在@Controller或@RestController注解的类中。

@Controller  
public class MyController {  
  
    @ExceptionHandler(value = Exception.class)  
    public ResponseEntity<Object> handleException(Exception e) {  
        // 处理异常  
        return new ResponseEntity<>("Exception occurred", HttpStatus.INTERNAL_SERVER_ERROR);  
    }  
}

         如果想对所有的控制器进行统一的异常处理,可以定义一个全局异常处理器。通过实现HandlerExceptionResolver接口或者使用@ControllerAdvice注解。

@ControllerAdvice  
public class GlobalExceptionHandler {  
  
    @ExceptionHandler(value = Exception.class)  
    public ResponseEntity<Object> handleException(Exception e) {  
        // 处理异常  
        return new ResponseEntity<>("Global Exception occurred", HttpStatus.INTERNAL_SERVER_ERROR);  
    }  
}

        也可以自定义异常类。

public class MyCustomException extends RuntimeException {  
    public MyCustomException(String message) {  
        super(message);  
    }  
}

10.SpringBoot中传入参数校验的注解

        requestBody参数校验:POST、PUT请求一般会使用requestBody传递参数,这种情况下,后端使用DTO对象进行接收。只要给DTO对象加上@Validated注解就能实现自动参数校验。比如,有一个保存User的接口,要求userName长度是2-10,account和password字段长度是6-20。如果校验失败,会抛出MethodArgumentNotValidException异常,Spring默认会将其转为400(Bad Request)请求。

        requestParam/PathVariable参数校验:GET请求一般会使用requestParam/PathVariable传参。如果参数比较多(比如超过6个),还是推荐使用DTO对象接收。否则,推荐将一个个参数平铺到方法入参中。在这种情况下,必须在Controller类上标注@Validated注解,并在入参上声明约束注解(如@Min等)。如果校验失败,会抛出ConstraintViolationException异常。

        更多相关内容可以参考:SpringBoot实现各种参数校验,写得太好了,建议收藏!_springboot get请求参数做校验-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_69632475/article/details/136531252

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值