从根本上理解Synchronized的加锁过程

这篇具有很好参考价值的文章主要介绍了从根本上理解Synchronized的加锁过程。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

作为一个Java开发,对于Synchronized这个关键字并不会陌生,无论是并发编程,还是与面试官对线,Synchronized可以说是必不可少。

在JDK1.6之前,都认为Synchronized是一个非常笨重的锁,就是在之前的《谈谈Java中的锁》中提到的重量级锁。但是在JDK1.6对Synchronized进行优化后,Synchronized的性能已经得到了巨大提升,也算是脱下了重量级锁这一包袱。本文就来看看Synchronized的使用与原理。

JDK1.6后优化点:

锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,synchronized的并发性能已经基本与J U C包提供的Lock持平

一、Synchronized的使用

在Java中,synchronized有:【修饰实例方法】、【修饰静态方法】、【修饰代码块】三种使用方式,分别锁住不同的对象。这三种方式,获取不同的锁,锁定共享资源代码段,达到互斥(mutualexclusion)效果,以此保证线程安全。

从根本上理解Synchronized的加锁过程

共享资源代码段又被称之为临界区,锁的作用就是保证临界区互斥,即同一时间临界区的只能有一个线程执行,其他线程阻塞等待,排队等待前一个线程释放锁

从根本上理解Synchronized的加锁过程

1.1 修饰实例方法

作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁

public synchronized void methodA() {
    System.out.println("作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁");
}

1.2 修饰静态方法

给当前类加锁,作用于当前类的所有对象实例,进入同步代码前要获得 当前 class 的锁

当被static修饰时,表明被修饰的代码块或者变量是整个类的一个静态资源,属于类成员,不属于任何一个实例对象,也就是说不管 new 了多少个对象,都只有一份,

public synchronized static void methodB() {
    System.out.println("给当前类加锁,作用于当前类的所有对象实例,进入同步代码前要获得 **当前 class 的锁**。");
}

如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

1.3 修饰代码块

指定加锁对象,对给定对象/类加锁

synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。

public void methodc() {
    synchronized(this) {
        System.out.println("锁住的是当前对象,对象锁");
    }
}

synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁

javapublic void methodd() {
    synchronized(SynchronizedDome.class) {
        System.out.println("给当前类加锁,SynchronizedDome class 的锁");
    }
}

二、Synchronized案例说明

了解了Synchronized的使用方法以后,接下来结合案例的方式,来详细看看Synchronized的加锁,多线程下是怎么执行的,我这里将按照上面三个使用方法来分别使用案例描述

2.1 修饰实例方法案例

  • 案例说明两个线程同时对一个共享变量sum进行累加3000,输出其最终结果,我们期望的结果最终应该是6000,接下来看看不加Synchronized修饰和加Synchronized修饰的情况下分别输出什么。

2.1.1 案例

  • 不加Synchronized修饰
package com.upup.edwin.sync;

public  class SynchronizedDome {

    //定义来了一个共享变量
    public static int sum = 0;

    // 进行累加3000次
    public void add() {
        for (int i = 0; i < 3000; i++) {
            sum = sum + 1;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedDome dome = new SynchronizedDome();

        Thread thread1 = new Thread(() -> dome.add());
        Thread thread2 = new Thread(() -> dome.add());
        thread1.start();
        thread2.start();
        //join()  方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出
        thread1.join();
        thread2.join();
        System.out.println("两个线程执行完成之后的累加结果:sum = " + sum);
    }

}

从根本上理解Synchronized的加锁过程

从结果上看,当我们不加synchronized修饰的时候,输出结果并不是我们锁期待的6000,这说明两个线程之间在执行的时候相互干扰了,也就是线程不安全。

加Synchronized修饰

我们对上面的add方法进行改造,在方法上加上synchronized关键字,也就是加上了锁,来看看它的执行结果

// 进行累加3000次
public synchronized void add() {
    for (int i = 0; i < 3000; i++) {
        sum = sum + 1;
    }
}

从根本上理解Synchronized的加锁过程

加上synchronized修饰后,发现输出结果与我们预期的是一致的,说明加上锁,两个线程是排队顺序执行的

2.1.2 案例执行过程

通过以上的两个案例对比,可以发现在synchronized修饰方法的时候,能够让结果正常输出,保证了线程安全,那么它是怎么做到的吗,两个线程的执行过程是怎么样的呢?

在前面我们提到了,当synchronized修饰实例方法的时候获取的是当前对象实例的锁,我们在代码中new出了一个SynchronizedDome对象,因此本质上锁住的是这个对象

SynchronizedDome dome = new SynchronizedDome();

所有线程要执行同步函数都要先获取锁(synchronized里面叫做监视器锁),获取到锁的线程才能执行同步函数,没有获取到的线程只能等待,抢到锁的线程执行完同步函数后会释放锁并通知唤醒其他等待的线程再次获取锁。流程如下

从根本上理解Synchronized的加锁过程

2.2 修饰静态方法案例

修饰静态方法与修饰实例方法基本一致,唯一的区别就是锁的不是当前对象,而是整个Class对象。我们只需要把上述案例中同步函数改成静态的就可以了

package com.upup.edwin.sync;

public  class SynchronizedDome {

