Java中的锁

1.什么是锁,锁有什么用?

Java中的锁是用于控制多个线程对共享资源进行访问的机制,以确保在任何给定时刻只有一个线程能够访问共享资源。锁的使用有助于避免多线程并发访问导致的数据竞争和不确定性行为,从而确保线程安全。

2.锁的理念

锁的理念主要有以下几种:

  1. 悲观锁(Pessimistic Locking):悲观锁的核心思想是在多线程环境中,悲观地认为共享资源会被频繁地修改,因此在访问共享资源之前,先获取锁,以确保只有一个线程能够访问资源,其他线程需要等待。典型的悲观锁就是使用 synchronized 关键字的同步代码块或方法。

    • 适用于写操作多、读操作少的情况。

    • 缺点是可能导致性能下降,因为大部分时间资源处于锁定状态,其他线程需要等待。

  2. 乐观锁(Optimistic Locking):乐观锁的核心思想是在多线程环境中,乐观地认为竞争的概率较低,因此允许多个线程同时访问共享资源,但在更新资源时,先检查资源是否被其他线程修改,如果没有被修改,就进行更新,否则进行回滚或重试。乐观锁常用的实现方式是版本控制,每个资源带有一个版本号,每次更新都会增加版本号。

    • 适用于读操作多、写操作少的情况。

    • 优点是能够提高性能,缺点是需要额外的版本控制机制,处理冲突可能较复杂。

  3. 自旋锁(Spin Locking):自旋锁的核心思想是在获取锁时,如果发现锁已经被其他线程占用,不立即进入阻塞状态,而是自旋等待一段时间,期望其他线程会释放锁,如果等待一定时间后锁仍未释放,才会进入阻塞状态等待。自旋锁的效率取决于自旋等待时间和处理器的性能。

    • 适用于锁被占用时间较短的情况,可以减少线程切换的开销。

    • 缺点是如果锁被占用时间过长,会浪费CPU资源。

  4. 轻量级锁(Lightweight Locking):轻量级锁是一种介于自旋锁和重量级锁之间的锁,它的核心思想是在获取锁时,使用CAS(Compare and Swap)操作来尝试获取锁,如果成功则继续执行,否则升级为自旋锁或重量级锁。

    • 适用于锁的竞争不激烈的情况,可以减少线程切换的开销。

    • 缺点是在竞争激烈的情况下,会升级为自旋锁或重量级锁,性能可能下降。

  5. 重量级锁(Heavyweight Locking):重量级锁是最传统的锁,它的核心思想是使用操作系统提供的阻塞机制,在获取锁时,如果锁已经被其他线程占用,当前线程会被阻塞,等待锁的释放。

    • 适用于锁的竞争激烈的情况,能够确保线程安全。

    • 缺点是线程切换开销较大,性能相对较低。

这些锁的理念和使用方式不同,选择合适的锁取决于应用场景和性能需求。在实际开发中,可以根据具体情况选择合适的锁来保障多线程程序的正确性和性能。

3.实现锁的方式

在 Java 中,最常用的锁实现通常是通过 synchronized 关键字和 ReentrantLock 类来完成的。这两种方式都提供了可靠的锁机制,可以确保多线程下的数据访问安全。它们的主要特点如下:

  1. synchronized 关键字

    • 是 Java 内置的关键字,用于实现同步。

    • 通过在方法上或代码块中添加 synchronized 关键字来创建同步块,从而限制只有一个线程可以访问这部分代码。

    • 自动释放锁,即使在代码块中发生异常,也会释放锁。

    • 支持可重入性,同一个线程可以多次获取同一把锁,不会发生死锁。

    • 简单易用,适合大多数情况下的锁需求。

  2. ReentrantLock 类

    • 是 Java 标准库提供的一个具有扩展功能的锁实现。

    • 需要显式地创建锁对象,使用 new ReentrantLock() 来创建。

    • 提供了更多的灵活性,如可中断锁、定时锁、公平锁等,适用于复杂的同步需求。

    • 需要手动释放锁,因此要小心避免出现忘记释放锁的情况。

    • 支持可重入性。

