JUC学习之锁

1、悲观锁与乐观锁

1.1 悲观锁

在要读取或写数据时,先对数据加锁,保证在读写过程中不会有其他线程进来修改该数据,在读写操作以及操作结束后再释放锁。 因此悲观锁适用于需要频繁写操作的代码中。

1.2 乐观锁

认为自己在访问数据时没有其他线程对该数据进行写操作,因此对数据不进行加锁操作。而当要进行写操作时,如果发现数据没有被修改,那么就直接进行写操作。 如果当数据被修改了,那么就执行相关的操作,如:放弃修改、重试抢锁等等。 判断规则

  • version版本号:假设第一次访问该数据时拿到的版本号为1,在后续进行访问时发现版本号发生了变化,那么就要执行相关的操作。
  • CAS算法:Java原子类中的递增操作就是通过CAS自旋实现的。 因此乐观锁适用于需要频繁进行读操作的代码中。

2、8种情况的锁使用

在阿里巴巴的Java开发手册中强调 高并发时,同步调用应该取考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。 说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC方法。 ==类锁==:每个类在加载的过程中都会由==ClassLoader==类加载器生成一个模板对象==Class==,所有的new对象都从这个模板对象==Class==中去创建实例,所以类锁指的是对这个模板对象==Class==进行加锁。 ==对象锁==:每个对象都通过对应类的模板对象==Class==创建实例,创建出来的都是不一样的,所以对象锁指的是对某个实例进行加锁。

2.1 8种锁的情况

2.1.1 标准访问有ab两个线程,请问先打印邮件还是短信

java 复制代码
public static void main(String[] args) {  
    Phone phone1 = new Phone();  
    
    new Thread(() -> {  
        phone1.sendEmail();  
    },"a").start();  
  
    new Thread(() -> {  
        phone1.sendSMS();  
    },"b").start();
    
}

class Phone {  
    public synchronized void sendEmail() {  
        System.out.println("------ sendEmail");  
    }  
  
    public synchronized void sendSMS() {  
        System.out.println("------ sendSMS");  
    }  
  
}

运行的结果为:

bash 复制代码
------ sendEmail
------ sendSMS

通过输出结果可以看到,此时是Email先输出,SMS后输出。 ==解析==:这是两个普通的同步方法,对于普通的同步方法,使用的是对象锁,在这道题中,两个线程ab都使用了同一个对象phone1,因此,当其中一个线程使用对象phone1去调用其中一个同步方法时,这个对象就被锁住了,导致另外的线程想要使用这个对象时,发现对象已经被锁住了,因此拿不到锁,就不能进行调用。

2.1.2 sendEmail方法中暂停3秒钟,先打印邮件还是短信

sendEmail方法暂停3秒钟

java 复制代码
public synchronized void sendEmail() {  
    try {  
        TimeUnit.SECONDS.sleep(3);  
    } catch (InterruptedException e) {  
        throw new RuntimeException(e);  
    }  
    System.out.println("------ sendEmail");  
}

此时的运行结果为:

bash 复制代码
------ sendEmail
------ sendSMS

可以发现,跟前面的结果一样,这是为什么呢??

解析:这里其实跟上面一样,都是用的同一个对象,所以不管哪个方法暂停多久,第一个线程进去后,只有等这个线程将该锁释放,其他线程才能争夺锁并访问。

2.1.3 添加一个普通的hello方法,先打印邮件还是hello

添加的普通hello方法

java 复制代码
public void hello() {  
    System.out.println("------ hello");  
}

此时运行的结果为:

bash 复制代码
------ hello
------ sendEmail

这又是为什么呢?

解析:普通方法不涉及到锁的概念,因此所有的线程都可以同时访问。

2.1.4 有两部手机,先打印邮件还是短信

java 复制代码
new Thread(() -> {  
            phone1.sendEmail();  
        },"a").start();  
  
