百度面试经历
意外之喜
因为疫情原因,又是从国外回来,耽误了不少时间,本以为已经赶不上校招的末班车了。抱着试一试的心态投了百度的简历,没想到收到了百度的面试通知了。
也不废话,聊一聊面试的时候都说了啥。顺便复盘面试,把解决的未解决的问题都记录下来。
实习经历
简单闻了一下实习的经历,以及使用到的技术。基于提到内容提问。比如sql语句。
提到了一个如何计算表中某列的重复数据。
我想到了至少有两种方法:
其中一种是:
Select count(1) from xxx where name = ‘zhangsan’;
另一种是:准备用Group by来实现。
结果一下子想不起来Group by怎么用了。(捂脸)
- Group by
GROUP BY 语句用于结合聚合函数,根据一个或多个列对结果集进行分组。SELECT column_name, aggregate_function(column_name) FROM table_name WHERE column_name operator value GROUP BY column_name;
- Having
在 SQL 中增加 HAVING 子句原因是,WHERE 关键字无法与聚合函数一起使用。HAVING 子句可以让我们筛选分组后的各组数据。SELECT column_name, aggregate_function(column_name) FROM table_name WHERE column_name operator value GROUP BY column_name HAVING aggregate_function(column_name) operator value;
项目经历
讲述了自己在学生时期做的项目,里面用到了什么技术。因为项目调用了第三方api,面试官认为这是分布式系统,并问我如何保证数据的一致性。
当时并不清楚如何解答,只是说了代码的逻辑。事后看了下面的文章,我应该是用了规避分布式事务——业务整合这个方法。嗯。结果就是性能下降。(捂脸)
6种方案:
- 规避分布式事务——业务整合
- 经典方案 - eBay 模式 (分布式处理的任务通过消息日志的方式来异步执行)
- 去哪儿网分布式事务方案(将分布式事务转换为多个本地事务,然后依靠重试等方式达到最终一致性)
- 蘑菇街交易创建过程中的分布式一致性方案
- 支付宝及蚂蚁金融云的分布式服务 DTS 方案
- 农信网数据一致性方案
设计模式
在讲解项目时提到了使用的设计模式。于是,面试官基于设计模式提出了如下问题。什么是单例及其种类。什么是策略模式和状态模式及其本质的区别。单例、策略和状态模式的概念我都懂,但是一深入,我又抓瞎了。这里补习一下。
单例模式
单例模式(Singleton Pattern)属于创建型模式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
《Head First 设计模式》中对它的定义是:单件模式(翻译的不同)确保一个类只有一个实例,并提供一个全局访问点。
主要解决
一个全局使用的类频繁地创建与销毁。
优点
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 避免对资源的多重占用(比如写文件操作)。
缺点
- 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景
- 要求生产唯一序列号
- WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
注意事项
getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
单例模式有什么种类
饿汉模式(线程安全,可用)
注意static关键字,这说明该模式在类加载时就完成了对象的创建。优势是获取对象快,但是类加载较慢。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){
}
public static Singleton getInstance() {
return instance;
}
}
懒汉模式(线程不安全,不可用)
懒汉模式声明了一个静态对象,在用户第一次调用时初始化。
虽然节约了资源,但第一次加载时需要实例化,要慢一些,而且在多线程会产生冲突。
举个栗子:线程A和线程B同时走到了第6行代码时,A和B会各自创建一个实例。
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉模式(线程安全、不推荐用)
synchronized关键字修饰了getInstance方法,保证每次只被一个线程调用。但创建过后,某些需求下,getInstance就不需要锁了,单例可以同时被多个线程调用。这时看来,造成了不必要的同步开销。
public class Singleton {
private static Singleton instance;
private Singleton (){
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
双重检查模式 (DCL、线程安全、推荐使用)
该模式下,对实例化过程加上了同步锁。并且,同时还在同步块内加上了第二次null判断。在实现延迟加载的情况下,保证了线程安全。
这样,即使像上述提到的,B线程在第6行代码为true,但是
- 先会因为没获得锁而进入阻塞状态。
- 获得锁后,再判定是否为null,同样防止了二次创建。
除此之外,使用volatile修饰了instance,保证有序性,防止代码重排,为什么要防止呢?
首先,在java中new这个过程不是原子的。它分为三个步骤:
- 申请空间。
- 在空间中创建实例。
- 把空间赋值给instance。(这时,instance才不为null)
JVM编译的时候会优化代码(优化顺序为其中一种),只要优化后的代码不改变输出结果。这个过程中,2和3的可能会被重排,导致其他线程获得分配了空间但是没有实例化的instance。
public class Singleton {
//保证instance的有序性。
private volatile static Singleton instance;
private Singleton (){
}
public static Singleton getInstance() {
if (instance== null) {
//防止出现上文提到的,B线程也判定instance == null时,各自创建单例
synchronized (Singleton.class) {
if (instance== null) {
instance= new Singleton();
}
}
}
return singleton;
}
}
DCL虽然在一定程度解决了资源的消耗和多余的同步,线程安全等问题,但是他还是在某些情况会出现失效的问题,也就是DCL失效,在《java并发编程实践》一书建议用静态内部类单例模式来替代DCL。
静态内部类单例模式(线程安全、强烈推荐使用)
该模式保证的线程安全,延迟加载,同时效率还高。
该模式和饿汉模式类似,但又不同。
相同点:
- 采用了类装载的机制,保证只有一个线程初始化实例
不同点:
- 饿汉模式,Singleton类被装载就实例化,没有Lazy-Loading的作用
- 静态内部类模式,Singleton类被装载时,并不会立即实例化。只有调用getInstance方法,才会装载SingletonHolder类,完成sInstance的实例化。
public class Singleton {
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}
详情请查看:设计模式(二)单例模式的七种写法_刘望舒的专栏-CSDN博客
详情请查看:单例模式的七种写法比较_ybb520chongren_的博客-CSDN博客
详情请查看:彻头彻尾理解单例模式与多线程_Rico’s Blogs-CSDN博客
策略模式
《Head First 设计模式》一书中对策略模式这样定义的:策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用它的客户。
因此,策略模式主要解决的是:将一组可相互替换的行为(互相替换的行为就是变化),封装为一个接口下一组的算法。从而达到封装变化,对修改关闭,对扩展开发的效果。
Note: 这些行为可能因为业务需求而增加,或修改。若使用了继承或者实现接口,还是需要修改大量的代码,达不到对修改关闭的效果。而策略模式使用的是组合形式,将一个接口作为成员变量。根据业务需求赋予行为主体不同的算法。算法由专门的实现类来实现,即使增加修改或者删除,都不会影响到原有的代码。
主要解决
在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护。
Note: 这句话引用自 策略模式 | 菜鸟教程, 但个人认为这句话说得一点都不对。原因如下:
- 确实将相似的算法独立出来,是一个好想法,这样封装了变化。对扩展开发,对修改关闭。但是这并不解决多个if…else带了的复杂和难以维护。
- 算法被独立出来了,方法内不需要多重判断来选择具体的操作了,这只代表判断被外移了(看下方代码demo)。照样难以维护。
- 真正解决多个if…else问题的模式,应该是工厂模式。工厂模式配合策略模式一起使用,才能达到效果。
优点
- 算法可以自由切换。
- 避免使用多重条件判断。 (个人不认同,必须配合工厂模式才能避免)
- 扩展性良好,避免类膨胀。当类的某种功能有多种实现时,可以定义策略接口,将功能实现委托给策略实现类。
缺点
- 策略类会增多。
- 所有策略类都需要对外暴露。违背了迪米特法则。Context类需要知道有什么策略类,才能根据条件new具体策略。同样,配合工厂模式可以解决。
使用场景
- 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
- 一个系统需要动态地在几种算法中选择一种。 3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
注意事项
如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
实现代码
1. 创建策略类接口
public interface Strategy {
public int doOperation(int num1, int num2);
}
2. 创建接口的实现类
//加法操作
public class OperationAdd implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
//减法操作
public class OperationSubtract implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
//乘法操作
public class OperationMultiply implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}
3. 创建Context类
Context类一般就是行为的主体。
public class Context {
private Strategy strategy;
public Context(Strategy strategy){
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2){
return strategy.doOperation(num1, num2);
}
}
4. 策略模式范例
正如上文提到的,并不能解决多重判断问题,只是把判断外移到Context或Demo
类中了。Demo根据业务需求(多重判断),给行为主体(Context)设置不同的行为(算法)。
public class StrategyPatternDemo {
public static void main(String[] args) {
if (arg[1] == "+") {
Context context = new Context(new OperationAdd());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
}
if (arg[1] == "-") {
context = new Context(new OperationSubtract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
}
if (arg[1] == "*") {
context = new Context(new OperationMultiply());
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
}
}
}
详情请查看:策略模式 | 菜鸟教程
详情请查看:深入解析策略模式_tugangkai的专栏-CSDN博客
详情请查看:设计模式中的多态——策略模式详解 - takumiCX - 博客园
状态模式
状态模式和策略模式是很相似的模式,都是将行为抽出,化为一个接口和其实现类。通过替换实现类,来实现行为的改变。
《Head First 设计模式》中是这样对状态模式定义的:允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
主要解决
对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。
优点
- 封装了转换规则。
- 枚举可能的状态,在枚举状态之前需要确定状态种类。
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
- 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
缺点
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
使用场景
对象内部的状态转换的条件过于复杂,且客户端调用之前不需要了解具体状态。
将状态的判断逻辑转到表现不同状态的一系列状态类当中,可以把复杂的判断逻辑简化。维持开闭原则,方便维护。
注意事项
在行为受状态约束的时候使用状态模式,而且状态不超过 5 个。
实现代码
1. 创建状态类接口
public interface State {
public void doAction(Context context);
}
2. 创建接口的实现类
将多重的判断逻辑,通过doAction()
简化,让代码逻辑变得更清晰,也易于维护。
状态的转换:
//开始状态
public class StartState implements State {
public void doAction(Context context) {
if (context.getOn() == true) {
System.out.println("Player is in start state");
//除了做简单的判断,还可以在业务逻辑完成之后,将context类的状态改变。
context.setOn(false);
} else {
context.setState(new StopState());
context.doAction();
}
}
}
//停止操作
public class StopState implements State {
public void doAction(Context context) {
if (context.getOn() == false) {
System.out.println("Player is in stop state");
//除了做简单的判断,还可以在业务逻辑完成之后,将context类的状态改变。
context.setOn(true);
} else {
context.setState(new StartState());
context.doAction();
}
}
}
3. 创建Context类
Context类一般就是行为的主体。Context直接调用状态state
,来实施行为。
public class Context {
private boolean on;
private State state;
public Context() {
state = new StartState();
on = true;
}
public void setState(State state) {
this.state = state;
}
public State getState() {
return state;
}
public boolean getOn() {
return on;
}
public void setOn(boolean on) {
this.on = on;
}
public void doAction() {
state.doAction(this);
}
}
4. 状态模式范例
创建Context后,可根据需要调用setOn()
来修改对象的状态。除此之外,Context的状态on
会在行为实施后自动发生改变。
public class StatePatternDemo {
public static void main(String[] args) {
Context context = new Context();
context.doAction();
//context.setOn(true);
context.doAction();
}
}
详情请查看:状态模式 | 菜鸟教程(请不要学习该链接中的代码实现,它的代码时错误的,且误导性极大。)
详情请查看: 策略模式的孪生兄弟——对状态模式的深度复习总结
策略模式和状态模式的本质区别
策略模式下,同接口的实现类是平等的关系。
状态模式下,通接口的实现类关系并不平等,只能由一个状态转向另一个状态。
这是面试官的原话。
第一句话还能理解。
第二句话是类似状态机的意思。下一个状态 由 上一个状态 [+ 输入变量(不一定需要)] 决定。并不是想变为哪个行为模式,就变为哪个行为模式的。
举个栗子:ABC三个行为模式。输入变量 X1,X2
行为的变换并不是直接传入一个实现类就行了。而是需要按逻辑转化状态才行。如上图,B永远不可能变换为A。但是策略模式下,可以直接用A替换B。
也就是说,策略模式的实现类可以随意替换(即平等)。但是状态模式的实现类替换,必须符合状态的变换逻辑。
详情请查看: 策略模式的孪生兄弟——对状态模式的深度复习总结
Java
原子性
-
原子性是什么?
原子性是java中一种性质,表示操作不能被分割。比如count++操作会一次性被执行完
不会被打断。而new操作,因为会被分成三个步骤进行:
1. 获取内存空间
2. 实例化内存空间
3. 将内存空间赋值给变量
这个过程会被并发操作打乱,所以new不具备原子性。 -
作用:原子性的操作,可以保证数据线程安全。
-
如何实现?
- reentrantLock
- sychronized关键字
- JUC中的atomic包类,其操作都是具备原子性的。
可见性
- 可见性是什么?
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
总结
虽然结果不是很理想,但是面试官是一个很和善的人。非常感谢他在我一时记不起名词给的提醒,也很感谢他在我思索问题时的循循善诱,以及最后对我的告诫。让我深刻的了解的自己的不足。
我还会再回来的。