选择使用哪种锁取决于具体的应用场景和需求。通常情况下,对于简单的同步需求,synchronized 关键字足够满足要求,并且更容易使用。而对于更复杂的同步需求,ReentrantLock 提供了更多的控制选项,可以更好地满足需求。

需要注意的是,无论使用哪种锁,都应该谨慎处理锁的释放,以避免死锁和资源泄漏等问题。同时,应该根据具体的业务需求选择合适的锁策略,以确保程序的性能和正确性。

4.锁、对象、类之间的关系

锁与类和对象之间的关系主要涉及到多线程编程中的同步和互斥。下面是一些关键点:

  1. 锁与类的关系: 锁是一种机制,用于在多线程环境下保护类的实例或静态数据。不同线程可以尝试获取锁,以控制对共享资源的访问。锁可以基于类的实例级别或类级别,具体取决于使用的锁类型。

  2. 锁与对象的关系: 锁通常与对象实例相关联。当多个线程尝试获取同一个对象实例上的锁时,只有一个线程能够成功,其他线程将被阻塞。这种锁称为对象级别的锁。

  3. 锁的种类: Java 中有内置锁(synchronized 关键字)、显示锁(如 ReentrantLock)、读写锁(ReentrantReadWriteLock)等不同类型的锁。内置锁与对象关联,而显示锁则是通过创建锁对象来管理。读写锁允许多个线程同时读取共享资源,但在写操作时需要独占锁。

  4. 锁与同步: 锁用于实现线程的同步,确保多个线程协调执行共享资源的访问,以避免竞争条件和数据不一致性。同步可以通过锁机制来实现,以保证线程安全。

  5. 锁与并发控制: 锁是一种并发控制机制,用于控制多个线程的执行顺序和互斥访问。通过适当的锁策略,可以防止多个线程同时修改共享数据,从而确保数据的一致性。

  6. 类锁与对象锁:对象锁和类锁的区别在于sync关键字作用于的方法或者代码是否为静态。是则是类锁反之对象锁。

总之,锁在多线程编程中起着关键作用,用于管理线程的并发访问,保护共享资源的完整性,以及确保线程安全。锁可以与类的实例或类的静态数据相关联,具体取决于应用程序的需求和设计。

5.Java对象内存结构

Java对象=对象头+对象体+对齐填充+填充数据

对象头
  • 对象头包含了一些元数据,如对象的哈希码、对象锁的信息、垃圾回收标记等。

  • 它的大小通常为 8 字节或更多,具体取决于 JVM 实现和对象的类型。

对象体
  • 对象体包含对象的字段值,即你在类中定义的各种成员变量。

  • 每个字段的大小取决于其数据类型,例如,一个整数字段占用 4 字节,一个引用字段占用 4 或 8 字节,一个浮点数字段占用 4 字节,等等。

对齐填充
  • 对齐补充是为了确保整个对象的大小是按照特定的对齐方式对齐的。

  • 这通常是对象大小不是 2 的幂次方时需要的一些额外字节。

填充数据
  • 填充数据是为了保证对象的起始地址是按照特定的对齐方式对齐的

  • JVM 使用内存对齐来提高访问速度,因此在对象头和实例数据之间可能需要添加填充数据。

  • 填充数据的大小取决于 JVM 和操作系统的要求。

总的来说:实际一个对象存储对象头、对象体,但是由于要保证对象存储位置的前后对齐、对象大小的对称从而需要添加填充数据和对齐填充。

细讲对象头

对象头是Java对象在内存中的一个重要部分,它包含了一些用于管理对象的元信息。对象头的结构可以因不同的JVM实现而异,但通常包括以下信息:

  1. 对象的哈希码(HashCode):这是对象的标识符,用于在哈希表等数据结构中查找对象。

  2. 对象的锁信息:这包括对象的锁状态,例如是否被某个线程持有、是否处于轻量级锁或重量级锁状态等。这些信息用于支持对象的同步操作,如synchronized关键字。

  3. 对象的垃圾回收信息:JVM 使用对象头来进行垃圾回收。对象头中包含了一些标志位,用于标识对象是否可以被回收、是否已被回收等信息。

  4. 对象的类型信息:对象头中通常包含了指向对象的类元数据(Class Metadata)的指针,用于确定对象的类型和方法调度。

  5. 其他元数据:根据具体的JVM实现和对象的状态,对象头还可能包含其他元数据,如数组长度等。