new Thread(() -> {  
//    phone1.sendSMS();  
//    phone1.hello();  
	phone2.sendSMS();  
},"b").start();

此时的运行结果为:

bash 复制代码
------ sendSMS
------ sendEmail

解析:两部手机,相当于两个不同的对象,而不同的对象在访问普通同步方法时,会分别获得一个锁,两个锁之间没有任何联系,互不干扰。

2.1.5 有两个静态同步方法,有一部手机,先打印邮件还是短信

将上述方法修改为静态的

java 复制代码
public static synchronized void sendEmail() {  
    try {  
        TimeUnit.MILLISECONDS.sleep(200);  
    } catch (InterruptedException e) {  
        throw new RuntimeException(e);  
    }  
    System.out.println("------ sendEmail");  
}  
  
public static synchronized void sendSMS() {  
    System.out.println("------ sendSMS");  
}

此时运行的结果为:

bash 复制代码
------ sendEmail
------ sendSMS

解析:静态同步方法使用的类锁,是对 对应类的模板对象==Class==进行加锁,而模板对象只有一个,因此当一个线程访问静态同步方法时,其他线程同样也想访问就只能先等待。

2.1.6 有两个静态同步方法,有两部手机,先打印邮件还是短信

静态同步方法

java 复制代码
public static synchronized void sendEmail() {  
    try {  
        TimeUnit.MILLISECONDS.sleep(200);  
    } catch (InterruptedException e) {  
        throw new RuntimeException(e);  
    }  
    System.out.println("------ sendEmail");  
}  
  
public static synchronized void sendSMS() {  
    System.out.println("------ sendSMS");  
}

两部手机

java 复制代码
public static void main(String[] args) {  
        Phone phone1 = new Phone();  
        Phone phone2 = new Phone();  
        new Thread(() -> {  
            phone1.sendEmail();  
        },"a").start();  
  
        new Thread(() -> {  
            phone2.sendSMS();  
        },"b").start();  
  
    }

此时运行的结果为:

bash 复制代码
------ sendEmail
------ sendSMS

解析:静态同步方法使用的类锁与对象无关,是对 对应类的模板对象==Class==进行加锁,而模板对象只有一个,因此不管有几部手机,最后都会对类锁进行争夺。

2.1.7 有一个静态同步方法,一个普通同步方法,一部手机,先打印邮件还是短信

java 复制代码
public static synchronized void sendEmail() {  
    try {  
        TimeUnit.MILLISECONDS.sleep(200);  
    } catch (InterruptedException e) {  
        throw new RuntimeException(e);  
    }  
    System.out.println("------ sendEmail");  
}  
  
public synchronized void sendSMS() {  
    System.out.println("------ sendSMS");  
}

此时运行的结果为:

bash 复制代码
------ sendSMS
------ sendEmail

解析:静态同步方法使用的类锁,普通同步方法使用的是对象锁,两个锁不一样,因此同一部手机调用时互不干扰。

2.1.8 有一个静态同步方法,一个普通同步方法,两部手机,先打印邮件还是短信

java 复制代码
public static synchronized void sendEmail() {  
    try {  
        TimeUnit.MILLISECONDS.sleep(200);  
    } catch (InterruptedException e) {  
        throw new RuntimeException(e);  
    }  
    System.out.println("------ sendEmail");  
}  
  
public synchronized void sendSMS() {  
    System.out.println("------ sendSMS");  
}

此时运行的结果为:

bash 复制代码
------ sendSMS
------ sendEmail

解析:静态同步方法使用的类锁,普通同步方法使用的是对象锁,两个锁不一样,因此不同的对象分别去调用静态同步方法和普通同步方法时,会去争抢对应的锁,而不同锁之间互不干扰。

2.1.9 小结