    //定义来了一个共享变量
    public static int sum = 0;

    // 进行累加3000次
    public synchronized static void add() {
        for (int i = 0; i < 3000; i++) {
            sum = sum + 1;
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread thread1 = new Thread(() -> add());
        Thread thread2 = new Thread(() -> add());
        thread1.start();
        thread2.start();
        //join()  方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出
        thread1.join();
        thread2.join();
        System.out.println("两个线程执行完成之后的累加结果:sum = " + sum);
    }

}

可以看到当我们改成静态方法之后,就不需要在main方法中new SynchronizedDome()了,直接调用add即可,这也说明锁的不是当前对象了,

从根本上理解Synchronized的加锁过程

我们知道在Java中静态资源是属于Class的,不属于任何一个实例对象,而每个Class对象在Jvm中都是唯一的,所以我们锁住Class对象后,其他线程无法获取其静态资源了,从而进入等待阶段,本质上,锁住静态资源的执行过程与锁住实例方法的执行过程是一致的,只是锁的对象不一样而已

2.3 修饰代码块案例

静态资源锁Class,实例方法锁对象,还有一种就是锁住一个方法的某一段代码,也就是代码块。比如我们在上述的add 方法中调用了一个print方法

 public static void print(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("这是一个不需要加锁的方法,当前执行的线程是 : " + Thread.currentThread().getName());
    }

如上,print就是睡眠了5秒钟后输出一句话,不涉及到线程安全问题,如果使用synchronized修饰整个Add方法,并且在add中调用 print(),如下

public synchronized static void add() {
    print();
    for (int i = 0; i < 3000; i++) {
        sum = sum + 1;
    }
}

这种方式synchronized就会把add()整个包裹,使整个程序执行时间变长,完整案例如下

package com.upup.edwin.sync;

public  class SynchronizedDome {

    //定义来了一个共享变量
    public static int sum = 0;

    // 进行累加3000次
    public synchronized static void add() {
        print();
        for (int i = 0; i < 3000; i++) {
            sum = sum + 1;
        }
    }
    // 可以异步执行的方法
    public static void print(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("这是一个不需要加锁的方法,当前执行的线程是 : " + Thread.currentThread().getName());
    }


    public static void main(String[] args) throws InterruptedException {
        long l1 = System.currentTimeMillis();
        Thread thread1 = new Thread(() -> add());
        Thread thread2 = new Thread(() -> add());
        thread1.start();
        thread2.start();
        //join()  方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出
        thread1.join();
        thread2.join();
        long l2 = System.currentTimeMillis();
        System.out.println("两个线程执行完成的时间是:l2 - l1 = " + (l2 - l1) + " 毫秒");
        System.out.println("两个线程执行完成之后的累加结果:sum = " + sum);

    }

}

以上案例执行结果如下

从根本上理解Synchronized的加锁过程

很明显,两个线程在排队执行Add方法时,连print方法一起等待,但是实际上print是一个线程安全的方法,不需要获取锁,并且print方法还比较耗时,这就拖慢了整个程序的执行总时长,其执行过程如下

从根本上理解Synchronized的加锁过程

这种方式会将线程安全的方法也锁住,导致排队执行代码变多,时间变长,其本质就是synchronized锁住的是整个Add方法,粒度比较大,我们可以对add进行改造一下,让它只锁累计的那一段代码

public static void add() {
    print();
    synchronized(SynchronizedDome.class){
        for (int i = 0; i < 3000; i++) {
            sum = sum + 1;
        }
    }
}

如上,synchronized只锁了这for循环段代码,print()是可以并行执行的,这样就可以提升整个方法的执行效率,完整代码如下

package com.upup.edwin.sync;

public  class SynchronizedDome {

