谈谈你对锁的理解,如何手动模拟一个死锁
死锁
指两个线程同时占用两个资源又在彼此等待对方释放锁资源
演示代码
public class LockExample {
public static void main (String[] args) {
deadLock(); // 死锁
}
private static void deadLock() {
Object lock1 = new Object();
Object lock2 = new Object();
// 线程一拥有lock1 试图获取lock2
new Thread(() -> {
synchronized(lock1) {
System.out.println("获取lock1成功");
try{
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e) {
e.printStackTrace();
}
// 试图获取锁lock2
synchronized(lock2) {
System.out.println(Thread.currentThread().getName());
}
}
}).start();
// 线程二lock2试图获取lock1
new Thread(() -> {
synchronized(lock2) {
System.out.println("获取lock2成功");
try{
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e) {
e.printStackTrace();
}
// 试图获取锁lock2
synchronized(lock1) {
System.out.println(Thread.currentThread().getName());
}
}
}).start();
}
}
锁相关面试问题
-
什么是乐观锁和悲观锁? 他们的应用都有哪些? 乐观锁有什么问题?
-
悲观锁
-
指的是数据对外界的修改采取保守策略, 它认为线程很容易会把数据修改掉, 因此在整个数据被修改的过程中都会采用锁定状态, 直到一个线程使用完, 其他线程才可以继续使用
-
代码如下:
public class LockExample { public static void main(String[] args) { synchronized(LockExample.class) { System.out.println("lock"); } } }
-
反编译结果如下:
Complied from "LockExample.java" public class com.example.interview.ext.LockExample{ public com.example.interview.ext.LockExample(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>";()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // class com/example/interview/ext/LockExample 2: dup 3: astore_1 4: monitorenter // 加锁 5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #4 // String lock 10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: aload_1 14: monitorexit // 释放锁 15: goto 23 18: astore_2 19: aload_1 20: monitorexit 21: aload_2 22: athrow 23: return Exception table: from to target type 5 15 18 any 18 21 18 any }
-
-
乐观锁
-
乐观锁认为一般情况下数据在修改时不会出现冲突, 所以在数据访问之前不会加锁, 只是在数据提交更改时, 才会对数据进行检测, 因此不会造成死锁
-
Java中的乐观锁大部分是通过 CAS (Compare And Swap, 比较并交换) 操作实现
-
CAS 是一个多线程同步的原子指令, CAS操作包含三个重要的信息: 内存位置, 预期原值, 新值
-
CAS 衍生问题 ABA的常见处理方式: 添加版本号, 每次修改之后更新版本号
-
JDK在1.5时提供了 AtomicStamedReference 类也可以解决 ABA 的问题, 此类维护了一个"版本号" Stamp, 每次在比较时不止比较当前值还比较版本号
public class AtomicStampedReference<V> { public static class Pair<V> { final T reference; final int stamp; // 版本号 private Pair(T reference, int stamp){ this.reference = reference; this.stamp = stamp; } static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference, stamp); } } // 比较并设置 public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp){ Pair<V> current = pair; return expectedREference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); } // ...其他源码 }
-
-
-
什么是可重入锁? 用代码如何实现? 他的实现原理是什么?
-
可重入锁
-
也叫递归锁, 指的是同一个线程, 如果外面的函数拥有此锁之后, 内层的函数也可以继续获取该锁, 在 Java 语言中 ReentrantLock 和 synchronized 都是可重入锁
/** * synchronized 演示可重入锁 */ public class LockExample{ public static void main(String[] args) { reentrantA(); // 可重入锁 } /** * 可重入锁A方法 */ private synchronized static void reentrantA() { System.out.println(Thread.currentThread().getName() + "执行 reentrantA"); reentrantB(); } /** * 可重入锁A方法 */ private synchronized static void reentrantB() { System.out.println(Thread.currentThread().getName() + "执行 reentrantB"); } }
-
可重入锁内部维护了一个计数器, 每加一重锁计数器 +1, 退出则 -1, 当计数器为 0 时, 则表示当前对象未加锁
-
-
-
什么是共享锁和独占锁?
-
独占锁
- 只能被单线程持有的锁, 指的是在任何时候, 最多只能有一个线程持有该锁, ReentrantLock 就是独占锁, 可以理解为悲观锁
-
共享锁
- 可以被多线程持有的锁, ReadWriteLock 读写锁允许同一时间内有多个线程进行读操作, 属于共享锁, 可以理解为乐观锁
-