需要注意的是,对象头通常占用的内存空间是固定的,不随对象的大小而变化。对象头的具体结构和占用空间大小可能会因不同的JVM实现而异,但通常在32位JVM中占用4字节,而在64位JVM中占用8字节。

结构图

我想要表达的是当我们的锁是对象锁或者类锁的时候都会有相应的信息保存,前者保存在请求头中而后者则是保存在字节码中

6.对象头、Monitor、Mark Word概念以及工作原理

概念:

对象头:从上述我们已经知道是用于存储对象信息的

Monitor:Monitor 是一种同步机制,用于保护共享资源的访问,防止多个线程同时修改共享资源而导致的数据不一致性。 在 Java 中,Monitor 通常与 synchronized 关键字一起使用。 每个对象都有一个关联的 Monitor,它包含了锁的状态(锁被占用或者空闲)、拥有锁的线程信息、等待队列等。 synchronized 保护的代码块时,它会尝试获取对象的 Monitor,如果锁被占用,线程将进入等待队列,直到获得锁才能执行代码。

Mark Word: Mark Word 是对象头的一部分,主要用于垃圾回收和同步。 它包含了对象的哈希码、锁信息、垃圾回收标记等。 具体的结构和位

  • 锁标志位:用于标识锁的状态,包括无锁、偏向锁、轻量级锁和重量级锁。

  • 偏向线程 ID:记录持有偏向锁的线程 ID。

  • 偏向时间戳:记录偏向锁的时间戳,用于判断是否达到撤销偏向锁的条件。

  • 分代年龄:用于垃圾回收,标

  • 是否是数组:用于区分对象是否

  • 对齐位:保证对象在内存中的对齐。

注意这里提到了Mark Word是对象头的一部分但是上述的对象头中并没有出现这么一个东西,那么Mark Word到底在哪里?用来干什么?

先上结论:MarkWork是对象头的一部分,从另外一个角度看对象头=Mark Word、+Klass Word。从上述的markword中可知它包含着之前结构图中除去对象信息和其他元信息的部分。也就是相当于分多了一级。

结构图更新为:

对象头大小问题:

我们需要根据jvm的位数和对象的种类来进行分类确定大小:32位普通对象、32位数组对象、64位普通对象、64位数组对象。

32位情况下

普通对象=32+32=64bit

数组对象=32+32+32=96bit

64位的情况下

普通对象=64+64=128bit

数组对象=64*3=192bit

知识补给(32、64位区别于作用):

32、64指的是什么?

32 位和 64 位指的是虚拟机的寻址能力,也就是它们可以表示的内存地址的数量。在一个 32 位虚拟机中,最大地址容量是 2^32,约为 4GB。而在一个 64 位虚拟机中,最大地址容量是 2^64。

有什么用?

这一寻址能力的差异也会影响到虚拟机中数据模型的大小和最大容量。例如,在 64 位虚拟机中,一个指针通常需要 8 字节来表示,而在 32 位虚拟机中只需要 4 字节。因此,对象和数据结构在 64 位虚拟机中可能会占用更多的内存空间。

位,字节,bit,byte之间的关系?
  1. 位(bit): 位是最小的数据单元,可以存储 0 或 1。

  2. 字节(byte): 字节是由 8 个位组成的单位。 一个字节可以表示 256(2^8) 种不同的值。

  3. 位和字节的关系: 1 字节等于 8 位。 也就是说,8 个位可以组成一个字节。

  4. 字节和其他单位的关系: 字节是更高级别的单位,通常用于描述文件大小、内存大小、网络传输速度等。通常,1 字节等于 8 位,1 千字节(KB)等于 1024 字节,1 兆字节(MB)等于 1024 KB,以此类推。