    //定义来了一个共享变量
    public static int sum = 0;

    // 进行累加3000次
    public static void add() {
        print();
        synchronized(SynchronizedDome.class){
            for (int i = 0; i < 3000; i++) {
                sum = sum + 1;
            }
        }
    }
    // 可以异步执行的方法
    public static void print(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("这是一个不需要加锁的方法,当前执行的线程是 : " + Thread.currentThread().getName());
    }


    public static void main(String[] args) throws InterruptedException {
        long l1 = System.currentTimeMillis();
        Thread thread1 = new Thread(() -> add());
        Thread thread2 = new Thread(() -> add());
        thread1.start();
        thread2.start();
        //join()  方法是让main线程等待线程 thread1 和thread2 都执行完成之后在继续执行下面的输出
        thread1.join();
        thread2.join();
        long l2 = System.currentTimeMillis();
        System.out.println("两个线程执行完成的时间是:l2 - l1 = " + (l2 - l1) + " 毫秒");
        System.out.println("两个线程执行完成之后的累加结果:sum = " + sum);
    }

}

从根本上理解Synchronized的加锁过程

修改之后,整个方法的执行时间只有5秒多,我们休眠的时间也是5秒,说明两个线程是一起进入的休眠,并不是排队的,其执行过程如下

从根本上理解Synchronized的加锁过程

在上述案例中我使用的是锁住整个class的方法:‘synchronized(SynchronizedDome.class)’,如果要改成锁住对象只需要改成’synchronized(this)'即可。其他执行流程都是一样的,只是获取的锁不一样

三、Synchronized原理剖析

以Hotspot(是Jvm的一种实现)为例,在Jvm中每个Class都有一个对象,对象又由 【对象头 + 实例数据 + 对齐填充(java对象必须是8byte的倍数)】三部分组成,每个对象都有一个对象头,synchronized的锁就是存在对象头中的。

3.1 对象头

既然synchronized的锁是存在对象头中的,那就先来了解一下对象头,Hotspot 有两种对象头:

  • 数组类型:如果对象是数组类型,则虚拟机用3字节存储对象头
  • 非数组类型:如果对象是非数组类型,则用2字节存储对象头

从根本上理解Synchronized的加锁过程

一般对象头由两部分组成

  • Mark Word

存储自身的运行时数据,比如:对象的HashCode,分代年龄和锁标志位信息。

Mark Word存储的信息与对象自身定义无关,所以Mark Word是一个一个非固定的数据结构,Mark Word里存储的数据会在运行期间随着锁标志位的变化而变化。

  • Klass Pointer:类型指针指向它的类元数据的指针。

Mark Word在不同的虚拟机下的bit位不一样,以下是32位与64位虚拟机的对比图

从根本上理解Synchronized的加锁过程

从根本上理解Synchronized的加锁过程

3.2 Monitor

在了解Monitor之前,先思考一个问题,前面我们说synchronized修饰方法和代码块的时候,加锁业务流程的执行过程是一样的,那么他们内部加锁实现是不是一样的呢?

其实加锁过程肯定是不一样的,不然加锁过程一样,锁一样,加锁业务流程的执行过程是一样,那就没必要分成方法和代码块了

我们可以找到上述案例中的SynchronizedDome类的Class文件,然后再命令行中执行javap -c -s -v -l SynchronizedDome.class就可以看到编译后指令集。可以分别查看synchronized修饰方法和代码块指令集的区别

synchronized代码块

从根本上理解Synchronized的加锁过程

上图中的指令集中有monitorenter、monitorexit两个指令,当synchronized修饰代码块时,JVM就是使用monitorenter和monitorexit两个指令实现同步的,当线程执行到monitorenter的时候要先获得monitor锁,才能执行后面的方法。当线程执到monitorexit的时候则要释放锁。

synchronized修饰方法

从根本上理解Synchronized的加锁过程

上图中的指令集中有一个ACCSYNCHRONIZED标记,当synchronized修饰方法时,JVM通过在方法访问标识符(flags)中加入ACCSYNCHRONIZED来实现同步功能,当线程执行有ACCSYNCHRONIZED标志的方法,需要获得monitor锁。每个对象都与一个monitor相关联,线程可以占有或者释放monitor。

3.1什么是Monitor锁

从上面的描述无论是修饰代码块还是修饰方法,都要获取一个Monitor锁,那么什么是Monitor锁呢?

Monitor即监视器,可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁

Monitor锁与对象的关系图:

从根本上理解Synchronized的加锁过程

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM中基于进入和退出Monitor对象,通过成对的MonitorEnter和MonitorExit指令来实现方法同步和代码块同步。

