synchronized()_synchronized底层原理 深入理解 面试

本文深入讲解Java中的synchronized锁机制,覆盖多线程三大特性、Java内存模型、synchronized的特性与原理等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

synchronized是java面试中的高频面试点,小秃在B站中看过一位黑马老师讲的免费课,思路清晰平易近人,再加上自己平时学习中收集的资料,吐血整理.

针对java基础薄弱的同学,对其中拓展涉到的概念进行了补充

希望有缘读到这篇文章的同学能够有所收获.

码字不易,有写的不好的地方希望大佬们不吝赐教.在这里跪谢了.

在深入学习synchronized之前,要首先了解一系列基本概念

多线程的三大特性.

  • 可见性(Visibility): 是指一个线程对共享变量进行修改,另一个先立即得到修改后的最新值。
  • 原子性(Atomicity): 在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。
  • 有序性(Ordering): 是指程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。

之后还要了解java的内存模型

java内存模型(JMM)

在介绍Java内存模型之前,先来看一下到底什么是计算机内存模型。

计算机内存模型

这里只做概念性的了解,即 计算机的主要组成CPU,内存,输入设备,输出设备。

着重介绍下计算机缓存:

306dffefa6b6fa91b63f159d06196240.png

正式介绍JMM

JMM即Java Memory Molde,千万不要和Java内存结构混淆

Java内存模型,是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别

Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,具体如下。

  • 主内存

主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。

  • 工作内存

每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操

作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接

访问对方工作内存中的变量。

96742c966cc6d6f11fcaa075273b809f.png

Java内存模型的作用:

Java内存模型是一套在多线程读写共享数据时,对共享数据的可见性、有序性、和原子性的规则和保障。

CPU缓存,内存与Java内存模型的关系

通过对前面的CPU硬件内存架构、Java内存模型以及Java多线程的实现原理的了解,我们应该已经意识到,多线程的执行最终都会映射到硬件处理器上进行执行。但Java内存模型和硬件内存架构并不完全一致。

对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存和主内存之分,也就是说Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。

主内存与工作内存之间的交互

6682434a458dd9bc6072eca2b8e55f7a.png

Java内存模型中定义了以下8种操作来完成,主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。

9c4d07ce8cdbd96bd5795d808ed682c2.png

PS:

1. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值

2. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中

总结

主内存与工作内存之间的数据交互过程

lock -> read -> load -> use -> assign -> store -> write -> unlock

synchronized保证三大特性的具体原理

有了前面的基础概念做铺垫就可以开始正式的原理学习了

synchronized能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。

synchronized (锁对象) {

// 受保护资源;

}

  • 1 使用synchronized保证原子性
import java.util.ArrayList;/** * 案例演示:5个线程各执行1000次 i++; */public class Test01Atomicity {    private static int number = 0;    public static void main(String[] args) throws InterruptedException {        Runnable increment = new Runnable() {            @Override            public void run() {                for (int i = 0; i < 1000; i++) {                    synchronized (Test01Atomicity.class) {                        number++;                    }                }            }        };        ArrayList ts = new ArrayList<>();        for (int i = 0; i < 50; i++) {            Thread t = new Thread(increment);            t.start();            ts.add(t);        }        for (Thread t : ts) {            t.join();        }        System.out.println("number = " + number);    }}
f23def50efff6b071a52088b13993062.png

synchronized保证原子性的原理

对number++;增加同步代码块后,保证同一时间只有一个线程操作number++;。就不会出现安全问题。

总结

synchronized保证原子性的原理,synchronized保证只有一个线程拿到锁,能够进入同步代码块。

  • 2 使用synchronized保证可见性

一个线程根据boolean类型的标记flag, while循环,另一个线程改变这个flag变量的值,另一个线程并不会停止循环。