这些知识与对象头的联系

也就是说不同的寻址能力下的数据模型大小不同从而导致对象头的大小不同会随着情况而变化,比如32位的情况下普通对象的对象头的大小就相当于32+32bits而64的则为64+64bits.而这些数据模型的大小会影响到我们的Markword内部的字位段分配。

MarkWord内部解

首先MarkWord是一组位字段,用于存储对象的各种信息,包括锁状态、垃圾回收标记等。用大白话来将就是一串32位数字分段来存储不同的信息。

结构图:

32位虚拟机中的结构图:

img

64位虚拟机中的结构图:

img

Mark Word 不同状态

1、Normal状态:此状态为普通状态,hashcode为对象的hashcode值 , age代表垃圾回收的分代年龄, biased_lock表示是否为偏向锁,最后两位代表加锁状态。

2、Biased状态:此状态为偏向锁状态,thread指向获得偏向锁的线程,后3位为101表示对象为偏向锁状态。

3、Lightweight Locked状态:轻量级锁状态,ptr_to_lock_record指向栈帧的锁记录。

4、Heavyweight Locked状态:重量级锁,ptr_to_heavyweight_monitor指向Monitor

Monitor

Monitor 是监视器的意思,在Java中被synchronized关键字修饰的对象头且为重量级锁时,会关联一个Monitor对象,Monitor有Owner、EntryList、WaitSet三个字段,分别表示Monitor的持有者线程(获得锁的线程)、阻塞队列、和等待队列。如下图: img

上图的情况下,MarkWord状态应为heavyweight Locked,ptr_to_heavyweight_monitor占用30位指向Monitor对象。Thread-2为Monitor的持有者,因此Thread-2是获得锁的线程,其它争抢锁的线程进入阻塞队列中。 注意:不加synchronized关键字的对象,是不会关联Monitor对象的。只有重量级锁对象才会关联Monitor。

7.无锁、偏向锁、轻量锁、重量锁--锁的四种状态

无锁

适用情况

没有锁任何线程都能进入并且修改

偏向锁

适用情况

适用于同一个线程多次获取一个锁的情况的出现,因此没有必要每次都要竞争锁,从而降低获取锁的代价。

执行过程
无锁转变为偏向锁(没有线程占用到有一个线程占用)

Mark Word中存储的内容会进行变化--->线程ID(23)+Epoch(2)+分代年龄(4)+是否为偏向锁(1)+锁标志位(01)

当锁升级为偏向锁后有线程来获取锁

对比当前线程ID和Java对象头的线程ID。

  1. 如果一致,就可以直接获取锁。

  2. 如果不一致,说明存在其他线程需要竞争锁对象,那么就需要查看Java对象头的记录的线程是否存活

    1. 如果没有存活则会将锁对象重置为无锁状态,其他线程都可以竞争将其设置为偏向锁。

    2. 如果存活,那么立刻查找该线程的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

轻量锁

适用情况

多个线程进行锁的竞争时。(竞争较为轻松-自旋次数未达到阈值)

执行过程

情景:已近有一个线程占用了偏向锁,另一个线程后来也想获取锁(a,b)

首先b线程会在自己的(内存/栈帧)中开辟一个区域存储锁记录的空间(Displaced Mark Word)

  1. 这部分是用于存储对象的MarkWord区域的信息,同时Markword区域存储的内容转变为对应位置的指针

  2. 线程用CAS的方式去尝试修改对象头中的锁区域信息将锁信息替换成线程b的

    1. 替换成功,线程B获取该锁

    2. 替换失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。

    3. 如果自旋次数到了线程B还没有释放锁,或者线程B还在执行,线程A还在自旋等待,这时又有一个线程C过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

轻量锁的释放(解锁)

轻量级锁的释放过程中,关键操作就是将对象的 markword 恢复为之前保存在 DisplacedMarkword 区域的内容,以还原锁的记录。 这个操作是为了将锁恢复到获取锁之前的状态

撤销操作