  • MonitorEnter插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor锁;
  • MonitorExit:插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit,释放Monitor锁;

3.2 Monitor锁的工作原理

每一个对象都会有一个monitor锁,Monitor锁的MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。

在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,ObjectMonitor中维护了一个锁池(EntryList)和等待池(WaitSet)。

ObjectMonitor工作模型图如下:

从根本上理解Synchronized的加锁过程

ObjectMonitor工作模型图大致描述了以下几个步骤

所有新的线程都会进入(①号入口)EntryList中去竞争锁

当有线程通过CAS把monitor的owner字段设置为自己时,说明这个线程获取到了锁,也就是进入图中的(②号入口)owner区域,其他线程进入阻塞状态

如果当前线程是第一次进入该monitor,将recursions由0设置为1,_owner为当前线程,该线程成功获得锁并返回

如果当前线程不是第一次进入该monitor,说明当前线程再次进入monitor,即重入锁,执行recursions ++ ,记录重入的次数

如果获取到锁的线程(owner)执行了wait等方法,就会释放锁,并进入(③号入口)waitset中,于此同时通知waitset中其他线程重新竞争锁,获取到锁(④号入口)进入owner区域

当线程执行完同步代码,会释放锁(由⑤号口出),于此同时通知waitset和EntryList中其他线程重新竞争锁

释放锁线程执行monitorexit,monitor的进入数-1,执行过多少次monitorenter,最终要执行对应次数的monitorexit

从根本上理解Synchronized的加锁过程

四、Synchronized锁优化

接下来看看Synchronized的锁优化。锁优化主要包含:锁粗化、锁消除、锁升级三部分。

4.1 锁粗化

同步代码块要求我们将同步代码的范围尽量缩小,这样可以使同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁

比如上述案例add的循环中,如果将Synchronized防止for循环里面不是范围更小吗?

for (int i = 0; i < 3000; i++) {
    synchronized(SynchronizedDome.class){
        sum = sum + 1;
    }
}

这样虽然缩小了范围,但是未必缩短了时间,因为在加锁过程中也会消耗资源,如果频繁的加锁释放锁,可能会导致性能损耗。

基于此,JVM会对这种情况进行锁粗化,锁粗化就是将【多个连续的加锁、解锁操作连接在一起】,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。

J V M在检测到上述for循环再频繁获取同一把锁的到时候,就会将加锁的范围粗化到循环操作的外部,使其只需要获取一次锁就可以,减小加锁释放锁的开销。

从根本上理解Synchronized的加锁过程

4.2 锁消除

Java虚拟机在JIT编译时,会进行逃逸分析(对象在函数中被使用,也可能被外部函数所引用,称为函数逃逸),通过对运行上下文的扫描,分析synchronized锁对象是不是只被一个线程加锁,不存在其他线程来竞争加锁的情况。这样就可以消除该锁了,提升执行效率。

锁消除的经典案例就是StringBuffer 了,StringBuffer 是线程安全的,其内部的append方法就是通过synchronized加锁的,源码如下

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

当我们调用StringBuffer 的append时,就会加锁,但是当我们使用的对象经过逃逸分析后,认为该对象不会被其他线程共享的时候,就会将append方法的synchronized去掉,编译不加入monitorenter和monitorexit指令。比如下面这个方法

public static String appendStr(String str, int i) {
    StringBuffer sb= new StringBuffer();
    sb.append(str);
    sb.append(i);
    return sb.toString();
}

StringBuffer的append虽然是同步方法。但appendStr中的sb对象没有传递到方法外,不会被其他线程引用,不存在锁竞争的情况,因此可以进行锁消除

五、Synchronized锁升级

我们常说的锁升级其实就是这几种锁的升级跃迁。其中有无锁、偏向锁 、轻量级锁、 重量级锁等几种锁的实现。锁升级过程:【无锁】—>【偏向锁】—>【轻量级锁】—>【 重量级锁】。

从根本上理解Synchronized的加锁过程

而锁的变化其实就是一个标志位的变化,在前面提到的对象头中Mark World时有提到它存储的就是对象的HashCode,分代年龄和锁标志位信息。因此锁的升级变化,本质上就是Mark World中锁标志位的变化。以上几种锁的标志位信息如下

锁状态 存储内容 存储内容
无锁 对象的hashCode、对象分代年龄、是否是偏向锁(0) 01
偏向锁 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(重量级锁)的指针 10

注意:

锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态!!!

锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态!!!

锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态!!!

5.1 无锁升级为偏向锁

