总结
虽然面试套路众多,但对于技术面试来说,主要还是考察一个人的技术能力和沟通能力。不同类型的面试官根据自身的理解问的问题也不尽相同,没有规律可循。
上面提到的关于这些JAVA基础、三大框架、项目经验、并发编程、JVM及调优、网络、设计模式、spring+mybatis源码解读、Mysql调优、分布式监控、消息队列、分布式存储等等面试题笔记及资料
有些面试官喜欢问自己擅长的问题,比如在实际编程中遇到的或者他自己一直在琢磨的这方面的问题,还有些面试官,尤其是大厂的比如 BAT 的面试官喜欢问面试者认为自己擅长的,然后通过提问的方式深挖细节,刨根到底。
解决顺序死锁的办法其实就是保证所有线程以相同的顺序获取锁就行。
3.2 动态锁顺序死锁
3.2.1 动态锁顺序死锁的产生与示例
动态锁顺序死锁与上面的锁顺序死锁其实最本质的区别,就在于动态锁顺序死锁锁住的资源无法确定或者会发生改变。
比如说银行转账业务中,账户A向账户B转账,账户B也可以向账户A转账,这种情况下如果加锁的方式不正确就会发生死锁,比如如下代码:
定义简单的账户类Account
package com.liziba.dl;
import java.math.BigDecimal;
/**
-
-
账户类
-
@Author: Liziba
*/
public class Account {
/** 账户 */
public String number;
/** 余额 */
public BigDecimal balance;
public Account(String number, BigDecimal balance) {
this.number = number;
this.balance = balance;
}
public void setNumber(String number) {
this.number = number;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
}
定义转账类TransferMoney,其中有transferMoney()方法用于accountFrom账户向accountTo转账金额amt:
package com.liziba.dl;
import java.math.BigDecimal;
/**
-
-
转账类
-
@Author: Liziba
*/
public class TransferMoney {
/**
-
转账方法
-
@param accountFrom 转账方
-
@param accountTo 接收方
-
@param amt 转账金额
-
@throws Exception
*/
public static void transferMoney(Account accountFrom,
Account accountTo,
BigDecimal amt) throws Exception {
synchronized (accountFrom) {
synchronized (accountTo) {
BigDecimal formBalance = accountFrom.balance;
if (formBalance.compareTo(amt) < 0) {
throw new Exception(accountFrom.number + " balance is not enough.");
} else {
accountFrom.setBalance(formBalance.subtract(amt));
accountTo.setBalance(accountTo.balance.add(amt));
System.out.println(“Form” + accountFrom.number + ": " + accountFrom.balance.toPlainString()
+“\t” + “To” + accountTo.number + ": " + accountTo.balance.toPlainString());
}
}
}
}
}
上面这个类看似规定了锁的顺序由accountFrom到accountTo不会产生死锁,但是这个accountFrom和accountTo是由调用方来传入的,当A向B转账时accountFrom = A,accountTo = B;当B向A转账时accountFrom = B,accountTo = A;假设两者在同一时刻给对方发起转账,则仍然存在3.1中锁顺序死锁问题。比如如下测试:
public static void main(String[] args) {
// 账户A && 账户B
Account accountA = new Account(“111111”, new BigDecimal(10000));
Account accountB = new Account(“2222222”, new BigDecimal(10000));
// 循环创建线程 A -> B ; B -> A 各一百个线程
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
// 转账顺序 A -> B
transferMoney(accountA, accountB, new BigDecimal(10));
} catch (Exception e) {
return;
}
}).start();
new Thread(() -> {
try {
// 转账顺序 B -> A
transferMoney(accountB, accountA, new BigDecimal(10));
} catch (Exception e) {
return;
}
}).start();
}
}
程序执行无法正确结束,如下所示:
依然使用jps+ jstack查看这个java进程的线程信息,发现Thread-89和Thread-90之间产生死锁
3.2.2 动态锁顺序死锁的解决
解决动态锁顺序死锁的办法,就是通过一定的手段来严格控制加锁的顺序。比如通过对象中某一个唯一的属性值比如id;或者也可以通过对象的散列值+hash冲突解决来控制加锁的顺序。
我们通过对象的散列值+hash冲突解决的方式来优化上面的代码:
package com.liziba.dl;
import java.math.BigDecimal;
/**
-
-
转账类优化 -> 通过hash算法
-
@Author: Liziba
*/
public class TransferMoneyOptimize {
/** hash 冲突时使用第三个锁(优秀的hash算法冲突是很少的!) */
private static final Object conflictShareLock = new Object();
/**
-
转账方法
-
@param accountFrom 转账方
-
@param accountTo 接收方
-
@param amt 转账金额
-
@throws Exception
*/
public static void transferMoney(Account accountFrom,
Account accountTo,
BigDecimal amt) throws Exception {
// 计算hash值
int accountFromHash = System.identityHashCode(accountFrom);
int accountToHash = System.identityHashCode(accountTo);
// 如下三个分支能一定控制账户之间的转是不会产生死锁的
if (accountFromHash > accountToHash) {
synchronized (accountFrom) {
synchronized (accountTo) {
transferMoneyHandler(accountFrom, accountTo, amt);
}
}
} else if (accountToHash > accountFromHash) {
synchronized (accountTo) {
synchronized (accountFrom) {
transferMoneyHandler(accountFrom, accountTo, amt);
}
}
} else {
// 解决hash冲突
synchronized (conflictShareLock) {
synchronized (accountFrom) {
synchronized (accountTo) {
transferMoneyHandler(accountFrom, accountTo, amt);
}
}
}
}
}
/**
-
账户金额增加处理
-
@param accountFrom 转账方
-
@param accountTo 接收方
-
@param amt 转账金额
-
@throws Exception
*/
private static void transferMoneyHandler(Account accountFrom,
Account accountTo,
BigDecimal amt) throws Exception {
if (accountFrom.balance.compareTo(amt) < 0) {
throw new Exception(accountFrom.number + " balance is not enough.");
} else {
accountFrom.setBalance(accountFrom.balance.subtract(amt));
accountTo.setBalance(accountTo.balance.add(amt));
System.out.println(“Form” + accountFrom.number + ": " + accountFrom.balance.toPlainString()
+“\t” + “To” + accountTo.number + ": " + accountTo.balance.toPlainString());
}
}
}
测试代码与上面错误的示例代码一致,经过数次其输出结果均为如下:
在上面两种死锁的产生原因都是因为两个线程以不同的顺序获取相同的所导致的,而解决的办法都是通过一定的规范来严格控制加锁的顺序,这样就能正确的规避死锁的风险。
3.3 协作对象之间的死锁
3.3.1 协作对象死锁的产生与示例
死锁的产生往往没有上述两种死锁产生的那么明显,就算其存在死锁风险也只有在高并发的场景下才会暴露出来(这并不意味着没得高并发的应用就不用考虑死锁问题了啊,弟兄们!)。如下介绍一种隐藏的比较深的死锁,这种死锁产生在多个协作对象的函数调用不透明。
如下以出租车为例介绍协作对象之间死锁的产生,其主要涉及到以下几个类(省略了很多代码,自行脑补哈!):
-
Coordinate -> 坐标类,出租车经纬度信息类
-
Taxi -> 出租车类,出租车所属于某个出租车车队Fleet,此外包含当前坐标location和目的地坐标destination,出租车在更新目的地信息的时候会判断当前坐标与目的地坐标是否相等,相等则会通知所属车队车辆空闲,可以接收下一个目的地
-
Fleet -> 出租车车队类,出租车类包含两个集合taxis和available,分别用来保存车队中所有车辆信息和车队中当前空闲的出租车信息,此外提供获取车队中所有出租车当前地址信息的快照方法getImage()
-
Image -> 车辆地址信息快照类,用于获取出租车的地址信息
Coordinate(坐标类) 代码示例:
package com.liziba.dl;
/**
-
-
坐标类
-
@Author: Liziba
*/
public class Coordinate {
/** 经度 */
private Double longitude;
/** 纬度 */
private Double latitude;
// 省略 getXxx,setXxx等方法
}
Taxi(出租车类)代码示例;
package com.liziba.dl;
import java.util.Objects;
/**
-
-
出租车类
-
@Author: Liziba
*/
public class Taxi {
/** 出租车唯一标志 */
private String id;
/** 当前坐标 */
private Coordinate location;
/** 目的地坐标 */
private Coordinate destination;
/** 所属车队 */
private final Fleet fleet;
/**
-
获取当前地址信息
-
@return
*/
public synchronized Coordinate getLocation() {
return location;
}
/**
-
更新当前地址信息
-
如果当前地址与目的地地址一致,则表名到达目的地需要通知车队,当前出租车空闲可用前往下一个目的地
-
@param location
*/
public synchronized void setLocation(Coordinate location) {
this.location = location;
if (location.equals(destination)) {
fleet.free(this);
}
}
public Coordinate getDestination() {
return destination;
}
/**
-
设置目的地
-
@param destination
*/
public synchronized void setDestination(Coordinate destination) {
this.destination = destination;
}
public Taxi(Fleet fleet) {
this.fleet = fleet;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Taxi taxi = (Taxi) o;
return Objects.equals(location, taxi.location) &&
Objects.equals(destination, taxi.destination);
}
@Override
public int hashCode() {
return Objects.hash(location, destination);
}
}
Fleet(出租车车队类)示例代码:
package com.liziba.dl;
import java.util.Set;
/**
-
-
车队类 -> 调度管理出租车
-
@Author: Liziba
*/
public class Fleet {
/** 车队中所有出租车 */
private final Set taxis;
/** 车队中目前空闲的出租车 */
private final Set available;
public Fleet(Set taxis) {
this.taxis = this.available = taxis;
/**
-
出租车到达目的地后调用该方法,向车队发出当前出租车空闲信息
-
@param taxi
*/
public synchronized void free(Taxi taxi) {
available.add(taxi);
}
/**
-
获取所有出租车在不同时刻的地址快照
-
@return
*/
public synchronized Image getImage() {
Image image = new Image();
for (Taxi taxi : taxis) {
image.drawMarker(taxi);
}
return image;
}
}
Image(车辆地址信息快照类)示例代码:
package com.liziba.dl;
import java.util.HashMap;
import java.util.Map;
/**
-
-
获取所有出租车在某一时刻的位置快照
-
@Author: Liziba
*/
public class Image {
Map<String, Coordinate> locationSnapshot = new HashMap<>();
public void drawMarker(Taxi taxi) {
locationSnapshot.put(taxi.getId(), taxi.getLocation());
}
}
在上述代码中,看不到一个方法中有对多个资源直接加锁,但仔细分析却能发现在方法的调用之间是存在对多个资源“隐式”加锁的,比如Taxi中的setLocation(Coordinate location)与Fleet中的Image getImage()。
-
setLocation(Coordinate location)方法需要获取当前出租车Taxi对象的锁以及出租车所属车队Fleet的锁
-
getImage()方法需要获取当前车队Fleet的锁,以及在遍历出租车获取其地址信息时需要获取每个出租车Taxi对象的锁
如上所示的这两种情况无法避免同时执行的情况,因此存在死锁的可能性,其执行流程如下:
3.3.2 协作对象之间的死锁解决
Taxi中的setLocation(Coordinate location)方法与getImage()方法中包含其他方法的调用,方法的调用应该是透明的也就是说,调用方无需知道方法内部的执行逻辑,这是正确的。但是方法中调用的其他方法可能是同步方法或者方法中会发生较长时间的阻塞,这会导致死锁或者线程长时间等待等问题。基于此类问题,可以采用缩小同步代码的访问(锁尽可能少的代码)和开放调用(不加锁)来解决(Open Call)。
上述代码我们基于上面提的两种方式来优化:
Taxi -> TaxiOptimize(优化出租车类):
最后
金三银四马上就到了,希望大家能好好学习一下这些技术点
学习视频:
大厂面试真题:
Taxi对象的锁
如上所示的这两种情况无法避免同时执行的情况,因此存在死锁的可能性,其执行流程如下:
3.3.2 协作对象之间的死锁解决
Taxi中的setLocation(Coordinate location)方法与getImage()方法中包含其他方法的调用,方法的调用应该是透明的也就是说,调用方无需知道方法内部的执行逻辑,这是正确的。但是方法中调用的其他方法可能是同步方法或者方法中会发生较长时间的阻塞,这会导致死锁或者线程长时间等待等问题。基于此类问题,可以采用缩小同步代码的访问(锁尽可能少的代码)和开放调用(不加锁)来解决(Open Call)。
上述代码我们基于上面提的两种方式来优化:
Taxi -> TaxiOptimize(优化出租车类):
最后
金三银四马上就到了,希望大家能好好学习一下这些技术点
学习视频:
[外链图片转存中…(img-NcaCtRcq-1714912796538)]
大厂面试真题:
[外链图片转存中…(img-xyteXdEC-1714912796538)]