如果解锁失败,当前线程就会进入撤销操作。 撤销操作的目的是将锁对象恢复到正常状态,通常 markword 恢复为无锁状态。这个操作是有代价的,因为它需要额外的自旋时间来等待其他线程完成对锁的访问。如果撤销操作成功,那么当前线程可以安全地释放锁。如果撤销操作失败,说明其他线程已经访问了锁,那么当前线程可能需要进入等待状态,等待锁的真正释放。

重量锁

适用情况

多个线程进行锁的竞争时。(竞争较为激烈-自旋达到阈值)

执行过程

Monitor流程图

首先Markword会从先前存储的DisplacedMarkword区域指针变成指向monitor的指针。

  1. 新线程请求获取锁:当一个新线程尝试获取一个已经被其他线程持有的重量级锁时,它会发起请求。

  2. 新线程进入阻塞状态:由于重量级锁是一种互斥锁,同一时刻只能有一个线程持有,因此新线程需要等待锁的释放。 此时,新线程进入阻塞状态。

  3. 新线程加入等待队列:新线程被放入等待队列(Wait Queue)中,等待队列是一个双向链

  4. 锁的拥有者释放锁:当原本持有锁的线程完成任务后,会释放锁。

  5. 唤醒等待线程:一旦锁被释放,JVM会从等待队列中唤醒队头的线程。这个被唤醒的线程会尝试获取锁。

  6. 竞争锁:被唤醒的线程会尝试竞争锁,如果成功获取锁,它将成为锁的新拥有者,然后继续执行。

  7. 等待队列的线程继续等待:如果被唤醒的线程没有竞争成功(例如,有其他线程在它前面竞争到了锁),它将继续等待在等待队列中,等待下一次被唤醒的机会。

需要注意的是,等待队列中的线程通常按照等待时间最长的先被唤醒,以确保公平性。 这种情况下,最早请求锁的线程将最早被唤醒,从而减少线程饥饿问题

总结流程

也就是说新的线程会先进入等待队列(Entry Set)中,当Owner中的线程对锁进行释放后根据先进先出的原则进行唤醒等待队列中的线程(通过判断等待时间来判断先后),WaitSet中的线程是在执行过程中调用了wait方法进而需要被唤醒才能够执行的线程。

8.公平锁和非公平锁 (原理?)

公平锁

概念

公平锁指的是线程按照申请顺序来获取锁,线程直接进入队列中排队,队列中的第一个能获取到锁。(Cpu只需要唤醒下一个对头)

优点
  1. 公平性: 公平锁会按照线程请求锁的顺序来分配锁,保证了线程的公平性。先来的请求会先获得锁,这种策略有助于避免线程饥饿问题。

  2. 合理分配资源: 公平锁能够合理分配资源,不会出现某些线程一直占用锁的情况,保证每个线程都有机会获得资源。

缺点
  1. 性能较低: 由于公平锁需要维护一个线程队列以按照请求的顺序分配锁,因此在高并发情况下,性能可能较低。公平锁会引入额外的线程调度开销。

  2. 可能会引发活跃性问题: 严格的公平性可能导致活跃性问题,例如死锁或线程长时间等待。如果一个线程一直无法获取到锁,其他线程也无法释放锁,可能会导致程序无法继续执行。

非公平锁

概念

非公平锁在争夺锁的时候,不会按照线程请求锁的顺序来分配锁,而是让所有线程都竞争锁,谁先竞争成功就会获取到锁。这意味着线程是同一起跑线的,不会排队等待锁。(Cpu每一轮都需要唤醒所有在阻塞的线程)

优点
  1. 性能较高: 非公平锁不关心线程请求锁的顺序,如果锁当前是空闲状态,任何线程都可以立即获取锁,这样可以提高性能。

  2. 不会引发活跃性问题: 非公平锁不会像公平锁那样严格按照顺序分配锁,因此不容易引发活跃性问题。

缺点
  1. 不公平: 非公平锁可能导致某些线程一直获取锁,而其他线程长时间无法获取到锁,不够公平。

  2. 可能造成线程饥饿: 如果某些线程一直获取锁,其他线程可能会长时间等待,造成线程饥饿问题。