  • 为啥要有偏向锁

大多数情况下是一个线程多次获得同一个锁,不存在锁竞争的,而竞争锁会增大资源消耗,,为了降低获取锁的代价,才引入的偏向锁。

当线程第一次执行到同步代码块的时候,锁对象变成就会偏向锁(通过CAS修改对象头里的锁标志位),其目标就是在只有一个线程执行同步代码块时,降低获取锁带来的消耗,提高性能

偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,可以通过JVM配置成没有延迟

-XX:BiasedLockingStartUpDelay=0

可以通过J V M参数关闭偏向锁,关闭之后程序默认会进入轻量级锁状态

-XX:-UseBiasedLocking=false

无锁升级为偏向锁,其本质是判断对象头的Mark Word中线程ID与当前线程ID是否一致以及偏向锁标识,如果一致直接执行同步代码或方法,具体流程如下

从根本上理解Synchronized的加锁过程

无锁状态,存储内容「是否为偏向锁(0)」,锁标识位01

CAS设置当前线程ID到Mark Word存储内容中,并且将是否为偏向锁0 修改为 是否为偏向锁1

在Mark Word和栈帧中记录获取到偏向的锁的threadID

执行同步代码或方法

偏向锁状态,存储内容「是否为偏向锁(1)、线程ID」,锁标识位01

对比线程ID是否一致,如果一致无需使用CAS来加锁、解锁,直接执行同步代码或方法

因为偏向锁不会自动释放锁,因此后续线程A再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致

如果不一致,CAS将Mark Word的线程ID设置为当前线程ID,设置成功,执行同步代码或方法

其他线程,如线程B要竞争锁对象,而偏向锁不会主动释放,因此Mark Word还是存储的线程A的threadID

此时会检查Mark Word的线程A是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程B)可以竞争将其设置为偏向锁;

CAS设置失败,证明存在多线程竞争情况,触发撤销偏向锁,当到达全局安全点,偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后在安全点的位置恢复继续往下执行。

如果Mark Word的线程A是存活,则线程B的CAS会失败,此时会暂停线程A,撤销偏向锁,升级为轻量级锁,

5.2 偏向锁升级为轻量级锁

轻量级锁又称自旋锁,一般在竞争锁对象的线程比较少,持有锁时间也不长的场景中,由于阻塞线程、唤醒线程需要C P U从用户态转到内核态,时间比较长,如果同步代码块执行的时间比这更时间短,那就本末倒置了,所以这种情况一般不阻塞线程,让其自旋一段时间等待锁其他线程释放锁,通过自旋换取线程在用户态和内核态之间切换的开销。

锁竞争

如果多个线程轮流获取一个锁,但是每次获取锁的时候没有发生阻塞,就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。

当前线程持有的锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

升级为轻量级锁有两种情况:

当关闭偏向锁功能时,会由无锁直接升级为轻量级锁

多个线程竞争偏向锁导致偏向锁升级为轻量级锁

这两种情况下偏向锁升级为轻量级锁过程如下

从根本上理解Synchronized的加锁过程

无锁状态:存储内容「是否为偏向锁(0)」,锁标识位01

关闭偏向锁功能时
  • CAS设置当前线程栈中锁记录的指针到Mark Word存储内容
  • 锁标识位设置为00
  • 执行同步代码或方法

轻量级锁状态:存储内容「线程栈中锁记录的指针」,锁标识位00

  • CAS设置当前线程栈中锁记录的指针到Mark Word存储内容,设置成功获取轻量级锁,执行同步块代码或方法
  • 设置失败,证明多线程存在一定竞争,线程自旋上一步的操作,自旋一定次数后还是失败,轻量级锁升级为重量级锁
  • Mark Word存储内容替换成重量级锁指针,锁标记位10

5.3 轻量级锁升级为重量级锁

轻量级锁在自旋一定次数之后还没获取到锁,就升级为重量级锁,重量级锁是依赖操作系统的MutexLock(互斥锁)来实现的,需要从用户态转到内核态,成本非常高,等待锁的线程都会进入阻塞状态,防止CPU空转。

从根本上理解Synchronized的加锁过程

计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改文章来源地址https://www.toymoban.com/news/detail-431710.html

到了这里,关于从根本上理解Synchronized的加锁过程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • synchronized简单理解

