1.锁消除
有些应用程序中用到了synchronized,但其实没有在多线程环境下,编译器+JVM 判断锁是否可消除,如果可以,就直接消除。
例如StringBuffer:
StringBuffer sb = new StringBuffer(); sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加 锁解锁操作是没有必要的, 白白浪费了一些资源开销.
2.锁粗化
一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化。实际开发过程中, 使用细粒度锁, 是期望释放锁的时候其他线程能使用锁. 但是实际上可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会自动把锁粗化, 避免频繁申请释放锁。
举个栗子理解锁粗化:
滑稽老哥当了领导, 给下属交代工作任务:
方式一:
打电话, 交代任务1, 挂电话.
打电话, 交代任务2, 挂电话.
打电话, 交代任务3, 挂电话.
方式二:
打电话, 交代任务1, 任务2, 任务3, 挂电话.
显然, 方式二是更高效的方案.
3.JUC的常见类
- ReentrantLock
可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.
- ReentrantLock 的用法:
- lock(): 加锁, 如果获取不到锁就死等.
- rylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
- unlock(): 解锁
ReentrantLock lock = new ReentrantLock(); -----------------------------------------
lock.lock();
try {
// working
} finally {
lock.unlock()
}
- ReentrantLock 和 synchronized 的区别:
- synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).
- synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
- synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就 放弃.
- synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启 公平锁模式.
- 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一 个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指 定的线程.
// ReentrantLock 的构造方法
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
- 如何选择使用哪个锁?
- 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
- 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
- 如果需要使用公平锁, 使用 ReentrantLock.
4.死锁
- 当两个或多个线程互相请求对方拥有的资源时,就可能发生死锁。
- 一个线程,一把锁,连续加锁两次,如果锁是不可重入锁就会死锁。
- 多个线程多把锁
死锁的四个必要条件:
- 互斥使用:
线程1拿到了锁,线程2就得等着。
- 不可抢占:
线程1拿到锁之后,必须是线程1主动释放。
- 请求和保持:
线程1拿到锁A之后,再次尝试获取锁B,A这把锁还是保持的(不会因为获取锁B就把A给释放了)。
- 循环等待:
线程1尝试获取锁A和锁B,线程2尝试获取锁B和锁A。线程1在获取B的时候等待线程2释放B,线程2在获取A的时候等待线程1释放A。
案例一:
线程1先锁定资源1,然后尝试锁定资源2。同时,线程2先锁定资源2,然后尝试锁定资源1。由于两个线程互相持有对方所需的资源,它们会相互等待对方释放资源,从而导致死锁。
public class DeadlockExample {
public static void main(String[] args) {
final Object resource1 = new Object();
final Object resource2 = new Object();
// 线程1尝试获取resource1,然后resource2
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: locked resource 1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("Thread 1: locked resource 2");
}
}
});
// 线程2尝试获取resource2,然后resource1
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: locked resource 2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1) {
System.out.println("Thread 2: locked resource 1");
}
}
});
// 启动两个线程
thread1.start();
thread2.start();
}
}
案例二:
不可重入锁造成死锁: 一个线程没有释放锁, 然后又尝试再次加锁。
// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();
按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待。
直到第一次的锁被释放, 才能获取到第二个锁。但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作,这时候就会死锁。
methodA 和 methodB 分别尝试先获取 lock1 和 lock2,然后再尝试获取另一个锁。当两个线程同时执行这两个方法时,它们会相互等待对方释放锁,从而导致死锁。
为了避免这种情况,应该使用可重入锁(Reentrant Lock),例如 java.util.concurrent.locks.ReentrantLock,它允许多次获得同一把锁,并且不会造成死锁。
public class DeadLockExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void methodA() {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " 获取了 lock1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " 尝试获取 lock2");
// 这里会发生死锁,因为当前线程已经持有 lock1,但无法再次获取 lock2
}
}
}
public void methodB() {
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " 获取了 lock2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " 尝试获取 lock1");
// 这里也会发生死锁,因为当前线程已经持有 lock2,但无法再次获取 lock1
}
}
}
public static void main(String[] args) {
DeadLockExample example = new DeadLockExample();
Thread threadA = new Thread(() -> example.methodA());
Thread threadB = new Thread(() -> example.methodB());
threadA.start();
threadB.start();
}
}
案例三:
两个线程thread1和thread2,它们都尝试以不同的顺序获取两把锁lock1和lock2。如果这两个线程几乎同时启动,它们可能会相互等待对方释放锁,从而导致死锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MultiLocksDeadlock {
// 定义两把锁
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(new Task("task1", lock1, lock2));
Thread thread2 = new Thread(new Task("task2", lock2, lock1));
thread1.start();
thread2.start();
}
static class Task implements Runnable {
private String name;
private Lock firstLock;
private Lock secondLock;
public Task(String name, Lock firstLock, Lock secondLock) {
this.name = name;
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
// 线程尝试以不同的顺序获取两把锁
firstLock.lock();
System.out.println(name + " got the first lock");
Thread.sleep(1000); // 模拟一些工作
secondLock.lock();
System.out.println(name + " got the second lock");
// 做一些工作...
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
secondLock.unlock();
firstLock.unlock();
}
}
}
}
5.如何避免死锁?
给锁编号,然后指定一个固定的顺序(比如从小到大)来加锁,任意线程加多把锁的时候,都让线程遵守上述程序,此时循环等待自然破除。