创建代码
// 创建一个公平锁
        Lock fairLock = new ReentrantLock(true);
        // 创建一个非公平锁(默认)
        Lock unfairLock = new ReentrantLock();
/*
创建一个ReentrantLock()不带入true时就是默认创建非公平锁
*/

9.可重入锁(递归锁)、非可重入锁

可重入锁

概念

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

优点
  1. 可重入性 :允许同一个线程多次获得锁,这对于一些递归算法和复杂的代码结构非常有用。

  2. 灵活性 :可重入锁提供了丰富的锁获取方式,可以根据需要选择公平锁或非公平锁,也可以指定超时时间。

  3. 中断响应 :可重入锁支持线程的中断响应,即当一个线程在等待锁的过程中可以被中断。

  4. 条件等待 :可重入锁提供了条件等待的功能,可以方便地实现线程的等待和通知机制。

缺点
  1. 性能开销 :相比于 synchronized 关键字,可重入锁通常会有更高的性能开销,因为它需要维护更多的信息来支持可重入性和其他功能。

  2. 复杂性 :可重入锁的使用相对于 synchronized 更加复杂,需要显式地获取和释放锁,容易出现错误使用的情况。

  3. 竞争问题 :虽然可重入锁支持公平锁和非公平锁,但在某些情况下,非公平锁可能会导致某些线程频繁地获取锁,而其他线程长时间等待,不够公平。

非可重入锁

概念

非可重入锁是一种简单的锁机制,它不记录锁的持有者,也不支持嵌套获取锁。

优点

简单性 :非可重入锁的实现相对简单,不需要记录锁的持有者或嵌套锁的层级。

缺点
  1. 缺乏嵌套支持 :非可重入锁无法支持同一个线程多次获取同一个锁。 这在某些情况下可能会导致不便,特别是在复杂的递归算法或代码结构中。

  2. 不支持嵌套 :由于不支持嵌套获取锁,可能会出现死锁的情况,因为一个线程在获取锁的同时阻塞了其他线程,而其他线程也在等待同一个锁。

  3. 性能问题 :非可重入锁通常会有一些性能问题,因为在每次获取锁时都需要检查是否是当前线程。

10.独享锁、共享锁

独享锁(Exclusive Lock):

  • 概念:独享锁是一种锁模式,只允许一个线程或进程同时访问被锁定的资源。其他线程或进程必须等待锁的释放才能访问该资源。

  • 优点

    • 保证了对资源的独占性,避免了并发访问导致的数据不一致问题。

    • 简单,易于实现。

  • 缺点

    • 性能较差,因为只有一个线程能够访问资源,其他线程必须等待。

    • 容易导致死锁,特别是在复杂的多线程应用中。

共享锁(Shared Lock):

  • 概念:共享锁是一种锁模式,允许多个线程或进程同时访问被锁定的资源,前提是它们都要求共享锁。当资源上没有独享锁时,多个线程可以同时访问。

  • 优点

    • 提高了并发性,允许多个线程并发读取共享资源,提高了系统性能。

    • 减少了死锁的风险,因为共享锁通常不会导致死锁。

  • 缺点

    • 可能导致写操作的竞争,需要额外的机制来保证写操作的一致性。

    • 实现更复杂,需要维护多个线程对锁的状态。

共享锁和独享锁在不同的场景下有不同的应用。例如,数据库管理系统中,读取操作可以使用共享锁以允许多个查询同时进行,而写入操作通常需要独享锁以确保数据的一致性。

代码演示

独享锁和共享锁

