深入理解synchronized关键字| java锁机制

在Java多线程编程中,锁是确保线程安全的重要机制之一。本文将深入介绍Java中的锁机制,包括基本的synchronized用法以及在Java SE 1.6中引入的偏向锁和轻量级锁的优化机制。通过深入理解这些机制,我们能够更好地编写高效、安全的多线程代码。

synchronized的锁信息是存在java对象头, 锁的递进流程可以理解为从偏向锁到轻量级锁,再到自旋锁,最终升级为重量级锁。首先,偏向锁适用于短时间内只有一个线程访问的情况,通过偏向锁提供低开销的锁机制。当有其他线程竞争同一锁时,系统可能升级为轻量级锁,此时线程会通过自旋等待锁的释放,避免阻塞线程带来的性能开销。如果自旋等待失败,系统可能进一步升级为重量级锁,将线程阻塞,确保同一时刻只有一个线程能够访问临界区,以保证数据的一致性。这一递进的锁机制旨在根据实际并发情况提供不同程度的性能和线程安全性的权衡。


synchronized的基本用法:

在Java中,synchronized关键字用于确保多个线程对共享资源的有序访问,避免竞态条件。基本用法如下:

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是synchronized括号里配置的对象。

monitorenter和monitorexit指令是实现这一机制的关键。当线程执行到monitorenter时,它尝试获取对象对应的monitor的所有权;当执行到monitorexit时,释放对象的锁。


Java对象头

在Java中,对象头通常包含两个主要部分:Mark Word(标记字段)Class Pointer(类型指针)


Mark Word(标记字段)

Mark Word存储了对象自身的运行时数据,其中包括哈希码、GC分代年龄、锁状态标志等。在JDK 1.8中,Mark Word的位数为64位,结构如下:

64 bits:

+--------------------------------------------------------------+
|   unused:25   | hash:31 | unused:1 | age:4 | biased_lock:1 |
+--------------------------------------------------------------+
  • unused(未使用的位):这是一些未被使用的位。
  • hash(哈希码):用于哈希散列等操作的对象哈希码。
  • age(年龄):表示对象的分代年龄,用于分代垃圾回收。
  • biased_lock(偏向锁标志):用于标识对象是否使用了偏向锁。

Class Pointer(类型指针)

Class Pointer指向对象的类元数据,即对象的类。在64位系统下,class Pointer同样是64位。

这样的对象头设计使得Java能够在运行时高效地管理对象的状态和元信息。Mark Word中的信息对于锁机制(如偏向锁)和垃圾回收都起着重要的作用,而Class Pointer则指明了对象的类型,允许在运行时进行多态操作。


无锁 | 偏向锁 | 轻量级锁 | 重量级锁 状态:

  • 偏向锁状态: 当线程第一次获得锁时,对象进入偏向锁状态。Mark Word记录持有锁的线程ID。在没有竞争的情况下,持有锁的线程再次进入同步块时,无需竞争锁,直接获取。

  • 轻量级锁状态: 当多个线程竞争一个锁时,偏向锁升级为轻量级锁。通过CAS操作竞争锁,减少线程阻塞的可能性,提高性能。

  • 重量级锁状态: 当轻量级锁竞争激烈时,锁升级为重量级锁。这是因为轻量级锁的竞争通常发生在锁的所有者已经解锁的情况下,多个线程争夺同一把锁。在重量级锁状态下,Mark Word将存储指向互斥量(Mutex)的指针,该互斥量由操作系统实现。这种情况下,线程会进入阻塞状态,操作系统会负责唤醒被阻塞的线程。重量级锁确保了线程之间的同步和协调,但由于引入了阻塞,性能相对较低。在锁的争用情况下,这种状态可以防止线程的忙等,减少资源浪费,但也会引入较大的上下文切换开销。因此,在设计并发系统时,需要权衡锁的性能和资源利用率。 


Java对象头在不同锁状态的变化:

  • 无锁状态: 刚创建的对象,Mark Word处于无锁状态,biased_lock 为 0.

  • 偏向锁状态: 当某个线程获取到锁时,对象进入偏向锁状态。 biased_lock 变为 1,记录拥有锁的线程 ID。当其他线程尝试获取锁时,发现有线程已经拥有偏向锁,会检查线程 ID 是否与自己相同,如果相同就直接获得锁.

  • 轻量级锁状态: 当有多个线程竞争锁时,对象进入轻量级锁状态。 Mark Word 存储指向锁记录(Lock Record)的指针,该记录包含了指向线程栈中锁记录的指针.

  • 重量级锁状态: 当轻量级锁竞争激烈,锁膨胀为重量级锁。 Mark Word 存储指向互斥量(Mutex)的指针,该互斥量由操作系统实现.


