目录
前言
本期将介绍Java内置锁,了解synchromized关键字的核心原理以及介绍一下线程间通信。
Java内置锁的核心原理
Java中每个对象都可以用作锁,这些锁称为内置锁,每个 Java 对象在内存中除了存储自身数据外,还隐含包含一个 “锁标记”(由 JVM 管理)。当多个线程需要访问同一个对象的共享状态(例如对象的成员变量)时,可以通过这个锁来协调线程的执行顺序,避免数据竞争。Java内置锁是一个互斥锁,这就意味着最多只有一个线程能够获得该锁。
synchronized关键字
synchronized关键字的底层实现就是基于对象的内置锁实现的,接下来,我们详细介绍一下,
在Java中,线程同步使用最多的方法是使用synchronized关键字,使用synchronized(syncObject)调用相当于获取syncObject的内置锁,所以可以使用内置锁对临界区代码段进行排他性保护。临界区代码段指的是访问临界资源的代码,临界资源指的是任何时刻只能有一个线程访问的资源。
synchronized同步方法
synchronized关键字是Java的保留字,当使用synchronized关键字修饰一个方法的时候,该方法被声明为同步方法,关键字synchronized的位置处于同步方法的返回类型之前。具体的例子如下:
//同步方法
public synchronized void selfPlus()
{
amount++;
}
synchronized同步块
对于小的临界区,我们直接在方法声明中设置synchronized同步关键字,可以避免竞态条件的问题。但是对于较大的临界区代码段,为了执行效率,最好将同步方法分为小的临界区代码段。通过下面这个例子来具体讲述:
public class TwoPlus {
private int sum1 = 0;
private int sum2 = 0;
//同步方法
public synchronized void plus(int val1, int val2){
//临界区代码段
this.sum1 += val1;
this.sum2 += val2;
}
}
在以上代码中,临界区代码段包含对两个临界区资源的操作,使用synchronized对plus(int val1,int val2)进行同步保护之后,进入临界区代码段的线程拥有sum1和sum2的操作权,并且是全部占用。一旦线程进入,当线程在操作sum1而没有操作sum2时,也将sum2的操作权白白占用。
为了提升吞吐量,可以将synchronized关键字放在函数体内,同步一个代码块。synchronized同步块的写法是:
synchronized(syncObject) //同步块而不是方法
{
//临界区代码段的代码块
}
由于每一个Java对象都有一把监视锁,因此任何Java对象都能作为synchronized的同步锁。使用synchronized同步块对上面的TwoPlus类进行吞吐量的提升改造,具体的代码如下:
public class TwoPlus{
private int sum1 = 0;
private int sum2 = 0;
private Integer sum1Lock = new Integer(1); // 同步锁一
private Integer sum2Lock = new Integer(2); // 同步锁二
public void plus(int val1, int val2){
//同步块1
synchronized(this.sum1Lock){
this.sum1 += val1;
}
//同步块2
synchronized(this.sum2Lock){
this.sum2 += val2;
}
}
}
synchronized方法和synchronized同步块有什么区别呢?
总体来说,synchronized方法是一种粗粒度的并发控制,某一时刻只能有一个线程执行该synchronized方法;而synchronized代码块是一种细粒度的并发控制,处于synchronized块之外的其他代码是可以被多个线程并发访问的。
静态的同步方法
在Java世界里一切皆对象。Java有两种对象:Object实例对象和Class对象。每个类运行时的类型信息用Class对象表示,它包含与类名称、继承关系、字段、方法有关的信息。JVM将一个类加载入自己的方法区内存时,会为其创建一个Class对象,对于一个类来说其Class对象是唯一的。Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机调用类加载器中的defineClass方法自动构造的,因此不能显式地声明一个Class对象。所有的类都是在第一次使用时被动态加载到JVM中的(懒加载),其各个类都是在必需时才加载的。这一点与许多传统语言(如C++)都不同,JVM为动态加载机制配套了一个判定一个类是否已经被加载的检查动作,使得类加载器首先检查这个类的Class对象是否已经被加载。如果尚未加载,类加载器就会根据类的全限定名查找.class文件,验证后加载到JVM的方法区内存,并构造其对应的Class对象。
**使用synchronized关键字修饰static方法时,synchronized的同步锁并不是普通Object对象的监视锁,而是类所对应的Class对象的监视锁。**为了以示区分,这里将Object对象的监视锁叫作对象锁,将Class对象的监视锁叫作类锁。
package com.crazymakercircle.plus;
// 省略import
public class SafeStaticMethodPlus
{ //静态的临界区资源
private static Integer amount = 0;
//使用synchronized关键字修饰 static方法
public static synchronized void selfPlus()
{
amount++;
}
}
通过synchronized关键字所抢占的同步锁什么时候释放呢?一种场景是synchronized块(代码块或者方法)正确执行完毕,监视锁自动释放;另一种场景是程序出现异常,非正常退出synchronized块,监视锁也会自动释放。所以,使用synchronized块时不必担心监视锁的释放问题。
Java对象结构与内置锁
Java对象(Object实例)结构包括三部分:对象头、对象体和对齐字节。

