【尚硅谷周阳--JUC并发编程】【第四章--锁】
一、乐观锁和悲观锁
1、悲观锁
1.1、简介
-
认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
-
synchronized关键字和Lock的实现都是悲观锁
1.2、适用场景
- 适合写操作多的场景,先加锁可以保证写操作时数据正确
- 显式的锁定之后再操作同步资源
- 一句话:狼性锁
2、乐观锁
2.1、简介
- 认为自己在使用数据时不会有别的线程修改数据表资源,所以不会添加锁
- 在Java中是通过使用无锁编程来实现,只是在更新数据的时候取判断,之前有没有别的线程更新了这个数据。
- 如果这个数据没有被更新,当前线程将自己修改的数据成功写入
- 如果这个数据已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等
2.2、适用场景
- 适合读操作多的场景,不加锁的特点能够使其操作的性能大幅提升。
- 乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再努力就是
- 一句话:佛系锁
2.3、两种实现方式
- 版本号机制Version
- 最常采用的是CAS(Compare-and-Swap,即比较并替换)算法,Java原子类中的递增操作就通过CAS自旋实现的
// ===============悲观锁的调用方式
public synchronized void m1() {
// 加锁后的业务逻辑
}
// 保证多个线程使用的是同一个lock对象的前提下
ReentrantLock lock = new ReentrantLock();
public void m2() {
lock.lock();
try {
// 操作同步资源
} finally {
lock.unlock();
}
}
// ===============乐观锁的调用方式--保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();
public void m3() {
atomicInteger.incrementAndGet();
}
二、八种情况演示锁运行案例,确认锁类型
1、锁相关的八种案例演示
1.1、阿里巴巴Java开发手册
高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用锁对象,就不要用类锁。
尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC方法。
1.2、八锁案例说明
- 标准访问有ab两个线程,请问先打印邮件还是短信
结果:先邮件再短信
理由:一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其他的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法。锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的synchronized方法。
public class Lock8Demo {
public static void main(String[] args) {
Phone phone1 = new Phone();
new Thread(() -> phone1.sendEmail(), "a").start();
// 保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> phone1.sendSMS(), "b").start();
}
}
// 资源类
class Phone {
public synchronized void sendEmail() {
System.out.println("------sendEmail");
}
public synchronized void sendSMS() {
System.out.println("------sendSMS");
}
public void hello() {
System.out.println("------hello");
}
}
- sendEmail方法中加入暂停三秒,请问先打印邮件还是短信
结果:先邮件再短信
理由:一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其他的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法。锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的synchronized方法。
public class Lock8Demo {
public static void main(String[] args) {
Phone phone1 = new Phone();
new Thread(() -> phone1.sendEmail(), "a").start();
// 保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> phone1.sendSMS(), "b").start();
}
}
// 资源类
class Phone {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("------sendEmail");
}
public synchronized void sendSMS() {
System.out.println("------sendSMS");
}
public void hello() {
System.out.println("------hello");
}
}
- 添加一个普通的hello方法,请问先打印邮件还是hello
结果:先打印hello再打印邮件
理由:普通方法没有通过synchronized修饰,不用获取钥匙,资源不需要争抢,可以直接访问
public class Lock8Demo {
public static void main(String[] args) {
Phone phone1 = new Phone();
new Thread(() -> phone1.sendEmail(), "a").start();
// 保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> phone1.hello(), "b").start();
}
}
// 资源类
class Phone {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("------sendEmail");
}
public synchronized void sendSMS() {
System.out.println("------sendSMS");
}
public void hello() {
System.out.println("------hello");
}
}
- 有两部手机,请问先打印邮件还是短信
结果:先打印短信再打印邮件
理由:synchronized锁的是对象,两个对象,资源不需要争抢
public class Lock8Demo {
public static void main(String[] args) {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> phone1.sendEmail(), "a").start();
// 保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> phone2.sendSMS(), "b").start();
}
}
// 资源类
class Phone {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("------sendEmail");
}
public synchronized void sendSMS() {
System.out.println("------sendSMS");
}
public void hello() {
System.out.println("------hello");
}
}
- 两个静态同步方法,有一部手机,请问先打印邮件还是短信
结果:先邮件再短信
理由:换成静态同步方法后,情况又变化,三种synchronized锁的内容有一些差别:- 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部手机,所有的普通同步方法用的都是同一把锁->实例对象本身
- 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
- 对于同步方法块,锁的是synchronized括号内的对象
public class Lock8Demo {
public static void main(String[] args) {
Phone phone1 = new Phone();
new Thread(() -> phone1.sendEmail(), "a").start();
// 保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> phone1.sendSMS(), "b").start();
}
}
// 资源类
class Phone {
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("------sendEmail");
}
public static synchronized void sendSMS() {
System.out.println("------sendSMS");
}
public void hello() {
System.out.println("------hello");
}
}
- 有两个静态同步方法,有两部手机,请问先打印邮件还是短信
结果:先邮件再短信
理由:换成静态同步方法后,情况又变化,三种synchronized锁的内容有一些差别:- 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部手机,所有的普通同步方法用的都是同一把锁->实例对象本身
- 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
- 对于同步方法块,锁的是synchronized括号内的对象
public class Lock8Demo {
public static void main(String[] args) {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> phone1.sendEmail(), "a").start();
// 保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> phone2.sendSMS(), "b").start();
}
}
// 资源类
class Phone {
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("------sendEmail");
}
public static synchronized void sendSMS() {
System.out.println("------sendSMS");
}
public void hello() {
System.out.println("------hello");
}
}
- 有一个静态同步方法,有一个普通同步方法,有一部手机,请问先打印邮件还是短信
结果:先短信再邮件
理由:当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。- 所有的普通同步方法用的都是同一把锁–实例对象本身,就是new出来的具体事例对象本身,本类this,也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
- 所有的静态同步方法用的也是同一把锁–类对象本身,就是我们说过的唯一模板Class。具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的,但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
public class Lock8Demo {
public static void main(String[] args) {
Phone phone1 = new Phone();
new Thread(() -> phone1.sendEmail(), "a").start();
// 保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> phone1.sendSMS(), "b").start();
}
}
// 资源类
class Phone {
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("------sendEmail");
}
public synchronized void sendSMS() {
System.out.println("------sendSMS");
}
public void hello() {
System.out.println("------hello");
}
}
- 有一个静态同步方法,有一个普通同步方法,有两部手机,请问先打印邮件还是短信
结果:先短信再邮件
理由:当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。- 所有的普通同步方法用的都是同一把锁–实例对象本身,就是new出来的具体事例对象本身,本类this,也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
- 所有的静态同步方法用的也是同一把锁–类对象本身,就是我们说过的唯一模板Class。具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的,但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
public class Lock8Demo {
public static void main(String[] args) {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> phone1.sendEmail(), "a").start();
// 保证a线程先启动
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(() -> phone2.sendSMS(), "b").start();
}
}
// 资源类
class Phone {
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("------sendEmail");
}
public synchronized void sendSMS() {
System.out.println("------sendSMS");
}
public void hello() {
System.out.println("------hello");
}
}
2、synchronized三种应用方式
2.1、JDK源码(notify方法)说明
即获取锁可以通过一下三种方式:
- 执行普通同步实例对象方法
- 同步代码块
- 类同步静态方法
2.2、8种锁的案例实际体现在3个地方
- 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁
- 作用于代码块,对括号里配置的对象加锁
- 作用于静态方法,当前类加锁,进去同步代码前要获得当前对象的锁
3、从字节码角度分析synchronized实现
3.1、文件反编译
# -c 对代码进行反汇编
javap -c ***.class
# 假如需要更多信息
# -v 即-verbose输出附加信息(包括行号、本地变量表、反汇编等详细信息)
javap -v ***.class
3.2、synchronized同步代码块
public class LockSyncDemo {
Object object = new Object();
public void m1() {
synchronized (object) {
System.out.println("---- hello synchronized 代码块");
}
}
public static void main(String[] args) {
}
}
找到LockSyncDemo.class文件反编译
命令:javap -c ***.class
- synchronized同步代码块实现是使用monitorenter和monitorexit指令来保证持有锁和释放锁的,为什么有两个monitorexit呢,如果程序正常进行的话,可以通过第一个monitorexit正常退出,但是若程序执行过程中出现异常,就通过第二个monitorexit来释放锁。
- 一般情况下1个enter对应2个exit,如果极端情况,在同步代码块中手动抛出了异常,那么就是1个enter对应1个exit了
3.3、synchronized普通同步方法
public class LockSyncDemo {
public synchronized void m2() {
System.out.println("----hello synchronized 普通同步方法");
}
public static void main(String[] args) {
}
}
找到LockSyncDemo.class文件反编译
命令:javap -v ***.class
- 调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将持有monitor锁,然后再去执行方法,最后在方法完成(无论是正常还是非正常完成)时释放monitor。
3.4、synchronized静态同步方法
public class LockSyncDemo {
public static synchronized void m3() {
System.out.println("----hello synchronized 静态普通同步方法");
}
public static void main(String[] args) {
}
}
找到LockSyncDemo.class文件反编译
命令:javap -v ***.class
- ACC_STATIC,ACC_SYNCHRONIZED访问标志区分方法是否静态同步方法
4、反编译synchronized锁的是什么
4.1、为什么任何一个对象都可以成为一个锁
- 面试题:synchronized实现原理,monitor对象什么时候生成的?知道monitor的monitorenter和monitorexit这两个是怎么保证同步的吗,或者说,这两个操作计算机底层是如何执行的?
- 管程(Monitor,也称为监视器)是一种程序结构,结构内的多个子线程(对象或模板)形成的多个工作线程互斥访问共享资源,这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程执行管程的某个子程序。管程提供了一种机制,管程可以看做是一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。 - 为什么所有对象都可以成为一个锁呢?
- 在HotSpot虚拟机中,monitor采用ObjectMonitor实现
- Java的底层是c++,在Java中任何一个对象对应一个Object.java.一个ObjectMonitor.java对应一个ObjectMonitor.cpp对应一个objectMonitor.hpp
- 由objectMonitor.hpp源码可以看出
因为每一个对象天生都带着一个对象监视器,每一个被锁住的对象都会和Monitor关联起来。
可以看到,objectMonitor对象中有一个_owner属性,它用于指向持有ObjectMonitor对象的线程
5、对于synchronized关键字,在《synchronized与锁升级》章节还会再深度讲解
三、公平锁和非公平锁
1、从ReentrantLock卖票demo演示公平和非公平现象
1.1、非公平锁
是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比现申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某一个线程一直得不到锁)
// false表示非公平锁,后来的也可能先获得锁
Lock lock = new ReentrantLock(false);
// 默认情况下是非公平锁
Lock lock = new ReentrantLock();
线程获取锁的几率不公平,即使是三个线程,最后处理卖票也可能全部由一个线程独揽完成
class Ticket {
private int number = 50;
ReentrantLock lock = new ReentrantLock();
public void sale() {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);
}
} finally {
lock.unlock();
}
}
}
public class SaleTicketDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 55; i++) {
ticket.sale();
}
}, "a").start();
new Thread(() -> {
for (int i = 0; i < 55; i++) {
ticket.sale();
}
}, "b").start();
new Thread(() -> {
for (int i = 0; i < 55; i++) {
ticket.sale();
}
}, "c").start();
}
}
1.2、公平锁
是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的。
Lock lock = new ReentrantLock(true);//true表示公平锁,先来先得
ReentrantLock的构造方法可以传入一个布尔值,fair(是否公平)
class Ticket {
private int number = 50;
ReentrantLock lock = new ReentrantLock(true);
public void sale() {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);
}
} finally {
lock.unlock();
}
}
}
public class SaleTicketDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 55; i++) {
ticket.sale();
}
}, "a").start();
new Thread(() -> {
for (int i = 0; i < 55; i++) {
ticket.sale();
}
}, "b").start();
new Thread(() -> {
for (int i = 0; i < 55; i++) {
ticket.sale();
}
}, "c").start();
}
}
2、为什么设计公平锁/非公平锁?为什么默认非公平锁
- 恢复挂起的线程到真正锁的获取还是有时间查的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程切换的开销。
- 什么时候用公平锁什么时候用非公平锁?
- 如果为了更高的吐吞量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了,否则那就用公平锁,大家公平使用。
四、可重入锁(又名递归锁)
1、可重入锁简介
- 可重入锁又名递归锁
- 是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞
- 如果是1个有synchroized修饰得递归调用方法,程序第2次进入被自己阻塞了岂不是天大得笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
- 可重入锁:可以再次进入同步域(即同步代码块/方法或显示锁锁定的代码)同步锁。
一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁。
2、可重入锁的种类
2.1、隐式锁
-
即synchronized关键字使用的锁,默认是可重入锁
-
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
简单来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的。
// 同步代码块可重入隐式锁
public class ReEntryLockDemo {
public static void main(String[] args) {
final Object object = new Object();
new Thread(() -> {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "\t------外层调用");
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "\t ------中层调用");
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "\t ------内层调用");
}
}
}
}, "t1").start();
}
}
// 同步方法可重入隐式锁
public class ReEntryLockDemo {
public synchronized void m1() {
// 在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
System.out.println(Thread.currentThread().getName() + "\t m1------come in");
m2();
System.out.println(Thread.currentThread().getName() + "\t m1------end");
}
public synchronized void m2() {
System.out.println(Thread.currentThread().getName() + "\t m2------come in");
m3();
System.out.println(Thread.currentThread().getName() + "\t m2------end");
}
public synchronized void m3() {
System.out.println(Thread.currentThread().getName() + "\t m3------come in");
System.out.println(Thread.currentThread().getName() + "\t m3------end");
}
public static void main(String[] args) {
ReEntryLockDemo entryLockDemo = new ReEntryLockDemo();
new Thread(entryLockDemo::m1, "t1").start();
}
}
2.2、synchronized的重入实现原理
- 每个锁对象都拥有一个锁的计数器(_count)和一个指向持有该锁的线程的指针(_owner)
- 当执行monitorenter时,如果目标对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1
- 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
- 当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
2.3、显示锁
- 即Lock也有ReentrantLock这样的可重入锁
public class ReEntryLockDemo {
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread( () -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 外层lock------come in");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 中层lock------come in");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 内层lock------come in");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}).start();
}
}
2.4、显示锁和隐式锁可以嵌入使用
public class ReEntryLockDemo {
public synchronized void m1() {
// 在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
System.out.println(Thread.currentThread().getName() + "\t m1------come in");
m2();
System.out.println(Thread.currentThread().getName() + "\t m1------end");
}
public synchronized void m2() {
System.out.println(Thread.currentThread().getName() + "\t m2------come in");
m3();
System.out.println(Thread.currentThread().getName() + "\t m2------end");
}
public synchronized void m3() {
System.out.println(Thread.currentThread().getName() + "\t m3------come in");
System.out.println(Thread.currentThread().getName() + "\t m3------end");
}
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
ReEntryLockDemo entryLockDemo = new ReEntryLockDemo();
new Thread( () -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 外层lock------come in");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 中层lock------come in");
entryLockDemo.m1();
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 内层lock------come in");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 外层lock------come in");
} finally {
lock.unlock();
}
}).start();
}
注意,显示锁,加锁几次,就需要解锁几次,否则资源不会被释放,就上述案例而言,当第一个线程未释放显示锁时,第二个线程是拿不到资源的。
五、死锁及排查
1、什么是死锁
- 死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会争夺有限的资源而陷入死锁。
- 产生死锁主要原因
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
2、死锁案例
// t1,t2线程都想获取资源A和B,但是t1持有A,t2持有B造成死锁
public class DeadLockDemo {
public static void main(String[] args) {
final Object objectA = new Object();
final Object objectB = new Object();
new Thread(() -> {
synchronized (objectA) {
try {
System.out.println(Thread.currentThread().getName() + "\t 获取资源objectA");
TimeUnit.SECONDS.sleep(1);
synchronized (objectB) {
System.out.println(Thread.currentThread().getName() + "\t 获取资源objectB");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "t1").start();
new Thread(() -> {
synchronized (objectB) {
try {
System.out.println(Thread.currentThread().getName() + "\t 获取资源objectB");
TimeUnit.SECONDS.sleep(1);
synchronized (objectA) {
System.out.println(Thread.currentThread().getName() + "\t 获取资源objectA");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "t2").start();
}
}
3、如何排查死锁
- 纯命令
# 查看进程编号
jps -l
# 查看进程栈信息
jstack 进程编号
- 图形化
jconsole