偏向锁

在多线程编程中,锁的性能一直是一个备受关注的问题。HotSpot JVM引入了偏向锁机制,旨在优化那些被同一线程多次获取的锁的情况。接下来将深入探讨偏向锁的引入、工作原理以及撤销机制,以帮助读者更好地理解这一优化策略.


偏向锁的引入

大多数情况下,锁并不会面临激烈的多线程竞争,而是由同一线程多次获得。为了降低线程获得锁的代价,HotSpot引入了偏向锁。


偏向锁的工作原理

当一个线程第一次获得锁时,会在对象头和栈帧中的锁记录中存储锁偏向的线程ID。以后,该线程再次进入同步块时,无需进行CAS操作,只需测试对象头的Mark Word是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已获得锁;否则,需要再测试Mark Word中偏向锁的标识。


偏向锁的撤销机制:

偏向锁采用一种等到竞争出现才释放锁的机制。当其他线程尝试竞争偏向锁时,拥有偏向锁的线程才会释放锁。撤销偏向锁需要等到全局安全点,即没有正在执行的字节码。具体过程包括:

  1. 暂停拥有偏向锁的线程。

  2. 检查持有偏向锁的线程是否活着。

  3. 如果线程不活动,将对象头设置成无锁状态。

  4. 如果线程仍然活动,执行偏向对象的锁记录,要么重新偏向于其他线程,要么恢复到无锁状态,标记对象不适合作为偏向锁。

  5. 最后唤醒暂停的线程。


使用和关闭偏向锁:

偏向锁在Java 6和Java 7中默认启用,但在应用程序启动几秒钟后才激活。可以使用JVM参数来调整偏向锁的行为,如关闭延迟或完全禁用。

  • 关闭延迟:-XX:BiasedLockingStartupDelay=0

  • 完全禁用偏向锁:-XX:-UseBiasedLocking=false


偏向锁的优缺点

优点

降低锁操作的开销: 在无竞争的情况下,偏向锁能够减少获取和释放锁的操作开销,因为只有在第一次获取锁时才涉及到CAS(比较并交换)操作。

缺点

线程间切换带来的开销: 当发生竞争时,需要撤销偏向锁,这时会涉及到线程间的竞争和切换,可能导致性能下降。


轻量级锁加锁

在Java虚拟机中,轻量级锁是一种用于提高多线程并发性能的机制。在一个线程尝试获取锁之前,JVM会为其在栈帧中创建一个专门的空间,用来存储锁记录。这个过程包括将对象头中的标记字(Mark Word)复制到锁记录中,这个过程被官方称为“Displaced Mark Word”。

接下来,线程试图使用CAS(Compare And Swap)操作,将对象头中的标记字替换为指向锁记录的指针。如果CAS操作成功,当前线程就成功获取了锁;如果失败,表示其他线程也在竞争这个锁,当前线程会尝试使用自旋来等待获取锁的机会。

简而言之,轻量级锁的流程可以概括为以下几个步骤:

  1. 为锁创建锁记录空间: 在当前线程的栈帧中为锁创建一个专门的存储空间,用于存储锁记录。

  2. 复制标记字到锁记录: 将对象头中的标记字复制到刚创建的锁记录中,形成“Displaced Mark Word”。

  3. 使用CAS替换标记字: 尝试使用CAS操作,将对象头中的标记字替换为指向锁记录的指针。

  4. 获取锁或自旋等待: 如果CAS操作成功,当前线程成功获取了锁;如果失败,表示其他线程也在竞争锁,当前线程会尝试使用自旋等待获取锁的机会。

轻量级锁通过减少锁竞争的影响,提高了多线程程序的并发性能,是Java中处理并发问题的重要机制之一。


轻量级锁解锁

在轻量级锁的解锁过程中,采用了原子的CAS操作来将之前复制到锁记录的“Displaced Mark Word”重新替换回对象头。这一步是为了检查在解锁时是否有竞争发生。

简而言之,轻量级解锁的过程如下:

  1. CAS操作替换标记字: 在轻量级解锁时,使用原子的CAS操作将之前复制到锁记录的“Displaced Mark Word”重新替换回对象头。这个过程是原子的,确保了在多线程环境下的线程安全性。

  2. 判断CAS操作结果: 如果CAS操作成功,表示在解锁时没有其他线程在竞争这个锁,当前线程成功释放了锁。如果CAS操作失败,说明有其他线程在竞争锁,这时锁就会膨胀成重量级锁。