Mark Word的结构信息
这里我们主要介绍Mark Word标记字,Mark Word(标记字)字段主要用来表示对象的线程锁状态,另外还可以用来配合GC存放该对象的hashCode。
Java内置锁涉及很多重要信息,这些都存放在对象结构中,并且存放于对象头的Mark Word字段中。ava内置锁的状态总共有4种,级别由低到高依次为:无锁、偏向锁、轻量级锁和重量级锁。
我们看一下不同锁状态下的Mark Word字段结构,JVM将Mark Word最低两个位设置为Java内置锁状态位。我们以32为JVM举例:

无锁、偏向锁、轻量级锁和重量级锁
在JDK 1.6版本之前,所有的Java内置锁都是重量级锁。重量级锁会造成CPU在用户态和核心态之间频繁切换,所以代价高、效率低。JDK 1.6版本为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁的实现。内置锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能再降级成偏向锁。这种能升级却不能降级的策略,其目的是提高获得锁和释放锁的效率。
无锁状态
Java对象刚创建时还没有任何线程来竞争,说明该对象处于无锁状态(无线程竞争它),这时偏向锁标识位是0,锁状态是01。

偏向锁状态
偏向锁是指一段同步代码一直被同一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。偏向锁状态的Mark Word会记录内置锁自己偏爱的线程ID,内置锁会将该线程当作自己的熟人,当这个线程要执行该锁关联的同步代码时,不需要再做任何检查和切换。偏向锁在竞争不激烈的情况下效率非常高。

至于为什么需要偏向锁,在实际场景中,如果一个同步块(或方法)没有多个线程竞争,而且总是由同一个线程多次重入获取锁,如果每次还要去通过CAS操作去判断锁是否被占用,甚至可能因误判竞争而升级为重量级锁(导致线程阻塞),那么对于CPU是一种资源的浪费,为了解决这类问题,就引入了偏向锁的概念。
轻量级锁状态
当有两个线程开始竞争这个锁对象时,情况就发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象,锁对象的Mark Word就指向哪个线程的栈帧中的锁记录。

当偏向锁被其他线程尝试抢占时,会升级为轻量级锁。此时,抢占线程会通过自旋方式尝试获取锁,而非直接阻塞,从而提升性能。自旋机制的核心在于:如果锁持有者能在短时间内释放锁,等待线程就无需进行内核态与用户态之间的切换来进入阻塞状态,只需短暂等待(自旋)即可在锁释放后立即获取,避免了线程切换的开销。
然而,自旋会消耗CPU资源。为避免线程因长时间自旋而浪费CPU,需要设置自旋的最大等待时间。自JDK 1.6起,JVM引入了适应性自旋锁机制,其自旋时间不再固定,而是根据同一锁上的前次自旋时间及锁持有者的状态动态调整:若自旋成功,则增加下次自旋次数;若失败,则减少自旋次数。
当锁持有者的执行时间超过最大自旋等待时间仍未释放锁时,争用线程将停止自旋并进入阻塞状态,此时锁将膨胀为重量级锁。
重量级锁状态
重量级锁会让其他申请的线程之间进入阻塞,性能降低。重量级锁也叫同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,该监视器对象用集合的形式来登记和管理排队的线程。

对比
总结一下synchronized的执行过程,大致如下:
- 当线程尝试获取锁时,JVM首先检查锁对象Mark Word中的偏向锁标识(biased_lock)是否为1,以及锁标志位(lock)是否为01。若两者均满足,则确认该锁对象处于可偏向状态。
- 确定锁对象可偏向后,JVM进一步检查Mark Word中的线程ID是否与当前抢锁线程ID一致。若匹配,则表明该线程已持有偏向锁,可直接进入临界区执行代码。
- 若Mark Word中的线程ID与抢锁线程不匹配,JVM将通过CAS操作竞争锁。若竞争成功,则将Mark Word中的线程ID更新为当前线程,并将偏向标志位设为1、锁标志位设为01,此时锁对象进入偏向锁状态,线程可进入临界区。
- 若CAS竞争失败,表明存在锁竞争,JVM将撤销偏向锁并将其升级为轻量级锁。
- JVM尝试使用CAS将锁对象的Mark Word替换为抢锁线程的锁记录指针。若替换成功,线程获得锁;若失败,则表明存在其他线程竞争,JVM将进行CAS自旋尝试。若自旋成功,锁对象仍保持轻量级锁状态。
- 若CAS自旋失败,轻量级锁将膨胀为重量级锁,后续等待锁的线程将进入阻塞状态。
总结来说,偏向锁适用于无竞争场景;当出现第二个竞争线程时,偏向锁将升级为轻量级锁;若竞争加剧,轻量级锁在CAS自旋达到阈值后将进一步升级为重量级锁。

