一、乐观锁和悲观锁
1. 悲观锁
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
悲观锁的实现方式
synchronized
关键字Lock
的实现类都是悲观锁
适合写操作多的场景,先加锁可以保证写操作时数据正确。显示的锁定之后再操作同步资源。
//=============悲观锁的调用方式
public synchronized void m1()
{
//加锁后的业务逻辑......
}
// 保证多个线程使用的是同一个lock对象的前提下
ReentrantLock lock = new ReentrantLock();
public void m2() {
lock.lock();
try {
// 操作同步资源
}finally {
lock.unlock();
}
}
2. 乐观锁
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
乐观锁的实现方式
- 版本号机制Version。(只要有人提交了就会修改版本号,可以解决ABA问题)
- ABA问题:再CAS中想读取一个值A,想把值A变为C,不能保证读取时的A就是赋值时的A,中间可能有个线程将A变为B再变为A。
- 解决方法:Juc包提供了一个
AtomicStampedReference
,原子更新带有版本号的引用类型,通过控制版本值的变化来解决ABA问题。
- 解决方法:Juc包提供了一个
- 最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
- ABA问题:再CAS中想读取一个值A,想把值A变为C,不能保证读取时的A就是赋值时的A,中间可能有个线程将A变为B再变为A。
适合读操作多的场景,不加锁的性能特点能够使其操作的性能大幅提升。
//=============乐观锁的调用方式
// 保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
二、synchronized 锁的8种情况
下面通过一段代码,演示 synchronized 锁的 8 种情况
标准访问,先打印短信还是邮件
class Phone { public synchronized void sendSMS() throws Exception { System.out.println("------sendSMS"); } public synchronized void sendEmail() throws Exception { System.out.println("------sendEmail"); } } public class Lock_8 { public static void main(String[] args) throws Exception { Phone phone = new Phone(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "AA").start(); Thread.sleep(100); new Thread(() -> { try { phone.sendEmail(); } catch (Exception e) { e.printStackTrace(); } }, "BB").start(); } }
打印结果:
------sendSMS ------sendEmail
分析:两个方法都加了 synchronized 关键字,因为是同一个对象调用,所有是同一把锁,按照顺序执行。
停4秒在短信方法内,先打印短信还是邮件
class Phone { public synchronized void sendSMS() throws Exception { //停留4秒 TimeUnit.SECONDS.sleep(4); System.out.println("------sendSMS"); } public synchronized void sendEmail() throws Exception { System.out.println("------sendEmail"); } } public class Lock_8 { public static void main(String[] args) throws Exception { Phone phone = new Phone(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "AA").start(); Thread.sleep(100); new Thread(() -> { try { phone.sendEmail(); } catch (Exception e) { e.printStackTrace(); } }, "BB").start(); } }
打印结果:
------sendSMS ------sendEmail
分析:两个方法都加了 synchronized 关键字,因为还是同一个对象调用,所有还是同一把锁,先拿到锁的方法执行结束后,另一个方法才能执行。
新增普通的hello方法,是先打短信还是hello
class Phone { public synchronized void sendSMS() throws Exception { //停留4秒 TimeUnit.SECONDS.sleep(4); System.out.println("------sendSMS"); } public synchronized void sendEmail() throws Exception { System.out.println("------sendEmail"); } public void getHello() { System.out.println("------getHello"); } } public class Lock_8 { public static void main(String[] args) throws Exception { Phone phone = new Phone(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "AA").start(); Thread.sleep(100); new Thread(() -> { try { phone.getHello(); } catch (Exception e) { e.printStackTrace(); } }, "BB").start(); } }
打印结果:
------getHello ------sendSMS
分析:hello方法没有加锁,调用即执行,sendSMS有sleep阻塞。
现在有两部手机,先打印短信还是邮件
class Phone { public synchronized void sendSMS() throws Exception { //停留4秒 TimeUnit.SECONDS.sleep(4); System.out.println("------sendSMS"); } public synchronized void sendEmail() throws Exception { System.out.println("------sendEmail"); } } public class Lock_8 { public static void main(String[] args) throws Exception { Phone phone = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "AA").start(); Thread.sleep(100); new Thread(() -> { try { phone2.sendEmail(); } catch (Exception e) { e.printStackTrace(); } }, "BB").start(); } }
打印结果:
------sendEmail ------sendSMS
分析:synchronized 在方法上,是方法级的锁,由于是两个 phone 对象调用不同的方法,所以 sendSMS() 和 sendEmail() 持有的是不同的锁,互不影响。
两个静态同步方法,1部手机,先打印短信还是邮件
class Phone { public static synchronized void sendSMS() throws Exception { //停留4秒 TimeUnit.SECONDS.sleep(4); System.out.println("------sendSMS"); } public static synchronized void sendEmail() throws Exception { System.out.println("------sendEmail"); } } public class Lock_8 { public static void main(String[] args) throws Exception { Phone phone = new Phone(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "AA").start(); Thread.sleep(100); new Thread(() -> { try { phone.sendEmail(); } catch (Exception e) { e.printStackTrace(); } }, "BB").start(); } }
打印结果:
------sendSMS ------sendEmail
分析:synchronized 在静态方法上,是类级别的锁,所以两个方法持有的是同一把锁,要按顺序执行
两个静态同步方法,2部手机,先打印短信还是邮件
class Phone { public static synchronized void sendSMS() throws Exception { //停留4秒 TimeUnit.SECONDS.sleep(4); System.out.println("------sendSMS"); } public static synchronized void sendEmail() throws Exception { System.out.println("------sendEmail"); } } public class Lock_8 { public static void main(String[] args) throws Exception { Phone phone = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "AA").start(); Thread.sleep(100); new Thread(() -> { try { phone2.sendEmail(); } catch (Exception e) { e.printStackTrace(); } }, "BB").start(); } }
打印结果:
------sendSMS ------sendEmail
分析:synchronized 在静态方法上,是类级别的锁,所以两个方法持有的是同一把锁,要按顺序执行
1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
class Phone { public static synchronized void sendSMS() throws Exception { //停留4秒 TimeUnit.SECONDS.sleep(4); System.out.println("------sendSMS"); } public void getHello() { System.out.println("------getHello"); } } public class Lock_8 { public static void main(String[] args) throws Exception { Phone phone = new Phone(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "AA").start(); Thread.sleep(100); new Thread(() -> { try { phone.getHello(); } catch (Exception e) { e.printStackTrace(); } }, "BB").start(); } }
打印结果:
------getHello ------sendSMS
分析:类加锁对普通方法无限制,普通方法调用即执行。
1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
class Phone { public static synchronized void sendSMS() throws Exception { //停留4秒 TimeUnit.SECONDS.sleep(4); System.out.println("------sendSMS"); } public void sendEmail() throws Exception { System.out.println("------sendEmail"); } } public class Lock_8 { public static void main(String[] args) throws Exception { Phone phone = new Phone(); Phone phone2 = new Phone(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "AA").start(); Thread.sleep(100); new Thread(() -> { try { phone2.sendEmail(); } catch (Exception e) { e.printStackTrace(); } }, "BB").start(); } }
打印结果:
------sendEmail ------sendSMS
分析:类加锁对普通方法无限制,普通方法调用即执行。
总结:
- synchronized 锁的是方法,则是对象锁,同个对象锁的机制要等待,不同对象锁的机制调用同一个不用等待
- 加了static则为class锁而不是对象锁
- 对于同步方法块,锁是 synchronized 括号里配置对象
三、字节码角度分析 synchronized 实现
1. synchronized 同步代码块
1.1 文件反编译技巧
文件反编译
javap -c ***.class
文件反编译,-c表示对代码进行反汇编假如需要更多信息
javap -v ***.class
,-v 即 -verbose 输出附加信息(包括行号、本地变量表、反汇编等详细信息)
1.2 synchronized 同步代码块
/**
* 锁同步代码块
*/
public class LockSyncDemo {
Object object = new Object();
public void m1(){
synchronized (object){
System.out.println("-----hello synchronized code block");
}
}
public static void main(String[] args) {
}
}
1.3 class 文件反编译
执行命令 javap -c LockSyncDemo.class
public class com.georg.controller.LockSyncDemo {
java.lang.Object object;
public com.zhang.admin.controller.LockSyncDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putfield #3 // Field object:Ljava/lang/Object;
15: return
public void m1();
Code:
0: aload_0
1: getfield #3 // Field object:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter //**注****------进入锁
7: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #5 // String -----hello synchronized code block
12: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: aload_1
16: monitorexit // **注**------退出锁
17: goto 25
20: astore_2
21: aload_1
22: monitorexit //**注**-----这里又有一个exit, 目的当出现异常时,保证能够释放锁
23: aload_2
24: athrow
25: return
Exception table:
from to target type
7 17 20 any
20 23 20 any
public static void main(java.lang.String[]);
Code:
0: return
}
总结
- synchronized 同步代码块,实现使用的是
moniterenter
和moniterexit
指令(moniterexit
可能有两个) - 那一定是一个enter两个exit吗?(不一样,如果主动throw一个RuntimeException,发现一个enter,一个exit,还有两个athrow)
2. synchronized 普通同步方法
/**
* 锁普通的同步方法
*/
public class LockSyncDemo {
public synchronized void m2(){
System.out.println("------hello synchronized m2");
}
public static void main(String[] args) {
}
}
- 类似于上述操作,最后调用
javap -v LockSyncDemo.class
.....
public synchronized void m2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED //请注意该标志
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String ------hello synchronized m2
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/zhang/admin/controller/LockSyncDemo;
......
总结
调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置。如果设置了,执行线程会先持有monitor, 然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor
3. synchronized 静态同步方法
/**
* 锁静态同步方法
*/
public class LockSyncDemo {
public synchronized void m2(){
System.out.println("------hello synchronized m2");
}
public static synchronized void m3(){
System.out.println("------hello synchronized m3---static");
}
public static void main(String[] args) {
}
}
- 调用
javap -v LockSyncDemo.class
......
public static synchronized void m3();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED //访问标志 区分该方法是否是静态同步方法
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String ------hello synchronized m3---static
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 15: 0
line 16: 8
......
总结
ACC_STATIC 访问标志区分该方法是否是静态同步方法。
四、反编译解析 synchronized 锁的是什么
1. 管程概念
- 管程:Monitor(监视器),也就是我们平时说的锁。监视器锁
- 信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。 管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现线程级别的并发控制。
- 执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管理。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。
2. 解释为什么任何一个对象都可以成为一个锁
Java Object 类是所有类的父类,也就是说 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法。
ObjectMonitor.java
→ObjectMonitor.cpp
→objectMonitor.hpp
ObjectMonitor.cpp
中引入了头文件(include)objectMonitor.hpp
objectMonitor.hpp
属性 作用 _owner 指向持有ObjectMonitor对象的线程 _WaitSet 存放处于wait状态的线程队列 _EntryList 存放处于等待锁block状态的线程队列 _recursions 锁的重入次数 _count 用来记录该线程获取锁的次数 因此:每个对象天生都带着一个对象监视器
五、关于锁升级
这里只是简单提及,做个了解,后面会再做深入讲解
synchronized必须作用于某个对象中,所以Java在对象的头文件存储了锁的相关信息。锁升级功能主要依赖于 MarkWord 中的锁标志位和释放偏向锁标志位。
六、公平锁和非公平锁
1. 案例演示
1.1 非公平锁
使用 ReentrantLock 实现抢票的案例
class Ticket {
private int number = 30;
private Lock 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) {
Ticket ticket = new Ticket();
new Thread(() -> {for (int i = 1; i <= 55; i++) ticket.sale();}, "a").start();
new Thread(() -> {for (int i = 1; i <= 55; i++) ticket.sale();}, "b").start();
new Thread(() -> {for (int i = 1; i <= 55; i++) ticket.sale();}, "c").start();
}
}
运行结果,不同的线程抢到票的几率差距很大,线程执行不公平。
a 卖出第: 30 还剩下: 29
a 卖出第: 29 还剩下: 28
a 卖出第: 28 还剩下: 27
a 卖出第: 27 还剩下: 26
c 卖出第: 26 还剩下: 25
c 卖出第: 25 还剩下: 24
c 卖出第: 24 还剩下: 23
c 卖出第: 23 还剩下: 22
c 卖出第: 22 还剩下: 21
c 卖出第: 21 还剩下: 20
c 卖出第: 20 还剩下: 19
c 卖出第: 19 还剩下: 18
c 卖出第: 18 还剩下: 17
c 卖出第: 17 还剩下: 16
c 卖出第: 16 还剩下: 15
c 卖出第: 15 还剩下: 14
c 卖出第: 14 还剩下: 13
c 卖出第: 13 还剩下: 12
c 卖出第: 12 还剩下: 11
c 卖出第: 11 还剩下: 10
c 卖出第: 10 还剩下: 9
c 卖出第: 9 还剩下: 8
c 卖出第: 8 还剩下: 7
c 卖出第: 7 还剩下: 6
c 卖出第: 6 还剩下: 5
c 卖出第: 5 还剩下: 4
c 卖出第: 4 还剩下: 3
c 卖出第: 3 还剩下: 2
c 卖出第: 2 还剩下: 1
c 卖出第: 1 还剩下: 0
1.2 公平锁
使用 ReentrantLock 实现公平抢票的案例
class Ticket {
private int number = 30;
private Lock lock = new ReentrantLock(true); //默认用的是非公平锁,传 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) {
Ticket ticket = new Ticket();
new Thread(() -> {for (int i = 1; i <= 55; i++) ticket.sale();}, "a").start();
new Thread(() -> {for (int i = 1; i <= 55; i++) ticket.sale();}, "b").start();
new Thread(() -> {for (int i = 1; i <= 55; i++) ticket.sale();}, "c").start();
}
}
执行结果,每个线程都得到了较为公平的执行机会。
a 卖出第: 30 还剩下: 29
a 卖出第: 29 还剩下: 28
a 卖出第: 28 还剩下: 27
a 卖出第: 27 还剩下: 26
b 卖出第: 26 还剩下: 25
c 卖出第: 25 还剩下: 24
a 卖出第: 24 还剩下: 23
b 卖出第: 23 还剩下: 22
c 卖出第: 22 还剩下: 21
a 卖出第: 21 还剩下: 20
b 卖出第: 20 还剩下: 19
c 卖出第: 19 还剩下: 18
a 卖出第: 18 还剩下: 17
b 卖出第: 17 还剩下: 16
c 卖出第: 16 还剩下: 15
a 卖出第: 15 还剩下: 14
b 卖出第: 14 还剩下: 13
c 卖出第: 13 还剩下: 12
a 卖出第: 12 还剩下: 11
b 卖出第: 11 还剩下: 10
c 卖出第: 10 还剩下: 9
a 卖出第: 9 还剩下: 8
b 卖出第: 8 还剩下: 7
c 卖出第: 7 还剩下: 6
a 卖出第: 6 还剩下: 5
b 卖出第: 5 还剩下: 4
c 卖出第: 4 还剩下: 3
a 卖出第: 3 还剩下: 2
b 卖出第: 2 还剩下: 1
c 卖出第: 1 还剩下: 0
2. 公平锁/非公平锁 概念解析
- 公平锁:是指多个线程按照申请锁的顺序来获取锁,这里类似于排队买票,先来的人先买,后来的人再队尾排着,这是公平的—– Lock lock = new ReentrantLock(true)—表示公平锁,先来先得。
- 非公平锁:是指多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级反转或者饥饿的状态(某个线程一直得不到锁)—- Lock lock = new ReentrantLock(false)—表示非公平锁,后来的也可能先获得锁,默认为非公平锁。
3. 关于 公平锁/非公平锁 的相关问题
3.1 为什么会有公平锁/非公平锁的设计为什么默认非公平?
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
3.2 使⽤公平锁会有什么问题
公平锁保证了排队的公平性,非公平锁霸气的忽视这个规则,所以就有可能导致排队的长时间在排队,也没有机会获取到锁,这就是传说中的 “锁饥饿”
3.3 什么时候用公平?什么时候用非公平?
- 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;
- 否则那就用公平锁,大家公平使用。
七、可重入锁
1. 概念解析
可重入锁又名递归锁
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有
synchronized
修饰的递归调用方法,程序第2次进入被自己阻塞了这样递归方法也就不能继续执行下去了。所以Java中ReentrantLock
和synchronized
都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
2. 可重入锁种类
2.1 隐式锁
隐式锁(即synchronized关键字使用的锁)默认是可重入锁
作用于同步代码块
static Object objectLock = new Object(); public static void syncBlock() { new Thread(() -> { synchronized (objectLock) {// lock System.out.println("-----外层"); synchronized (objectLock) { System.out.println("-----中层"); synchronized (objectLock) { System.out.println("-----内层"); } } }//unlock },"t1").start(); }
打印结果:
-----外层 -----中层 -----内层
作用于同步方法中
public class ReEntryLockDemo { public synchronized void m1() { //指的是可重复可递归调用的锁,在外层使用之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁 System.out.println(Thread.currentThread().getName()+"\t"+"-----come in m1"); m2(); System.out.println(Thread.currentThread().getName()+"\t-----end m1"); } public synchronized void m2() { System.out.println("-----m2"); m3(); } public synchronized void m3() { System.out.println("-----m3"); } public static void main(String[] args) { ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo(); reEntryLockDemo.m1(); } } /** * main -----come in m1 * -----m2 * -----m3 * main -----end m1 */
打印结果:
main -----come in m1 -----m2 -----m3 main -----end m1
2.2 显式锁
显式锁(即Lock)也有 ReentrantLock 这样的可重入锁。
- ReentrantLock 实现的显示可重入锁
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t" + "-----外层");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t" + "-----内层");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}, "t1").start();
new Thread(() -> {
lock.lock();
try {
System.out.println("------22222");
} finally {
lock.unlock();
}
}, "t2").start();
}
打印结果:
t1 -----外层
t1 -----内层
------22222
3. Synchronized的可重入锁实现机理
再看 ObjectMoitor.hpp
140行
ObjectMonitor() {
_header = NULL;
_count = 0; //用来记录该线程获取锁的次数
_waiters = 0,
_recursions = 0; //锁的重入次数
_object = NULL;
_owner = NULL; //------最重要的----指向持有ObjectMonitor对象的线程,记录哪个线程持有了我
_WaitSet = NULL; //存放处于wait状态的线程队列
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //存放处于等待锁block状态的线程队列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
ObjectMoitor.hpp
底层:每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。_count
_owner
- 首次加锁:当执行
monitorenter
时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1
。 - 重入:在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器
加1
,否则需要等待,直至持有线程释放该锁。 - 释放锁:当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
4. 死锁及排查
4.1 死锁是什么
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
4.2 死锁产生的原因
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
4.3 死锁代码案例
public class DeadLockDemo {
static Object lockA = new Object();
static Object lockB = new Object();
public static void main(String[] args) {
Thread a = new Thread(() -> {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "\t" + " 自己持有A锁,期待获得B锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "\t 获得B锁成功");
}
}
}, "a");
a.start();
new Thread(() -> {
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "\t" + " 自己持有B锁,期待获得A锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "\t 获得A锁成功");
}
}
}, "b").start();
}
}
运行结果:
a 自己持有A锁,期待获得B锁
b 自己持有B锁,期待获得A锁
程序运行到此没有停止,一直等待着……
4.4 如何排查死锁
4.4.1 命令行
jps -l
查看当前进程运行状况jstack 进程编号
查看该进程信息
PS F:\> jps -l
29104
33360 com.atguigu.juc.locks.DeadLockDemo
11172 org.jetbrains.jps.cmdline.Launcher
27412 org.jetbrains.idea.maven.server.RemoteMavenServer36
30148 org.jetbrains.idea.maven.server.RemoteMavenServer36
45796 sun.tools.jps.Jps
####################################################################################
PS F:\> jstack 33360
2024-09-21 16:39:01
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.241-b07 mixed mode):
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x0000000026eea000 nid=0x8df0 in Object.wait() [0x000000002a8ff000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000716508ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x0000000716508ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000028ba8000 nid=0x3cb4 in Object.wait() [0x000000002a7ff000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000716506c00> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x0000000716506c00> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Periodic Task Thread" os_prio=2 tid=0x000000002b8e4000 nid=0x1378 waiting on condition
JNI global references: 317
Found one Java-level deadlock:
=============================
"b":
waiting to lock monitor 0x0000000028bab9f8 (object 0x00000007166f0fe8, a java.lang.Object),
which is held by "a"
"a":
waiting to lock monitor 0x0000000028bae338 (object 0x00000007166f0ff8, a java.lang.Object),
which is held by "b"
Java stack information for the threads listed above:
===================================================
"b":
at com.atguigu.juc.locks.DeadLockDemo.lambda$main$1(DeadLockDemo.java:44)
- waiting to lock <0x00000007166f0fe8> (a java.lang.Object)
- locked <0x00000007166f0ff8> (a java.lang.Object)
at com.atguigu.juc.locks.DeadLockDemo$$Lambda$2/1199823423.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"a":
at com.atguigu.juc.locks.DeadLockDemo.lambda$main$0(DeadLockDemo.java:27)
- waiting to lock <0x00000007166f0ff8> (a java.lang.Object)
- locked <0x00000007166f0fe8> (a java.lang.Object)
at com.atguigu.juc.locks.DeadLockDemo$$Lambda$1/1342443276.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
4.4.2 图形化工具
这里使用JDK 自带的 jconsole
演示,此工具在jdk的安装的 bin目录
下,双击 jconsole.exe 打开即可。
八、总结
指针指向monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp,C++实现的)
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 george_95@126.com