这个机制的设计旨在降低解锁的开销,只有在真正需要时才将锁膨胀成重量级锁。这样,当锁的竞争不激烈时,能够更高效地处理锁的获取和释放操作,提升整体的并发性能。理解这一过程对于深入掌握Java并发编程机制非常重要。


轻量级锁的优缺点

优点

减小阻塞时间: 在低竞争情况下,轻量级锁通过自旋等待避免了线程阻塞的开销,从而减小了锁的竞争和释放的开销。

缺点

自旋带来的性能开销: 自旋的过程可能会导致一定的性能开销,特别是在高竞争的情况下,线程可能长时间自旋而无法获取锁。


轻量级锁自旋: 自旋锁

自旋锁在Java中的主要目的是避免线程阻塞,因为线程阻塞和唤醒涉及到操作系统的内核态和用户态的切换,开销较大。自旋锁允许线程在一定的时间内自旋等待,期望其他线程会在短时间内释放锁,避免阻塞。如果在自旋等待的时间内锁被释放,那么线程就可以避免进入阻塞状态。

具体自旋等待的时间是由JVM自动调整的,通常包括以下几个阶段:

  1. 初始自旋: 线程尝试获取锁时,会进行一定次数的自旋。如果在这个初始自旋的阶段锁被释放,线程就可以顺利进入临界区。

  2. 自适应自旋: 如果初始自旋失败,JVM 可能会根据之前获取锁的历史来调整自旋等待的次数。如果发现该线程在过去成功地进行了自旋等待,可能会增加自旋等待的次数,反之可能减少。

  3. 自旋次数达到上限: 如果自旋次数达到了一定的上限,但仍未获得锁,线程可能会将自旋锁升级为重量级锁,即进入阻塞状态。

在JDK 1.8中,可以使用以下JVM参数来控制自旋锁的行为:

  1. -XX:PreBlockSpin: 这个参数用于设置线程在进行自旋之前,预先执行的自旋次数。这个值的默认是10。如果自旋次数超过这个预设值,线程可能会进入阻塞状态。

  2. -XX:AbortablePrecleanSpin: 用于设置JVM在放弃对锁的争夺之前,所允许的预清理自旋次数。默认值是5。

  3. -XX:ThreadPriorityPolicy: 用于设置线程的优先级策略。在一些JVM实现中,线程优先级可能会影响自旋锁的行为。


重量级锁加锁

  1. 尝试获取锁: 当一个线程尝试获取一把重量级锁时,首先会尝试通过CAS(Compare and Swap)操作来竞争锁。如果成功,线程直接获得锁;如果失败,会自旋获取锁, 自旋获取失败, 膨胀成重量级别锁, 进入下一步.

  2. 创建Monitor对象: JVM 会为这个锁创建一个Monitor对象,该对象会与被锁定的对象关联。Monitor对象内部包含了等待队列和持有锁的线程信息。

  3. 进入阻塞状态: 获取锁失败的线程会被阻塞,并进入Monitor对象的等待队列。线程状态变为BLOCKED。

  4. 争夺Monitor的所有权: 线程在等待队列中等待,等待其他线程释放锁。一旦Monitor的所有权被释放,等待队列中的线程会竞争锁的拥有权。

  5. 获得锁: 竞争成功的线程将获得Monitor的所有权,其状态变为RUNNABLE,可以继续执行。


重量级锁解锁

  1. 释放锁: 当线程执行完临界区代码后,要释放锁。释放锁的过程涉及到将对象的锁状态复原为初始状态,以允许其他线程获得锁。

  2. 对象头的标记: Java对象的头部包含了一些用于同步的信息,其中之一是锁标记。在重量级锁的情况下,通常使用的是互斥量来表示锁的状态。对象头中的锁标记会被置为初始状态,表示这个锁已经被释放。

  3. 唤醒等待线程: 如果有其他线程因为争夺这个锁而被阻塞,那么在锁释放后,会唤醒其中一个或多个等待线程,使其重新竞争锁。唤醒的过程通常涉及到操作系统的线程调度机制。


重量级锁的优缺点

优点

适用于高竞争: 在高度竞争的场景中,重量级锁通过阻塞线程来确保同一时刻只有一个线程能够执行临界区内的代码,从而保证数据的一致性。

缺点

阻塞带来的性能开销: 阻塞线程会带来较大的性能开销,因为涉及到线程的切换和上下文切换。

你可能感兴趣的:(并发编程学习指南,java基础,java,开发语言)