/** * 案例演示: 一个线程根据boolean类型的标记flag, while循环, *另一个线程改变这个flag变量的值, 另一个线程并不会停止循环. */public class Test01Visibility {    // 多个线程都会访问的数据,我们称为线程的共享数据     private static boolean run = true;    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(() -> {            while (run) { // 增加对象共享数据的打印,println是同步方法                 System.out.println("run = " + run);            }        });        t1.start();        Thread.sleep(1000);        Thread t2 = new Thread(() -> {            run = false;            System.out.println("时间到,线程2设置为false");        });        t2.start();    }}

synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变量的值

  • 3 使用synchronized保证有序性

先了解一些前置概念

为了提高程序的执行效率,编译器和CPU会对程序中代码进行重排序。

as-if-serial语义

as-if-serial语义的意思是:不管编译器和CPU如何重排序,必须保证在单线程情况下程序的结果是正确

的。

以下数据有依赖关系,不能重排序。

写后读:

写后读:

写后写:

int a = 1;int a = 2;

读后写:

int a = 1; int b = a; int a = 2;

编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如

果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

int a = 1; int b = 2; int c = a + b;

a和c之间存在数据依赖关系,同时b和c之间也存在数据依赖关系。因此在最终执行的指令序列中,c不能被重排序到a和b的前面。但a和b之间没有数据依赖关系,编译器和处理器可以重排序a和b之间的执行顺序。

可以这样:int a = 1; int b = 2;int c = a + b; 也可以重排序这样: int b = 2;int a = 1; int c = a + b;

synchronized保证有序性的原理

synchronized后,虽然进行了重排序,保证只有一个线程会进入同步代码块,也能保证有序性。

总结

synchronized保证有序性的原理,我们加synchronized后,依然会发生重排序,只不过,我们有同步

代码块,可以保证只有一个线程执行同步代码中的代码。保证有序性

synchronized的特性

1 可重入

一个线程可以多次执行synchronized,重复获取同一把锁。

public class Demo01 {    public static void main(String[] args) {        Runnable sellTicket = new Runnable() {            @Override            public void run() {                synchronized (Demo01.class) {                    System.out.println("我是run");                    test01();                }            }            public void test01() {                synchronized (Demo01.class) {                    System.out.println("我是test01");                }            }        };        new Thread(sellTicket).start();        new Thread(sellTicket).start();    }}

可重入原理

synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁.

可重入的好处

1. 可以避免死锁

2. 可以让我们更好的来封装代码

总结

synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁啦,在执行完同步代码块

时,计数器的数量会-1,知道计数器的数量为0,就释放这个锁。

2 不可中断

一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第

二个线程会一直阻塞或等待,不可被中断

public class Demo02_Uninterruptible {    private static Object obj = new Object();    /* 目标:演示synchronized不可中断        1.定义一个Runnable        2.在Runnable定义同步代码块        3.先开启一个线程来执行同步代码块,保证不退出同步代码块        4.后开启一个线程来执行同步代码块(阻塞状态)        5.停止第二个线程    */    public static void main(String[] args) throws InterruptedException {        // 1.定义一个Runnable        Runnable run = () -> {            // 2.在Runnable定义同步代码块            synchronized (obj) {                String name = Thread.currentThread().getName();                System.out.println(name + "进入同步代码块");                // 保证不退出同步代码块                try {                    Thread.sleep(888888);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        };        // 3.先开启一个线程来执行同步代码块        Thread t1 = new Thread(run,"t1线程");        t1.start();        Thread.sleep(1000);        // 4.后开启一个线程来执行同步代码块(阻塞状态)        Thread t2 = new Thread(run,"t2线程");        t2.start();        // 5.停止第二个线程        System.out.println("停止线程前");        t2.interrupt();        System.out.println("停止线程后");        System.out.println(t1.getState());        System.out.println(t2.getState());    }}
3f6316cf3b2db3a3dae98b2708671af8.png

不得不提到,也是面试中会问到的

ReentrantLock可中断

/* 目标:演示Lock不可中断和可中断 */public class Demo03_Interruptible {    private static Lock lock = new ReentrantLock();    public static void main(String[] args) throws InterruptedException {        // test01();        test02();    }    // 演示Lock可中断    public static void test02() throws InterruptedException {        Runnable run = () -> {            String name = Thread.currentThread().getName();            boolean b = false;            try {                b = lock.tryLock(3, TimeUnit.SECONDS);                if (b) {                    System.out.println(name + "获得锁,进入锁执行");                    System.out.println(name + "t1休眠五秒");                    Thread.sleep(5000);                } else {                    System.out.println(name + "在指定时间没有得到锁做其他操作");                }            } catch (InterruptedException e) {                e.printStackTrace();            } finally {                if (b) {                    lock.unlock();                    System.out.println(name + "释放锁");                }            }        };        Thread t1 = new Thread(run,"t1线程");        t1.start();        Thread.sleep(1000);        Thread t2 = new Thread(run,"t2线程");        t2.start();        System.out.println("停止t2线程前");        t2.interrupt();        System.out.println("停止t2线程后");        Thread.sleep(1000);        System.out.println(t1.getState());        System.out.println(t2.getState());    }    // 演示Lock不可中断    public static void test01() throws InterruptedException {        Runnable run = () -> {            String name = Thread.currentThread().getName();            try {                lock.lock();                System.out.println(name + "获得锁,进入锁执行");                Thread.sleep(88888);            } catch (InterruptedException e) {                e.printStackTrace();            } finally {                lock.unlock();                System.out.println(name + "释放锁");            }        };        Thread t1 = new Thread(run);        t1.start();        Thread.sleep(1000);        Thread t2 = new Thread(run);        t2.start();        System.out.println("停止t2线程前");        t2.interrupt();        System.out.println("停止t2线程后");        Thread.sleep(1000);        System.out.println(t1.getState());        System.out.println(t2.getState());    }}
00afd98bc9565e3d64b584dd211e9157.png

不可中断是指,当一个线程获得锁后,另一个线程一直处于阻塞或等待状态,前一个线程不释放锁,后一个线程会一直阻塞或等待,不可被中断。

synchronized属于不可被中断

Lock的lock方法是不可中断的

Lock的tryLock方法是可中断的

synchronized原理

现在进入底层原理的学习

前置知识:

javap反汇编 命令 javap -p -v 类名

先上一段代码

我们要看synchronized的原理,但是synchronized是一个关键字,看不到源码。我们可以将class文件

进行反汇编。

JDK自带的一个工具: javap ,对字节码进行反汇编,查看字节码指令

先编译:

c1119822e92e042856b998379731ed06.png

在DOS命令行输入:

javap -p -v Demo01.class
先  public static void main(java.lang.String[]);    descriptor: ([Ljava/lang/String;)V    flags: ACC_PUBLIC, ACC_STATIC    Code:      stack=2, locals=3, args_size=1         0: getstatic     #2                  // Field obj:Ljava/lang/Object;         3: dup         4: astore_1         5: monitorenter         6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;         9: ldc           #4                  // String 1        11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V        14: aload_1        15: monitorexit        16: goto          24        19: astore_2        20: aload_1        21: monitorexit        22: aload_2        23: athrow        24: return      Exception table:         from    to  target type             6    16    19   any            19    22    19   any      LineNumberTable:        line 5: 0        line 6: 6        line 7: 14        line 8: 24      StackMapTable: number_of_entries = 2        frame_type = 255 /* full_frame */          offset_delta = 19          locals = [ class "[Ljava/lang/String;", class java/lang/Object ]          stack = [ class java/lang/Throwable ]        frame_type = 250 /* chop */          offset_delta = 4  public synchronized void test();    descriptor: ()V    flags: ACC_PUBLIC, ACC_SYNCHRONIZED    Code:      stack=2, locals=1, args_size=1         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;         3: ldc           #6                  // String a         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V         8: return      LineNumberTable:        line 11: 0        line 12: 8  static {};    descriptor: ()V    flags: ACC_STATIC    Code:      stack=2, locals=0, args_size=0         0: new           #7                  // class java/lang/Object         3: dup         4: invokespecial #1                  // Method java/lang/Object."":()V         7: putstatic     #2                  // Field obj:Ljava/lang/Object;        10: return      LineNumberTable:        line 2: 0}SourceFile: "Demo01.java"
6974787ea0993c42dc17e6e99c5ad8fb.png
8a2d94aed1784055ffc360763a8767a9.png

monitorenter

首先我们来看一下JVM规范中对于monitorenter的描述:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter

Each object is associated with a monitor. A monitor is locked if and only if it has an owner.

The thread that executes monitorenter attempts to gain ownership of the monitor

associated with objectref, as follows: • If the entry count of the monitor associated with

objectref is zero, the thread enters the monitor and sets its entry count to one. The thread

is then the owner of the monitor. • If the thread already owns the monitor associated with

objectref, it reenters the monitor, incrementing its entry count. • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's

entry count is zero, then tries again to gain ownership.

翻译过来:

每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。 当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:

1. 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为

monitor的owner(所有者)

2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1

3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直

到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

面试题: 当被问到synchronized的原理的时候就可以这么跟面试官介绍了.嘿嘿..(笑容逐渐猥琐...)

还可以加一句:

synchronized代码块是由一对儿monitorenter/monitorexit指令实现的,Monitor对象是同步的基本实现单元。(笑容逐渐变态...)

讲到这大家应该基本能明白个袋盖了,当被问到恶心的底层指令相关的层面时候,我们的思路已经开始有头绪了,再被问到这的时候也不会尴尬了

monitorenter小结:

synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是JVM的线程执行到这个

同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量owner:拥有

这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待

monitorexit

首先我们来看一下JVM规范中对于monitorexit的描述:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit

The thread that executes monitorexit must be the owner of the monitor associated with the

instance referenced by objectref. The thread decrements the entry count of the monitor

associated with objectref. If as a result the value of the entry count is zero, the thread

exits the monitor and is no longer its owner. Other threads that are blocking to enter the

monitor are allowed to attempt to do so

翻译过来:

1. 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。

2. 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出

monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个

monitor的所有权

ps:+1/-1的操作就是由 recursions 负责管理 后面会讲到

monitorexit释放锁。

monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。

面试题synchroznied出现异常会释放锁吗?

会释放锁

同步方法

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10

2d5a5056ca7e0557842c172f0bc4901f.png

可以看到同步方法在反汇编后,会增加 ACC_SYNCHRONIZED 修饰。会隐式调用monitorenter和

monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit。

总结

通过javap反汇编我们看到synchronized使用编程了monitorentor和monitorexit两个指令.每个锁对象

都会关联一个monitor(监视器,它才是真正的锁对象),它内部有两个重要的成员变量owner会保存获得锁

的线程,recursions会保存线程获得锁的次数,当执行到monitorexit时,recursions会-1,当计数器减到0时

这个线程就会释放锁

后面还会更详细的讲到recursions

面试题:synchronized与Lock的区别

1. synchronized是关键字,而Lock是一个接口。

2. synchronized会自动释放锁,而Lock必须手动释放锁。

3. synchronized是不可中断的,Lock可以中断也可以不中断。

4. 通过Lock可以知道线程有没有拿到锁,而synchronized不能。

5. synchronized能锁住方法和代码块,而Lock只能锁住代码块。

6. Lock可以使用读锁提高多线程读效率。

7. synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。

深入JVM源码

前置知识

过synchronized是JVM内部的Intrinsic Lock,所以偏斜锁、轻量级锁、重量级锁的代码实现,并不在核心类库部分,而是在JVM的代码中。不过不用担心后面也会详细讲到.

JVM源码下载

http://openjdk.java.net/ --> Mercurial --> jdk8 --> hotspot --> zip

monitor监视器锁

可以看出无论是synchronized代码块还是synchronized方法,其线程安全的语义实现最终依赖一个叫

monitor的东西,那么这个神秘的东西是什么呢?下面让我们来详细介绍一下。

在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的,位于HotSpot虚

拟机源码ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)。ObjectMonitor主

要数据结构如下:

1. _owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程

释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线

程安全的。

2. _cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资

源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指

向新值(新线程).因此_cxq是一个后进先出的stack(栈)。有些文章中也会说ContentionList.可以先理解为一个意思.

3. _EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中。

4. _WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中。

每一个Java对象都可以与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执

行一段被synchronized圈起来的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象

对应的monitor。

我们的Java代码里不会显示地去创造这么一个monitor对象,我们也无需创建,事实上可以这么理解:

monitor并不是随着对象创建而创建的。

我们是通过synchronized修饰符告诉JVM需要为我们的某个对象创建关联的monitor对象。

每个线程都存在两个ObjectMonitor对象列表,分别为free和used列表。

同时JVM中也维护着global locklist。当线程需要ObjectMonitor对象时,首先从线程自身的free表中申

请,若存在则使用,若不存在则从global list中申请。

ObjectMonitor的数据结构中包含:_owner、_WaitSet和_EntryList,它们之间的关系转换可以用下图

表示:

482307542ab57f57938204561296fe65.png

下面开始学习一些细节.

对Java内置锁实现的掌握,也是并发的经典题目。我们之前学习的基础,涵盖了一些基本概念。如果基础不牢,以下包括后面一些概念理解起来就比较晦涩,秃哥建议还是尽量理解和掌握,即使有不懂的也不用担心,在后续学习中还会逐步加深认识。很多同学看到这里为止觉得可以应付一大部分面试了,但是大厂的面试大佬绝对不会就在这个层面就此打住,只要坚持住学到最后一定会豁然开朗.

cf938a0860fa7bb11e48146ccd44ead8.png

下面会分别学习monitor竞争,等待,释放,以及对mointor为什么是重量级锁的解释


monitor竞争

1. 执行monitorenter时,会调用InterpreterRuntime.cpp

(位于:src/share/vm/interpreter/interpreterRuntime.cpp) 的 InterpreterRuntime::monitorenter函

数。具体代码可参见HotSpot源码。第561行.

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread,                    BasicObjectLock* elem)) #ifdef ASSERT thread->last_frame().interpreter_frame_verify_monitor(elem); #endif if (PrintBiasedLockingStatistics) {   Atomic::inc(BiasedLocking::slow_path_entry_count_addr()); }Handle h_obj(thread, elem->obj()); assert(Universe::heap()->is_in_reserved_or_null(h_obj()),             "must be NULL or an object"); if (UseBiasedLocking) {   // Retry fast entry if bias is revoked to avoid unnecessary inflation   ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK); } else {   ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK); }assert(Universe::heap()->is_in_reserved_or_null(elem->obj()), "must be NULL or an object");

秃哥猜你一定看不懂,尽量理解,看个大概意思,大家都是写java的,我懂~

关键代码

f4e710802e2301a95fd4f263dcfa7aad.png

UseBiasedLocking: 是否使用偏向锁(别急先知道概念后面也会详细讲)

先简单解释一波,要不然看到这一定头大很难受:

偏向锁是JDK 6中的重要引进,因为HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多

线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。

偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对

象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID即可.

秃哥猜你一定又挠头了,啥是对象头???(大佬看到了咧嘴微笑..)

java中锁的使用和对象头紧密相关.由于Java面向对象的思想,在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。

所以,当一个线程尝试访问synchronized修饰的代码块时,它首先要获得锁,那么这个锁到底存在哪里呢?没错,是存在 锁对象 的 对象头 中的。

是不是舒服了...一点

dba7391d4a8b4a7ad89ee71df7c50bbc.png

这是此时电脑前的你吗

(对象头后面也会讲,先简单解释一下解解渴,秃哥以前学到这的时候也是肥肠的难受)

言归正传 如果没设置偏向锁,所进入另一段判断逻辑:

ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK); 

2.对于重量级锁,monitorenter函数中会调用 ObjectSynchronizer::slow_enter (慢进入,知道意思即可)

3.最终调用 ObjectMonitor::enter(位于:src/share/vm/runtime/objectMonitor.cpp),总之又回到ObjectMonitor, 源码如下:(请仔细阅读中文注释,要不然还是很懵...)

void ATTR ObjectMonitor::enter(TRAPS) {   // The following code is ordered to check the most common cases first   // and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.   Thread * const Self = THREAD ; void * cur ;   // 通过CAS操作尝试把monitor的_owner字段设置为当前线程   cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;   if (cur == NULL) {     // Either ASSERT _recursions == 0 or explicitly set _recursions = 0.     assert (_recursions == 0 , "invariant") ;     assert (_owner == Self, "invariant") ;     // CONSIDER: set or assert OwnerIsThread == 1     return ;   }  // 线程重入,recursions++  if (cur == Self) {     // TODO-FIXME: check for integer overflow! BUGID 6557169.     _recursions ++ ; return ; }  // 如果当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程  if (Self->is_lock_owned ((address)cur)) {     assert (_recursions == 0, "internal state error"           ); _recursions = 1 ; // Commute owner from a thread-specific on-stack BasicLockObject address to // a full-fledged "Thread *". _owner = Self ; OwnerIsThread = 1 ; return ; }// 省略一些代码 for (;;) { jt->set_suspend_equivalent(); // cleared by handle_special_suspend_equivalent_condition() // or java_suspend_self() // 如果获取锁失败,则等待锁的释放; EnterI (THREAD) ; if (!ExitSuspendEquivalent(jt)) break ; //// We have acquired the contended monitor, but while we were // waiting another thread suspended us. We don't want to enter // the monitor while suspended because that would surprise the // thread that suspended us. // _recursions = 0 ; _succ = NULL ; exit (false, Self) ; jt->java_suspend_self(); }Self->set_current_pending_monitor(NULL); }

此处省略锁的自旋优化等操作,统一放在后面synchronzied优化中说。(看到自旋又开始挠头...稳住..)

以上代码的具体流程概括如下:

1. 通过CAS尝试把monitor的owner字段设置为当前线程。

2. 如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行

recursions ++ ,记录重入的次数。

3. 如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获

得锁并返回。

4. 如果获取锁失败,则等待锁的释放。


monitor等待

竞争失败等待调用的是ObjectMonitor对象的EnterI方法,上文代码中提到过

94765e153d157cc8287e27731f7d97b4.png

(位于:src/share/vm/runtime/objectMonitor.cpp),源码如下所示:

先解释一下线程 挂起 唤醒

挂起实际上是让线程进入“非可执行”状态下,在这个状态下CPU不会分给线程时间片,进入这个状态可以用来暂停一个线程的运行;在线程挂起后,可以通过重新唤醒线程来使之恢复运行。

挂起的原因可能是如下几种情况:
(1)通过调用sleep()方法使线程进入休眠状态,线程在指定时间内不会运行。
(2)通过调用join()方法使线程挂起,使自己等待另一个线程的结果,直到另一个线程执行完毕为止。
(3)通过调用wait()方法使线程挂起,直到线程得到了notify()和notifyAll()消息,线程才会进入“可执行”状态。
(4)使用suspend挂起线程后,可以通过resume方法唤醒线程。
虽然suspend和resume可以很方便地使线程挂起和唤醒,但由于使用这两个方法可能会造成死锁,因此,这两个方法被标识为deprecated(抗议)标记,这表明在以后的jdk版本中这两个方法可能被删除,所以尽量不要使用这两个方法来操作线程。
调用sleep()、yield()、suspend()的时候并没有被释放锁
调用wait()的时候释放当前对象的锁

wait()方法表示,放弃当前对资源的占有权,一直等到有线程通知,才会运行后面的代码。
notify()方法表示,当前的线程已经放弃对资源的占有,通知等待的线程来获得对资源的占有权,但是只有一个线程能够从wait状态中恢复,然后继续运行wait()后面的语句。
notifyAll()方法表示,当前的线程已经放弃对资源的占有,通知所有的等待线程从wait()方法后的语句开始运行。

void ATTR ObjectMonitor::EnterI (TRAPS) {   Thread * Self = THREAD ;    // Try the lock - TATAS   // 再次尝试获取锁,抢到就执行判断里面的逻辑,没抢到继续往下走  if (TryLock (Self) > 0) {    assert (_succ != Self , "invariant") ;     assert (_owner == Self , "invariant") ;     assert (_Responsible != Self , "invariant") ;     return ;   }    //TrySpin 函数就是一个自旋操作,可以理解为多次循环来尝试 看是否能抢到锁.(嘿嘿...)  //抢到了执行判断里面的逻辑.  //如果还是没抢到,执行后面的逻辑.  if (TrySpin (Self) > 0) {     assert (_owner == Self , "invariant") ;     assert (_succ != Self , "invariant") ;     assert (_Responsible != Self , "invariant") ;    return ;   }    // 省略部分代码     // 当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ;   ObjectWaiter node(Self) ;   Self->_ParkEvent->reset() ;   node._prev = (ObjectWaiter *) 0xBAD ;   node.TState = ObjectWaiter::TS_CXQ ;                  // 通过CAS把node节点push到_cxq列表中  ObjectWaiter * nxt ;                  for (;;) {     node._next = nxt = _cxq ;        //通过cas操作,把竞争失败的线程 加入_cxq竞争队列    if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;         // Interference - the CAS failed because _cxq changed. Just retry.    // As an optional optimization we retry the lock.     //到这里 还是依然在尝试获得锁(疯狂抢救...)    if (TryLock (Self) > 0) {       assert (_succ != Self , "invariant") ;       assert (_owner == Self , "invariant") ;       assert (_Responsible != Self , "invariant") ;       return ;     }   }    // 省略部分代码    for (;;) {         // 当线程被放倒_cxq链表中以后,还要把这些线程挂起    // 线程在被挂起前做一下挣扎,看能不能获取到锁     if (TryLock (Self) > 0) break ;         assert (_owner != Self, "invariant") ;         if ((SyncFlags & 2) && _Responsible == NULL) {       Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ; }        // park self     // park() 就是线程挂起的操作了    // 可以看到 不管走哪个逻辑 最后都会被挂起.    // 挂起后的线程就不会再执行了,只有等待被唤醒后,才能继续执行.    if (_Responsible == Self || (SyncFlags & 1)) {       TEVENT (Inflated enter - park TIMED) ;       Self->_ParkEvent->park ((jlong) RecheckInterval) ;       // Increase the RecheckInterval, but clamp the value.       RecheckInterval *= 8 ;       if (RecheckInterval > 1000)  RecheckInterval = 1000 ;           } else {         TEVENT (Inflated enter - park UNTIMED) ;        // 通过park将当前线程挂起,等待被唤醒         Self->_ParkEvent->park() ;     }        //这里就是 被唤醒后,再次尝试获得锁    if (TryLock(Self) > 0) break ;        // 省略部分代码       }    // 省略部分代码   }

当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁,TryLock方

法实现如下:

int ObjectMonitor::TryLock (Thread * Self) {  for (;;) {     void * own = _owner ; if (own != NULL)       return 0 ;     if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {      // Either guarantee _recursions == 0 or set _recursions = 0.       assert (_recursions == 0, "invariant") ;       assert (_owner == Self, "invariant") ;             // CONSIDER: set or assert that OwnerIsThread == 1 return 1 ; }      // The lock had been free momentarily, but we lost the race to the lock.       // Interference -- the CAS failed.       // We can either return -1 or retry.       // Retry doesn't make as much sense because the lock was just acquired.      if (true) return -1 ;     }   }

以上代码的具体流程概括如下:

1. 当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ。

2. 在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node

节点push到_cxq列表中。

3. node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当

前线程挂起,等待被唤醒。

4. 当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁


monitor释放

当某个持有锁的线程执行完同步代码块时,会进行锁的释放,给其它线程机会执行同步代码,在

HotSpot中,通过退出monitor的方式实现锁的释放,并通知被阻塞的线程,具体实现位于

ObjectMonitor的exit方法中。(位于:src/share/vm/runtime/objectMonitor.cpp),源码如下所

void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {   Thread * Self = THREAD ;    // 省略部分代码     if (_recursions != 0) {     _recursions--;     // this is simple recursive enter    TEVENT (Inflated exit - recursive) ;    return ;   }    // 省略部分代码     ObjectWaiter * w = NULL ;   int QMode = Knob_QMode ;   // qmode = 2:直接绕过EntryList队列,从cxq队列中获取线程用于竞争锁   if (QMode == 2 && _cxq != NULL) {    w = _cxq ; assert (w != NULL, "invariant"                      ) ; assert (w->TState == ObjectWaiter::TS_CXQ, "Invariant") ; ExitEpilog (Self, w) ; return ; }// qmode =3:cxq队列插入EntryList尾部; if (QMode == 3 && _cxq != NULL) { w = _cxq ; for (;;) { assert (w != NULL, "Invariant") ; ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ; if (u == w) break ; w = u ; }assert (w != NULL , "invariant") ; ObjectWaiter * q = NULL ; ObjectWaiter * p ; for (p = w ; p != NULL ; p = p->_next) { guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ; p->TState = ObjectWaiter::TS_ENTER ; p->_prev = q ; q = p ; }ObjectWaiter * Tail ; for (Tail = _EntryList ; Tail != NULL && Tail->_next != NULL ; Tail = Tail->_next) ; if (Tail == NULL) { _EntryList = w ; } else { Tail->_next = w ; w->_prev = Tail ; } }// qmode =4:cxq队列插入到_EntryList头部 if (QMode == 4 && _cxq != NULL) { w = _cxq ; for (;;) { assert (w != NULL, "Invariant") ; ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ; if (u == w) break ; w = u ;

1. 退出同步代码块时会让_recursions减1,当_recursions的值减为0时,说明线程释放了锁。

2. 根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过ObjectMonitor::ExitEpilog 方法唤醒该节点封装的线程,唤醒操作最终由unpark完成,实现

如下:

void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) {   assert (_owner == Self, "invariant") ;   _succ = Knob_SuccEnabled ? Wakee->_thread : NULL ;   ParkEvent * Trigger = Wakee->_event ;     Wakee = NULL ;     // Drop the lock   OrderAccess::release_store_ptr (&_owner, NULL) ;  OrderAccess::fence() ;     // ST _owner vs LD in unpark()    if (SafepointSynchronize::do_call_back()) {     TEVENT (unpark before SAFEPOINT) ;  }    DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self);     Trigger->unpark() ;  // 唤醒之前被pack()挂起的线程.     // Maintain stats and report events to JVMTI   if (ObjectMonitor::_sync_Parks != NULL) {     ObjectMonitor::_sync_Parks->inc() ;  } }

被唤醒的线程,会回到 void ATTR ObjectMonitor::EnterI (TRAPS) 的第600行,继续执行monitor

的竞争。

// park selfif (_Responsible == Self || (SyncFlags & 1)) {   TEVENT (Inflated enter - park TIMED) ;   Self->_ParkEvent->park ((jlong) RecheckInterval) ;   // Increase the RecheckInterval, but clamp the value.   RecheckInterval *= 8 ;   if (RecheckInterval > 1000) RecheckInterval = 1000 ;} else {  TEVENT (Inflated enter - park UNTIMED) ;  Self->_ParkEvent->park() ; }if (TryLock(Self) > 0) break ;

monitor是重量级锁

可以看到ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic::inc_ptr等内核函数,

执行同步代码块,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这个时候就

会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。

所以synchronized是Java语言中是一个重量级(Heavyweight)的操作。

用户态和和内核态是什么东西呢?要想了解用户态和内核态还需要先了解一下Linux系统的体系架构:

e0965b2837ebc6dd832c206d781de7a7.png

从上图可以看出,Linux操作系统的体系架构分为:用户空间(应用程序的活动空间)和内核。

内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。

用户空间:上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。

系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用

所有进程初始都运行于用户空间,此时即为用户运行状态(简称:用户态);

但是当它调用系统调用执行某些操作时,例如 I/O调用,此时需要陷入内核中运行,我们就称进程处于内核运行态(或简称为内核态)。

系统调用的过程可以简单理解为:

1. 用户态程序将一些数据值放在寄存器中, 或者使用参数创建一个堆栈, 以此表明需要操作系统提

供的服务。

2. 用户态程序执行系统调用。

3. CPU切换到内核态,并跳到位于内存指定位置的指令。

4. 系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务。

5. 系统调用完成后,操作系统会重置CPU为用户态并返回系统调用的结果。

由此可见用户态切换至内核态需要传递许多变量,同时内核还需要保护好用户态在切换时的一些寄存器

值、变量等,以备内核态切换回用户态。这种切换就带来了大量的系统资源消耗,这就是在synchronized未优化之前,效率低的原因。


以上 就是本篇对synchronized的底层原理的解析,读到这里相信像小秃我这样的普通程序员已经开始感觉到脑子不够用了. 但是读到这相信你应该有那么一点豁然开朗的感觉.

读的比较认真的同学可能会有一些问题 比如

jvm是如何控制 Knob_QMode 参数(希望你还没有忘...) 来决定采用哪种模式来释放锁.

这些问题 我会在后续更新的文章中都做解释.

后面的内容会继续更新

JDK6对synchronized的优化,

cas的概念,作用和原理,

java对象的布局,

锁升级,

所偏向,

轻量级锁,

自旋锁,

锁消除

锁粗化

日常编写代码时如何synchronized进行优化

这些也都是大厂面试中会被频繁考察的知识点,敬请期待.

最后,谢谢有缘人的阅读,如有疑问和写的不对的地方请在留言区指正,我一定会吸取并改正.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值