对于[[#2.1 标准访问有ab两个线程,请问先打印邮件还是短信]]和[[#2.2 sendEmail方法中暂停3秒钟,先打印邮件还是短信]]。得出的结论是: 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其他的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其他线程 都不能进入到当前对象的其他的synchronized方法。

对于[[#2.3 添加一个普通的hello方法,先打印邮件还是hello]]和[[#2.4 有两部手机,先打印邮件还是短信]],得出的结论是: 普通方法不涉及到锁的概念,多个线程可以同时访问 如果是两个对象,在访问同步方法时,因为对象不一样而导致锁不一样,所以两者不涉及到锁的概念。

对于[[#2.5 有两个静态同步方法,有一部手机,先打印邮件还是短信]]和[[#2.6 有两个静态同步方法,有两部手机,先打印邮件还是短信]],得出的结论是:

  • 对于静态同步方法,因为静态方法的特殊性,所有对象访问同一个静态方法(这里的相同指的是只有这一个静态同步方法,而不是每个对象有一个静态同步方法),因此锁的是当前类的Class对象(类锁)。
  • 对于普通同步方法,锁的是当前实例对象,每个对象都会有一个普通同步方法,即对象锁。
  • 对于同步方法块,锁的是synchronized括号里的对象

对于[[#2.7 有一个静态同步方法,一个普通同步方法,一部手机,先打印邮件还是短信]]和[[#2.8 有一个静态同步方法,一个普通同步方法,两部手机,先打印邮件还是短信]],得出的结论是: 对于一个静态同步方法,锁的是Class对象,对于一个普通同步方法,锁的是当前对象。当两个不同的对象一个访问静态同步一个访问普通同步时,两者因为锁的对象不一样,所以互不干扰。

2.2、synchronized三种应用

2.2.1 jdk源码中notify的说明

notify是用于唤醒一个阻塞的线程,而线程会阻塞的原因是想要的资源得不到,因此暂停使用,而这里的资源指的是线程执行过程中需要使用的内容。而这些内容得不到的原因是,这些内容被设置成有限的,当有线程得到时,会对该内容进行加锁,使得其他线程无法使用该内容。 因此,就必然涉及到如何获得锁:

  • 在synchronized修饰的方法中可以获得锁
  • 在静态的synchronized修饰的方法中可以获得锁
  • 在由synchronized修饰的代码块中可以获得锁。 因此,synchronized的三种应用就在于上述的三个获得锁的方法中。

2.2.2 synchronized的三种应用

下面会用字节码的方式来分析这三种的区别,而查看一个程序的字节码可以使用javap -c 文件路径或者是javap -v 文件路径-c-v的区别在于,-v的内容会更详细些。

① 实例方法

当线程要进入该实例方法时,需要先获取这个对象的锁,只有获得了才能进入该实例方法。

java 复制代码
public synchronized void m1() {  
    System.out.println("hello non-static synchronized m1");  
}

查看其字节码,只列举出重要的部分,其余省略部分在jvm中有详细介绍。 [[JVM/JVM#2.0 字节码内容分析]]

bash 复制代码
public synchronized void m1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    ...

可以看到,在flags属性中有一个ACC_SYNCHRONIZED,来标识这是一个同步方法

② 代码块

会对代码块的括号的对象进行加锁。

java 复制代码
public void m2() {  
    synchronized (object) {  
        System.out.println("hello synchronized code block");  
    }  
}

字节码内容:

bash 复制代码
public void m2();
    ...
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         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           #8                  // 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
        23: aload_2
        24: athrow
        25: return
      ...

通过机器码前面的序号我们可以知道,第6到第16之间是m3方法的具体代码,刚好对应一个monitorenter和一个monitorexitmonitorenter用于加锁,monitorexit用于解锁。但是我们可以看到,第22行也有一个monitorexit,但是前面却没有再多一个monitorenter。这是因为,为了保证这个锁能正常的被解除,防止同步代码在运行的过程中因为出现了异常导致不能正常执行monitorexit而不能解锁,因此这是一个保底机制,如果出现了异常也会进行解锁操作。这一点可以通过第24行athrow进行验证,athorw是指抛出异常。一般情况下会是一个monitorenter对应两个monitorexit。但是也有一种特殊情况,即在同步方法中抛出异常,就会覆盖一个锁。

java 复制代码
public void m4() {  
    synchronized (object) {  
        throw new RuntimeException("test");  
    }  
}

对应的字节码

bash 复制代码
public void m4();
    ...
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: getfield      #3                  // Field object:Ljava/lang/Object;
         4: dup
         5: astore_1
         6: monitorenter
         7: new           #7                  // class java/lang/RuntimeException
        10: dup
        11: ldc           #8                  // String test
        13: invokespecial #9                  // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
        16: athrow
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
      ...

可以看到,在这里只有一个monitorenter和一个monitorexit

③ 静态方法

会对这个对象的模板对象Class进行加锁。

java 复制代码
public static synchronized void m3() {  
    System.out.println("hello static synchronized m3");  
}

字节码信息:

bash 复制代码
 public static synchronized void m3();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    ...

可以在flags属性中看到两个值,一个是ACC_STATIC,一个是ACC_SYNCHRONIZED,用这两个来标记这是一个静态同步方法,明显比同步方法多了一个ACC_STATIC值。

2.3、synchronized锁的是什么

面试题:为什么任何一个对象都可以成为一把锁

2.3.1 什么是管程monitor

面试题:synchronized实现原理,monitor对象什么时候生成的?知道monitor的monitorentermonitorexit怎么保证同步的,或者说,这两个操作计算机底层是如何执行的?

2.3.2 管程

管程是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。 执行线程就要求先成功持有管程,如何才能执行方法,最后当方法完成时(不论是正常完成还是非正常完成)释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。

2.3.3 C底层原语了解

在HotSpot虚拟机中,monitor采用ObjectMonitor实现。 jvm虚拟机底层是使用C++来实现的 ObjectMonitor.java 会在虚拟机上去调用 ObjectMonitor.cpp,而ObjectMonitor.cpp引入了objectMonitor.hpp头文件。 在objectMonitor.hpp头文件中有这样的定义

cpp 复制代码
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor(){
	_header = NULL;
	_count = 0;
	_waiters = 0;
	_recursions = 0;
	_object = NULL;
	_owner = NULL;
	_WaitSet = NULL;
	_WaitSetLock = 0;
	_Responsible = NULL;
	_succ = NULL;
	_cxq = NULL;
	FreeNext = NULL;
	_EntryList = 0;
	_SpinFreq = 0;
	_SpinClock = 0;
	OwnerIsThread = 0 ;
	_previous_owner_tid = 0;
}

其中下面这些是关键的属性:

属性 作用
owner 指向持有ObjectMonitor对象的线程
WaitSet 存放处于wait状态的线程队列
EntryList 存放处于等待锁block状态的线程队列
recursions 锁的重入次数
count 用来记录该线程获取锁的次数

3、公平锁与非公平锁

3.1 公平锁和非公平锁的定义

现有如下的测试代码:

java 复制代码
public class FairAndUnfairLock {  
  
    public static void main(String[] args) {  
        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();  
    }  
}  
  
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()+ "卖出第:" + number + "  还剩下:" + --number);  
            }  
        } finally {  
            lock.unlock();  
        }  
    }  
  
}

3.1.1 公平锁

多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的。 使用公平锁:

java 复制代码
Lock lock = new ReentrantLock(true);

使用了公平锁之后的运行结果为:

bash 复制代码
a卖出第:50  还剩下:49
a卖出第:49  还剩下:48
b卖出第:48  还剩下:47
a卖出第:47  还剩下:46
b卖出第:46  还剩下:45
a卖出第:45  还剩下:44
b卖出第:44  还剩下:43
c卖出第:43  还剩下:42
a卖出第:42  还剩下:41
b卖出第:41  还剩下:40
c卖出第:40  还剩下:39
a卖出第:39  还剩下:38
b卖出第:38  还剩下:37
c卖出第:37  还剩下:36
a卖出第:36  还剩下:35
b卖出第:35  还剩下:34
c卖出第:34  还剩下:33
a卖出第:33  还剩下:32
b卖出第:32  还剩下:31
c卖出第:31  还剩下:30
a卖出第:30  还剩下:29
b卖出第:29  还剩下:28
c卖出第: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

可以看到,abc三个线程都有机会获取到这个锁。

3.1.2 非公平锁

多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)。 使用非公平锁:

java 复制代码
// 两种方式都可以使用非公平锁
Lock lock = new ReentrantLock(false);
Lock lock = new ReentrantLock();

使用了非公平锁之后的运行结果为:

bash 复制代码
a卖出第:50  还剩下:49
c卖出第:49  还剩下:48
...
# 全为c
c卖出第:1  还剩下:0

3.2 面试题

3.2.1 为什么会有公平锁和非公平锁的设计?为什么默认非公平?

① 公平锁和非公平锁的设计

实际上公平锁和非公平锁各有其优缺点,适用的场景也不同。设计这两种锁是为了让开发人员能够根据实际情况选择适合自己应用需求的锁实现类型。

公平锁在锁竞争激烈的情况下,在保证资源按顺序访问的同时,可能会导致线程等待时间过长,降低系统性能。而非公平锁通过允许新到来的请求锁的线程直接抢占等待队列中的锁,可以显著提高系统处理并发请求时的吞吐量和性能,但有可能使线程饥饿,导致某些线程一直无法获取锁。

因此在实际开发中,需要根据具体情况综合考虑应用场景、性能要求、任务处理顺序等因素来选择锁实现类型。如果程序要求严格按照请求顺序获得锁,则可以使用公平锁;如果要求处理并发请求时的吞吐量和性能,则可以使用非公平锁。对于大多数应用场景而言,更倾向于使用非公平锁,因为它比公平锁更具灵活性和效率。

② 为什么默认非公平
  • 恢复挂起的线程到获取锁是有时间差的,从开发人员而言这个时间很小,但从CPU而言,这个时间差很明显。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。
  • 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,就减少了线程的开销。

3.2.2 什么时候使用公平,什么时候使用非公平

如果业务上只考虑实现这个功能而不考虑由谁来实现,那么用非公平就行 如果业务上除了考虑实现这个功能还要考虑每个能完成这个功能的模块能交替实现,那么就得用公平锁。

4、可重入锁(又名递归锁)

4.1 可重入锁定义

可重入锁是指在同一个线程在外层方法获取锁的时候,进入该线程的内层方法会自动获取锁(前提,锁对象是同一个对象),不会因为之前已经获取果还没释放而阻塞。

可重入锁的解释:

  • 可:可以
  • 重:再次
  • 入:进入
  • 锁:同步锁
  • 进入什么:进入同步域(同步代码块/方法或显示锁锁定的代码) 总之,可重入锁就是指一个线程中的多个流程可以获取同一把锁,持有这把锁可以再次进入内部的同步域。

4.2 可重入锁的种类

4.2.1 隐式锁

隐式锁:使用synchronized关键字使用的锁,默认是可重入锁。 下面用于测试可重入锁的使用

① 同步块
java 复制代码
private static void reEntryLockWithCodeBlock() {  
    Object o = new Object();  
    String threadName = Thread.currentThread().getName();  
    new Thread(() -> {  
        synchronized (o) {  
            System.out.println(threadName + "\t -----> 外层方法" );  
  
            synchronized (o) {  
                System.out.println(threadName + "\t -----> 中层方法" );  
  
                synchronized (o) {  
                    System.out.println(threadName + "\t -----> 内层方法" );  
                }  
            }        
        }    
    },"t1").start();  
}

此时的运行结果为:

bash 复制代码
t1	 -----> 外层方法
t1	 -----> 中层方法
t1	 -----> 内层方法

可以看到,三个同步块都正常执行。如果使用的不是可重入锁的话,那么,当线程t1进入外层方法时,获得了o对象锁。接着在要进入中层方法时,也要获得o对象锁,但是因为这个o对象锁在外层方法时已经被获取了,因此就要等待外层方法释放o对象锁,因此就形成了死锁,就不会接着往下走。而此时三个同步块正常执行,说明使用的是同一个锁,因此是可重入锁。

② 同步方法
java 复制代码
public static void main(String[] args) {  
    new Thread(() -> {  
        ReEntryLock reEntryLock = new ReEntryLock();  
        reEntryLock.m1();  
    },"t1").start();  
  
}  
  
public synchronized void m1() {  
    System.out.println(Thread.currentThread().getName() + "\t -----> come in m1" );  
    m2();  
    System.out.println(Thread.currentThread().getName() + "\t -----> m1 end" );  
}  
  
public synchronized void m2() {  
    System.out.println(Thread.currentThread().getName() + "\t -----> come in m2" );  
    m3();  
}  
  
public synchronized void m3() {  
    System.out.println(Thread.currentThread().getName() + "\t -----> come in m3" );  
}

此时的运行结果为:

bash 复制代码
t1	 -----> come in m1
t1	 -----> come in m2
t1	 -----> come in m3
t1	 -----> m1 end

这里使用的是reEntryLock这个对象锁。同理,如果不是可重入锁,那么在执行完m1方法后,m2方法是不能正常执行的。

③ synchronized可重入锁的原理

其原理是在objectMonitor.hpp头文件中定义的一些重要属性

属性 作用
owner 指向持有ObjectMonitor对象的线程
WaitSet 存放处于wait状态的线程队列
EntryList 存放处于等待锁block状态的线程队列
recursions 锁的重入次数
count 用来记录该线程获取锁的次数
通过_owner属性来记录哪个线程获得了锁,通过_count属性来记录这个线程获得锁的次数,通过_recursions来记录当前锁的重入次数。
所以获取锁的过程是:当一个线程要去获取某个锁时,会先去判断_count的值,当_count = 0时说明当前还没有其他线程获得该锁,因此可以获得该锁。当_count的值大于0时,说明有其他线程获得了该锁,因此就被阻塞了。

4.2.2 显式锁

使用ReentrantLock。ReentrantLock也有可重入锁,其可重入锁的过程与synchronized一样。

① 单线程环境下
java 复制代码
public static void main(String[] args) {  
    ReentrantLock 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();
}

此时运行的结果为:

java 复制代码
t1	 ----> 外层方法
t1	 ----> 内层方法

当调用lock()方法时,对应的_count属性值会为自增,当调用unlock()方法,对于的_count属性值会自减。假设此时减少了一次unlock(),那么此时的运行结果如何呢??

bash 复制代码
t1	 ----> 外层方法
t1	 ----> 内层方法

可以发现,跟刚刚的没区别,但是此时的_count = 1,而不为0。因为是单线程,没有其他线程过来获取,因此是正常的。

② 多线程环境下

在前面的基础上,我们添加一个t2线程

java 复制代码
new Thread(() -> {  
    lock.lock();  
    try {  
        System.out.println(Thread.currentThread().getName() +  "\t ----> 外层方法");  
    } finally {  
        lock.unlock();  
    }  
},"t2").start();

正常情况下的执行情况是:

bash 复制代码
t1	 ----> 外层方法
t1	 ----> 内层方法
t2	 ----> 外层方法

可以发现此时两个线程都正常执行了。 同样,我们让t1少释放一次锁。此时的运行结果又如何呢??

bash 复制代码
t1	 ----> 外层方法
t1	 ----> 内层方法

可以发现,此时只有t1线程执行了,而t2线程并没有执行,而且程序并没有终止。说明此时的t2线程进入了阻塞状态,原因在于t1线程在释放锁的过程中少释放了一次,导致_count的值一直为1。当t2线程要去获取这个锁时,发现_count值为1,因此进入了阻塞。

5、死锁及排查

5.1 死锁的定义

死锁指的是,多个线程因争夺资源而造成一种互相等待的现象。

实现一个死锁代码:

java 复制代码
public static void main(String[] args) {  
    Object o1 = new Object();  
    Object o2 = new Object();  
  
    new Thread(() -> {  
        String threadName = Thread.currentThread().getName();  
        synchronized (o1) {  
            System.out.println(threadName + "\t -----> 获取了o1锁");  
            System.out.println(threadName + "\t -----> 想要获取o2锁");  
  
            synchronized (o2) {  
                System.out.println(threadName + "\t -----> 获取了o2锁");  
            }  
        }    },"t1").start();  
  
    new Thread(() -> {  
        String threadName = Thread.currentThread().getName();  
        synchronized (o2) {  
            System.out.println(threadName + "\t -----> 获取了o2锁");  
            System.out.println(threadName + "\t -----> 想要获取o1锁");  
            synchronized (o1) {  
                System.out.println(threadName + "\t -----> 获取了o1锁");  
            }  
        }    },"t2").start();  
}

此时如果没有发生死锁的话,那么执行的结果应该为:

bash 复制代码
t1	 -----> 获取了o1锁
t1	 -----> 想要获取o2锁
t1   -----> 获取了o2锁
t2	 -----> 获取了o2锁
t2	 -----> 想要获取o1锁
t2	 -----> 获取了o1锁

那么,实际情况是这样子的嘛??

bash 复制代码
t1	 -----> 获取了o1锁
t1	 -----> 想要获取o2锁
t2	 -----> 获取了o2锁
t2	 -----> 想要获取o1锁

可以发现,运行结果并不是所预想的那样子,而没有执行的方法都在各自的第二个synchronized中,而且程序也没有终止。

5.2 排查死锁

当程序没有终止时,要么出现了死循环,要么出现了死锁。那么我们应该如何判断这是一个由死锁导致的程序没有终止呢?

5.2.1 纯命令

使用jps -l查看当前运行的程序的线程号,使用jstack 线程号查看具体的原因。 jps -l查看线程号

bash 复制代码
PS F:\Projects\novel-master\JUCStudy\target\classes\com\linhanji\juc\lock> jps -l 
36672 org.jetbrains.jps.cmdline.Launcher
25652 com.linhanji.juc.lock.DeadLock
37268 sun.tools.jps.Jps
4168 

jstack 查看原因

bash 复制代码
PS F:\Projects\novel-master\JUCStudy\target\classes\com\linhanji\juc\lock> jstack 25652
...
Found 1 deadlock.

可以看到,执行完jstack之后显示了Found 1 deadlock,说明确实由死锁导致的。

5.2.2 图形化

使用jconsole打开图形化界面,并且连接对应的线程号。

相关推荐
YMWM_1 分钟前
第一章 Go语言简介
开发语言·后端·golang
y25085 分钟前
《Object类》
java·开发语言
曙曙学编程7 分钟前
初级数据结构——树
android·java·数据结构
BestandW1shEs12 分钟前
彻底理解消息队列的作用及如何选择
java·kafka·rabbitmq·rocketmq
爱吃烤鸡翅的酸菜鱼15 分钟前
Java算法OJ(8)随机选择算法
java·数据结构·算法·排序算法
码蜂窝编程官方18 分钟前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
Viktor_Ye34 分钟前
高效集成易快报与金蝶应付单的方案
java·前端·数据库
hummhumm36 分钟前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
一二小选手41 分钟前
【Maven】IDEA创建Maven项目 Maven配置
java·maven
J老熊1 小时前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程