线程间通信
线程的通信可以被定义为:当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以避免无效的资源争夺。线程间通信的方式可以有很多种:等待-通知、共享内存、管道流。每种方式用不同的方法来实现,这里首先介绍等待-通知的通信方式。
Java语言中“等待-通知”方式的线程间通信使用对象的wait()、notify()两类方法来实现。每个Java对象都有wait()、notify()两类实例方法,并且wait()、notify()方法和对象的监视器是紧密相关的。
监视器
我们需要介绍一下监视器,在 Java 中,监视器是JVM 实现多线程同步和协作的核心底层机制。每个 Java 对象在创建时,JVM 会自动为其关联一个唯一的监视器,用于管理线程对该对象关联资源的访问。监视器的核心由以下三个组件构成,分别管理不同状态的线程:
- Owner:记录当前持有监视器锁的线程 ID。若为 null,表示监视器未被任何线程占用。
- EntryList:存储所有等待获取监视器锁的线程(状态为 BLOCKED)。这些线程因无法立即获取锁(锁被其他线程占用)而进入此队列,等待锁释放后重新竞争。
- WaitSet:存储调用过 wait() 方法的线程。这些线程因等待某个条件而主动释放锁,并进入此队列,直到其他线程调用 notify() 或 notifyAll() 唤醒它们。
对象的wait()方法
对象的wait()方法的主要作用是让当前线程阻塞并等待被唤醒。
synchronized(locko)
{
//同步保护的代码块
locko.wait();
...
}
Object类中的wait()方法有三个版本:
(1)void wait()
这是一个基础版本,当前线程调用了同步对象locko的wait()实例方法后,将导致当前的线程等待,当前线程进入locko的监视器WaitSet,等待被其他线程唤醒。。
(2)void wait(long timeout)
这是一个限时等待版本,当前的线程等待,等待被其他线程唤醒,或者指定的时间timeout用完,线程不再等待。
(3)void wait(long timeout,int nanos)
这是一个高精度限时等待版本,其主要作用是更精确地控制等待时间。参数nanos是一个附加的纳秒级别的等待时间,从而实现更加高精度的等待时间控制。
wait()方法的核心原理:
(1)当线程调用了locko(某个同步锁对象)的wait()方法后,JVM会将当前线程加入locko监视器的WaitSet(等待集),等待被其他线程唤醒。
(2)当前线程会释放locko对象监视器的Owner权利,让其他线程可以抢夺locko对象的监视器。
(3)让当前线程等待,其状态变成WAITING。
对象的notify()方法
对象的notify()方法的主要作用是唤醒在等待的线程。notify()方法与对象监视器紧密相关,调用notify()方法时也需要放在同步块中。notify()方法的调用方法如下:
synchronized(locko)
{
//同步保护的代码块
locko.notify();
...
}
notify()方法有两个版本:
版本一:void notify()notify()方法的主要作用为:locko.notify()调用后,唤醒locko监视器等待集中的第一条等待线程;被唤醒的线程进入EntryList,其状态从WAITING变成BLOCKED。
版本二:void notifyAll()locko.notifyAll()被调用后,唤醒locko监视器等待集中的全部等待线程,所有被唤醒的线程进入EntryList,线程状态从WAITING变成BLOCKED。
notify()方法的核心原理
(1)当线程调用了locko的notify()方法后,JVM会唤醒locko监视器WaitSet中的第一条等待线程。
(2)当线程调用了locko的notifyAll()方法后,JVM会唤醒locko监视器WaitSet中的所有等待线程。
(3)等待线程被唤醒后,会从监视器的WaitSet移动到EntryList,线程具备了排队抢夺监视器Owner权利的资格,其状态从WAITING变成BLOCKED。
(4)EntryList中的线程抢夺到监视器的Owner权利之后,线程的状态从BLOCKED变成Runnable,具备重新执行的资格。
总结
本期介绍了Java内置锁和synchronized的核心底层原理,下期将介绍CAS原理与JUC原子类。
写在文末
有疑问的友友,欢迎在评论区交流,笔者看到会及时回复。
请大家一定一定要关注!!!
请大家一定一定要关注!!!
请大家一定一定要关注!!!
友友们,你们的支持是我持续更新的动力~

被折叠的 条评论
为什么被折叠?