import java.util.concurrent.locks.ReentrantReadWriteLock;
​
public class SharedExclusiveLockExample {
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
​
    public void sharedResourceAccess() {
        // 获取共享锁
        lock.readLock().lock();
        try {
            // 读取共享资源的操作
        } finally {
            lock.readLock().unlock();
        }
    }
​
    public void exclusiveResourceAccess() {
        // 获取独享锁
        lock.writeLock().lock();
        try {
            // 修改或写入共享资源的操作
        } finally {
            lock.writeLock().unlock();
        }
    }
​
    public static void main(String[] args) {
        SharedExclusiveLockExample example = new SharedExclusiveLockExample();
​
        // 多个线程可以同时访问共享锁
        Thread sharedThread1 = new Thread(() -> {
            example.sharedResourceAccess();
        });
        Thread sharedThread2 = new Thread(() -> {
            example.sharedResourceAccess();
        });
​
        // 独享锁的情况下,只能有一个线程访问
        Thread exclusiveThread1 = new Thread(() -> {
            example.exclusiveResourceAccess();
        });
        Thread exclusiveThread2 = new Thread(() -> {
            example.exclusiveResourceAccess();
        });
​
        sharedThread1.start();
        sharedThread2.start();
        exclusiveThread1.start();
        exclusiveThread2.start();
    }
}
//在上面的示例中,ReentrantReadWriteLock 用于实现读写锁,readLock() 方法返回一个共享锁,writeLock() 方法返回一个独享锁。sharedResourceAccess() 方法演示了如何使用共享锁,多个线程可以同时访问共享锁保护的资源。exclusiveResourceAccess() 方法演示了如何使用独享锁,只有一个线程能够访问独享锁保护的资源。

公平锁和非公平锁

// 创建一个公平锁
        Lock fairLock = new ReentrantLock(true);
        // 创建一个非公平锁(默认)
        Lock unfairLock = new ReentrantLock();
/*
创建一个ReentrantLock()不带入true时就是默认创建非公平锁
*/

可重入锁和非可重入锁

//可重入锁示例
import java.util.concurrent.locks.ReentrantLock;
​
public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
​
    public void performTask() {
        lock.lock(); // 获取锁
        try {
            System.out.println("Performing task");
            // 嵌套调用,可重入
            nestedTask();
        } finally {
            lock.unlock(); // 释放锁
        }
    }
​
    public void nestedTask() {
        lock.lock(); // 获取锁
        try {
            System.out.println("Nested task");
        } finally {
            lock.unlock(); // 释放锁
        }
    }
​
    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();
        example.performTask();
    }
}
//上述代码中,ReentrantLock 是可重入锁的一种实现。在 performTask() 方法中,我们首先获取锁,然后调用 nestedTask() 方法,它也获取了同一个锁,这是可重入的示例。
​
//非可重入锁示例:
public class NonReentrantLock {
    private boolean isLocked = false;
​
    public synchronized void lock() {
        while (isLocked) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        isLocked = true;
    }
​
    public synchronized void unlock() {
        isLocked = false;
        notify();
    }
​
    public static void main(String[] args) {
        NonReentrantLock lock = new NonReentrantLock();
        lock.lock();
        System.out.println("Locked");
​
        // 尝试嵌套获取锁,会导致死锁
        Thread nestedLockAttempt = new Thread(() -> {
            lock.lock();
            System.out.println("Nested lock attempt");
        });
​
        nestedLockAttempt.start();
    }
}
//上述代码中,NonReentrantLock 是一个自定义的非可重入锁。在 main 方法中,我们首先获取锁,然后尝试在嵌套中再次获取锁,这将导致死锁。

总结

  1. 首先我们了解到了什么是锁、锁的作用、实现方式是什么?

  2. 锁、对象、类之间的关系

  3. 锁在对象中和在类中是如何存储的

  4. 锁在对象中存储需要了解的知识

    1. 对象的组成

    2. 对象头的结构

    3. Monitor、MarkWord的概念和变化流程

    4. 补充了32、64位虚拟机对于对象结构的影响和区别

  5. Sync锁的四个等级---无锁、偏向锁、轻量级锁、重量级锁

  6. ReentrantLock的六个锁

    1. 公平锁和非公平锁

    2. 可重入锁和非可重入锁

    3. 独享锁和共享锁

参考文章

java中的各种锁_java锁分类_xyzko1的博客-CSDN博客

Java对象头&&Monitor概念及工作原理_java 对象头 monitor_m0_63463465的博客-CSDN博客

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值