    synchronized是一种互斥锁,也成为同步锁,它的 作用是保证在同一时刻,被修饰的代码块或方法只会有一个线程执行,以到达保证并发安全效果。 在JDK1.6以前,很多人称之为重量级锁,性能不高。但是在JDK1.6以后,对sychronized进行了一些优化,引入了偏向锁,轻量级锁,以及重

    2024年02月08日
    浏览(23)
  • 【JVM】synchronized锁升级的过程

    目录 如何从无锁状态到偏向锁状态: 偏向锁升级为轻量级锁: 轻量级锁到自旋锁的状态: 自旋锁升级为重量级锁: 下面是自旋锁升级到重量级锁的过程: 重量级锁的特点如下: synchronized锁分为三种状态——偏向锁、轻量级锁、重量级锁         当一个线程访问被syn

    2024年02月09日
    浏览(30)
  • 多线程JUC 第2季 synchronized锁升级过程

    用锁能够实现数据的安全,但是会带来性能下降。Synchronized是一个重量级锁,锁的升级过程: 无锁-偏向锁-轻量级锁-重量级锁。 高并发时,同步调用应尽量考虑锁的性能损耗,能用无锁数据结构,就不要用锁,能用区块不要用锁住整个方法体;能有对象锁,就不要用类锁。

    2024年02月09日
    浏览(38)
  • 【并发编程】深入理解Java并发之synchronized实现原理

    分析: 通过 new MyThread() 创建了一个对象 myThread ,这时候堆中就存在了共享资源 myThread ,然后对 myThread 对象创建两个线程,那么thread1线程和thread2线程就会共享 myThread 。 thread1.start() 和 thead2.start() 开启了两个线程,CPU会随机调度这两个线程。假如 thread1 先获得 synchronized 锁,

    2024年02月04日
    浏览(59)
  • java八股文面试[多线程]——synchronized锁升级过程

    速记:偏向-轻量-重量 上面讲到锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是: 无锁——偏向锁——轻量级锁——重量级锁 ,并且膨胀方向 不可逆 一.锁升级理论. 在synchronized锁升级过程中涉及到以下几种锁.先说一下这几种锁是什么意思. 偏向锁: 只有一个

    2024年02月10日
    浏览(42)
  • Java 八股文面试过程系列之synchronized关键字

    本文通过一场虚拟的面试对话,详细解析了Java中的synchronized关键字。从基本用法到底层实现原理,再到性能优化策略,全面提升您对同步机制的理解。

    2024年02月07日
    浏览(50)
  • centos7将jar作为一个服务操作过程

    打开 /etc/systemd/system 目录 新建一个文件 写入以下内容 这里写个例子 :wq 保存退出 服务相关命令

    2024年02月13日
    浏览(35)
  • 全面理解C++函数最难理解的部分:数组形参,函数指针,以及函数指针作为形参

    我提到的这些部分,是我在自学C与C++中遇到的比较困难的点。因为初学者的编程,不太容易使用到这些点,所以很容易造成遗忘,并且自己写很容易出错。 最近在看标准C库的源码的时候遇到了这样的困惑,就是关于函数指针,或者说,把一个函数作为另一个函数的参数的这

    2024年02月07日
    浏览(36)
  • chatGPT4问世,作为一个程序员应当如何去理解?

    前几年 AI 发展也遇到过许多瓶颈,甚至很多AI投资者因为技术得不到突破而破产。但近几年AI技术飞速发展,特别是今天的主题chatGPT 第一次问世还只是一个帮学生写作业的工具,第二次迭代即可完成大部分市场业务,回答很多刁钻的问题。 有人测试过问chatGPT一些很难以回答

    2023年04月10日
    浏览(56)
  • 深入理解深度学习——正则化(Regularization):作为约束的范数惩罚

    分类目录:《深入理解深度学习》总目录 考虑经过参数范数正则化的代价函数: J ~ ( θ ; X , y ) = J ( θ ; X , y ) + α Ω ( θ ) tilde{J}(theta;X, y) = J(theta;X, y) + alphaOmega(theta) J ~ ( θ ; X , y ) = J ( θ ; X , y ) + α Ω ( θ ) 回顾《拉格朗日乘子法(二):不等式约束与KKT条件》我们可以构

    2024年02月08日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包