Synchronized详细介绍之锁升级过程

synchronized锁升级过程详解 注:synchronized锁流程如下 第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁” 第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。 第三步,两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作,把共享对象的MarKword的内容修改为自己新建.. 阅读详情

前言

我们在并发编程过程中,会有一些资源或者操作是必须要进行序列化访问的,就是线程之间不能并发的访问,必须要进行串行访问,所以就引入了锁的概念,java中只用的锁主要有两种,一种是jdk内置的锁,一种是juc包下面的锁,jdk内置的锁是不要释放的,由jvm自动给我们释放锁,而juc包下面的锁是有Doug Lea大神开发的,在编程的时候需要在finally进行锁的释放,否则很容易导致死锁;

线程与进程的区别

谈到并发编程,系统的运行至少要有一个线程,而线程是运行在进程之中的,进程是存在于操作系统中的,操作系统中可以存在很多进程,这些进程可以并发的进行,而每个进程都必须至少包含一个线程,线程是运行在进程中的,进程是操作系统资源分配的最小单位,线程不直接操作操作系统资源,而是共享进程的资源,所以我们也可以称线程是轻量级的进程。

进程

进程,简单来说就是我们开发一个程序,存放在服务器的硬盘上,当程序运行时,会在操作系统的内存空间形成一块独立的内存空间,有自己的内存地址、堆,上面挂靠的单位是操作系统,操作系统会以进程为单位,分配系统资源,包括CPU时间片段、内存空间等,进程是资源分配的最小单位。

线程

线程是运行在进程中的,线程也被称为轻量级的进程,是操作系统调度(cpu调度)的最小单位。

区别

调度方面:线程作为操作系统调度的最小单位,而进程是作为拥有系统资源的基本单位;
并发性: 不仅进程之间可以并发的进行,同一进程之间的不同线程也可以并发的执行;
拥有资源:进程是拥有资源的基本单位,线程不拥有系统资源,但是线程可以访问隶属于进程共享资源,
进程维护的是程序所包含的静态资源如:地址空间、打开的文件句柄集、问系统状态、信号处理等;而线程所维护的是线程资源如:线程栈资源、调度控制信息、待处理的信息等。
系统开销:当进程进行进行创建或者销毁时,因为系统都要为进程分配进程资源和回收资源,导致创建进程或者销毁进程所带来的开销明细大于线程创建和销毁的开销;但是进程有自己独立的内存空间,内存地址,一个进程销毁或者崩溃过后,在保护模式下,其他进程不受影响,而线程是运行在进程中的,线程是没有自己单独的内存地址的,一个进程的死掉过后,那么运行这个进程之下的所有线程都将被死掉,所以在某种意义上来说,多进程比多线程更健壮,但是多进程在进程切换时开销较大,效率要低一些。

协程

协程,英文Coroutines, 是一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对内核来说不可见的特性。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源
在这里插入图片描述

JVM线程调度原理

在前面的笔记中曾多次提到我们创建一个线程执行,最终会提交给内核态去执行,也就是由用户态切换到内核态,我们想一下,为什么需要线程池管理?用户态创建的所有线程都最终都需要内核态去创建执行,那么如果不启用线程池,那么来一个线程就由用户态创建一个线程交给内核态去创建执行,那么这样的开销是非常大的,就比如本文的主题是synchronized在jdk15之前就是采用内核态来控制的同步操作,在这种模式下,就是通过monitorEnter和monitorExit来控制的,而操作底层是通过mutex互斥信号量来实现的,那么这种情况下有锁的竞争就会在用户态和内核态之间反复切换,这样在性能就影响很大,那么在JDK1.6之后就引入了锁的优化,也就是锁升级,由无锁升级为偏向锁,如果竞争紧张,由升级轻量级锁,如果并发上来,升级为重量级锁,在这个过程中根据不同的业务场景选取不同的锁来实现;举个例子,两个人同时要进一个房间,这个房间是有钥匙的,钥匙在前台管理员处,比如第一个人首先拿到管理员的钥匙,进入了房间,那么第二人过来也要进这个房间,如果他会去管理员处不断询问是否可以进,如果可以,管理员就拿钥匙给他,但是如果第一个人迟迟没有出来,那么第二个人会一直不停的去询问,直到第一个人出来为止,那么如果门上如果有个标记,表示房间有人,直到进入房间的人出来过后标记清除,那么第二个人就可以进入了,那么这样是不是效率要更高一点,避免了反复去管理员处询问房间是否可以使用的的性能开销。所以这里把管理员比喻成操作系统内核,而这两个人比喻成两个线程,而房间是一个对象,那么对象上不打标记,那么第二个线程会反复的去操作系统内核判断是否可以进入同步代码块,那么就会有反复的进行用户态与内核态直接的切换,这样的性能开销是非常大的,而采用对象打标机的模式,那么就只有在用户态之间切换,这样就提升了性能,避免了不必要的性能开销,这也就是synchronized在jdk1.6过后的锁优化达到的效果,也就是通过锁升级来实现的。

JVM线程调用过程

在这里插入图片描述
JAVA中创建线程由用户态切换到内核态进行创建线程,也就是上面所说线程是操作系统调度的最小单位

JAVA线程与内核线程的关系

在这里插入图片描述

源码分析

public
class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

这个是Thread的源码的类信息,首先实现了Runnable接口,这个接口中run方法是我们线程执行的具体业务逻辑方法,如果创建了线程,则要实现这个run方法,实现我们自己的业务逻辑,上面已经说了我们的线程调用是要交给内核去处理的,而java和jvm都是用户态的模式,所有需要调用本地c++方法开启内核线程,所以registerNatives就是注册本地方法

public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}
private native void start0();

这是线程启动方法,线程启动方法中有一行代码star0(),这个是调用C++中的本地方法启动线程,也就是告诉内核要启动一个线程,而线程启动过后,C++会回调我们的run方法,start0()这个方法是C++中的方法,如果要分析的话,那么只有使用openjdk的源码去分析,目前这个电脑上没有jdk的源码,空了会把jdk线程启动这块写出来,我这里总结下启动流程:
1.java中使用new Thread()创建一个线程,然后调用start方法启动一个java级别也就是用户态的一个线程;
2.然后Thread会调用本地方法start0(),去调用JVM中的JVM_startThread方法进行线程创建和启动;
3.JVM调用new javaThread进行线程的创建,并且会根据不同的操作系统平台调用对应的操作系统的线程创建os::create_thread;
4.创建的线程状态为Initialized,初始化过后调用了sync->wait方法进行等待,等到被唤醒过后调用thread ->run;
5.调用Thread::start方法进行线程启动,此时线程状态设置为RUNNABLE,接着调用os::start_thread,根据不同的操作系统选择不同的线程启动方式;
6.线程启动过后状态设置为RUNNABLE,并唤醒第四步中处于等待的线程,接着执行thread->run方法;
7.JavaThread::run会回调第一步new Thread中复写的run方法。

线程状态

//Thread中的线程状态枚举
public enum State {
    /**
     * Thread state for a thread which has not yet started.
     */
     //当一个线程被创建的时候就处于NEW的状态
    NEW,

    /**
     * Thread state for a runnable thread.  A thread in the runnable
     * state is executing in the Java virtual machine but it may
     * be waiting for other resources from the operating system
     * such as processor.
     */
     //线程调用start过后的状态
    RUNNABLE,

    /**
     * Thread state for a thread blocked waiting for a monitor lock.
     * A thread in the blocked state is waiting for a monitor lock
     * to enter a synchronized block/method or
     * reenter a synchronized block/method after calling
     * {@link Object#wait() Object.wait}.
     */
     //线程被阻塞的状态(这个阻塞不是调用sleep不是wait,一定要区分开来,官方的解释是只有当这个线程
     //进入synchronized 或者再次进入synchronized 才会有这个状态)
    BLOCKED,

    /**
     * Thread state for a waiting thread.
     * A thread is in the waiting state due to calling one of the
     * following methods:
     * <ul>
     *   <li>{@link Object#wait() Object.wait} with no timeout</li>
     *   <li>{@link #join() Thread.join} with no timeout</li>
     *   <li>{@link LockSupport#park() LockSupport.park}</li>
     * </ul>
     *
     * <p>A thread in the waiting state is waiting for another thread to
     * perform a particular action.
     *
     * For example, a thread that has called <tt>Object.wait()</tt>
     * on an object is waiting for another thread to call
     * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
     * that object. A thread that has called <tt>Thread.join()</tt>
     * is waiting for a specified thread to terminate.
     */
     //官方解释一句很明白了,当一个线程被调用了eait过后就进行了wating,将cpu执行执行时间片给其他线程,
     //当wait的对象在调用notify或者notifyall时会被唤醒,如果调用join也会进行waiting
    WAITING,

    /**
     * Thread state for a waiting thread with a specified waiting time.
     * A thread is in the timed waiting state due to calling one of
     * the following methods with a specified positive waiting time:
     * <ul>
     *   <li>{@link #sleep Thread.sleep}</li>
     *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
     *   <li>{@link #join(long) Thread.join} with timeout</li>
     *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
     *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
     * </ul>
     */
     //超时等待,官方解释的意思是当调用sleep(time),wait(time),join(time)等等会在指定
     的时间内等待
    TIMED_WAITING,

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */
     //线程完成
    TERMINATED;
}

在这里插入图片描述

Synchronized锁

加锁方式

在这里插入图片描述
总结:
同步方法锁:锁的是当前实例对象
同步静态方法:锁的类对象
同步代码块:锁的是指定对象

原理

互斥性:synchronized修饰的方法、代码块只能由一个线程进行访问,其他线程只能阻塞等着;
可见性:某线程 A 对于进入 同步块之前或在 synchronized 中对于共享变量的操作,对于后续的持有同一个监视器锁的其他线程可见。
在早期的synchronized中的同步锁实现比较简单,我们通过实例类分析,看下面的代码:

public class T0923 {

    private int sum = 1;
    public static void main(String[] args) {

        T0923 t = new T0923();
        t.add();

    }

    public synchronized void  add(){
        sum ++ ;


    }
}

我们查看add方法字节码:在命令窗口输入javap -verbose T0923 .class

public synchronized void add();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field sum:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field sum:I
        10: return
      LineNumberTable:
        line 14: 0
        line 17: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/t0923/T0923;
}

如果说在方法上加了synchronized的话,那么在falgs上会有一个ACC_SYNCHRONIZED标志,
那么我修改下代码,synchronized修饰的是代码块呢?

public  void  add(){
    synchronized(this){
        sum ++ ;
    }
}
 public void add();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_0
         5: dup
         6: getfield      #2                  // Field sum:I
         9: iconst_1
        10: iadd
        11: putfield      #2                  // Field sum:I
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return

上述代码的计数器
3: monitorenter
15: monitorexit
21: monitorexit
也就是说我们如果锁的是代码块,你那么进入的是monitorenter,退出锁是monitorexit,但是我们看21行还有一个monitorexit,是因为如果在抛出异常过后,也是进行monitorexit的,所以早期的synchronized就是通过这两种方式实现的锁,两个指令的执行是JVM通过调用操作系统的互斥量mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
其实对于synchronized来说,它会自动释放锁,并且在程序抛出异常过后也能自动释放锁,从上述代码的21行就知道,主义看代码23行,就是在抛出异常之前回把锁释放掉,其实从上述的代码字节码序列也可以看出synchronized用的就是monitor机制,常常也被称作为管程,而monitor机制底层其实用的就是Unsafe的CAS操作,就是谁先进入同步代码块,谁就更改标志位当前线程,monitor机制是通过操作系统的互斥量mutex来实现的,比如我们来看一段程序:

public class SyncT1 {

    private static final Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        SyncT1 t1 = new SyncT1();
        Thread thread1 = new Thread(()->{
            t1.test();
        },"Thread-1");
        Thread thread2 = new Thread(()->{
            t1.test();
        },"Thread-2");

        thread1.start();
        thread2.start();
    }
    private void test(){
        synchronized (obj){
            System.out.println(Thread.currentThread().getName()+" 进入");
            try {
                Thread.sleep(1000000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

上述代码,在sleep时间结束内,线程2肯定是进不去的,这个毫无疑问,运行结果如下:
在这里插入图片描述
线程1进入了同步代码块,然后sleep过后,线程2是无法进入同步代码块的,所以我们修改下代码:

private void test(){
    synchronized (obj){
        System.out.println(Thread.currentThread().getName()+" 进入");
        try {
            UnSafeFactory.getUnsafe().monitorExit(obj);
            Thread.sleep(1000000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

UnSafeFactory.getUnsafe().monitorExit(obj)让obj这个对象的监视器退出,执行结果如下:
在这里插入图片描述
可以看到线程2已经进入了,因为直行了monitorExit过后,synchronized已经退出了,所以通过这里演示可以知道synchronized锁定代码块是通过对象监视器来实现的。

synchronized锁优化

从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。在 JDK 1.6 中默认是开启偏向锁的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。使用-XX:-UseSpinning参数关闭自旋锁优化;-XX:PreBlockSpin参数修改默认的自旋次数。
偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
重量级锁:有实际竞争,且锁竞争时间长。

monitor监视器

monitor概念

管程,监视器。在操作系统中,存在着semaphore和mutex,即信号量和互斥量,使用基本的mutex进行开发时,需要小心的使用mutex的down和up操作,否则容易引发死锁问题。为了更好的编写并发程序,在mutex和semaphore基础上,提出了更高层次的同步原语,实际上,monitor属于编程语言的范畴,C语言不支持monitor,而java支持monitor机制。
一个重要特点是,在同一时间,只有一个线程/进程能进入monitor所定义的临界区,这使得monitor能够实现互斥的效果。无法进入monitor的临界区的进程/线程,应该被阻塞,并且在适当的时候被唤醒。显然,monitor作为一个同步工具,也应该提供这样管理线程/进程的机制。

Monitor基本元素

1.临界区
2.monitor对象和锁
3.条件变量以及定义在monitor对象上的wait,signal操作。
使用monitor主要是为了互斥进入临界区,为了能够阻塞无法进入临界区的进程,线程,需要一个monitor object来协助,这个object内部会有相应的数据结构,例如列表,用来保存被阻塞的线程;同时由于monitor机制本质是基于mutex原语的,所以object必须维护一个基于mutex的锁。

临界区的圈定

被synchronized关键字修饰的方法,代码块,就是monitor机制的临界区

Monitor Object

我们知道java的所有对象都都默认继承了java.lang.Object,一个Object对象在内存中的默认都是16byte,对象在内存中布局是mark word,类型指针,实例数据和对齐填充,如下图:
在这里插入图片描述
根据笔记上面的描述来说,在JDK1.6过后,对锁做了优化,比如我们上面所描述的例子,进入房间的例子,那么就会频繁的与内核进行交互,这样就会出现用户态与内核态频繁的交互导致性能低下,因为synchronized锁的对象,而jdk的锁优化就会在对象的mark word中进行打标记,这样就不用每次都去内核态通过互斥量mutex来进行同步控制;我们知道java.lang.Object中的wait和notify以及notifyAll是必须要在synchronized中进行使用的,也就是说当线程进入了synchronized代码块过后,可以通过wait、notify/notifyall进行等待唤醒操作,这个是在同步代码中使用的,如果想通过线程通信来阻塞和唤醒,那么可以使用juc包下面的相关线程类。我们知道了Object中的wait和notify等操作过后,那么在C++中JVM是通过ObjectMonitor这个类来实现的monitor机制,而monitor机制其实也是用的CAS机制,待会儿分析下源码 ,这里先来看下monitor Object机制
在这里插入图片描述
The Owner表示当前线程执行的同步代码块
Entry Set表示等待进入线程的队列
Wait Set表示进入了同步代码块而由于sleep或者wait进入的队列
上图的表达的就是一个Monitor Object机制的原理
简单来说就是所有线程都想要进入同步代码块,首先线程3先进入同步代码块,然后由于线程3调用了wait或者sleep进入了Wait Set,这个时候线程3让出了CPU的时间片段,其他线程就开始竞争,下个线程4得到也竞争得到了进入同步代码块的权限,然后也线程4和3一样调用了wait或者sleep进入了等待对垒,这个时候又进入竞争,线程5进入了执行同步代码块,然后线程5执行完成了释放了锁,这个时候1,2,3,4开始竞争,而线程4得到了权限从wait set进入同步代码块执行,其他线程进入等待队列。上图要表达的意思就是这样的,至少在我看来是这样的;
总结来说,就是ObjectMonitor维护了两个队列,一个进入的等待队列,一个是wait的等待队列,wait的等待队列是由于在同步代码中调用了wait或者sleep进入的,而Owner是只允许一个线程进入执行的,当Owner执行完成过后,两个队列中的线程开始进入竞争,因为synchronized是非公平的,所以竞争中,谁都有可能竞争到,如果某个线程优先级比较高,那么就算进入了等待队列,那么很有可能每次都有机会论到它执行。

ObjectMonitor

由于我对C++不是太懂,只能看懂一些,小部分,所以我也只能把实现的意思大概读懂

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;//表示获得锁的次数,因为synchronized是可重入锁,所以可以获得多次
    _object       = NULL;
    _owner        = NULL;//当前执行的线程
    _WaitSet      = NULL;//调用了wait或者sleep进入的队列
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;//在进入同步代码块之前的阻塞队列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

monitorenter

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操作,self是当前想要进入的先,owner监视器中的线程对象,
 //而NULL就是代表我们要比较的值
 //如果owner==null,比较成功,则证明是可以进入的,那么这个时候将owner设置为self,也就是当前进入的线程
 //如果cas成功,则返回比较前的值null
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  if (cur == NULL) {
  	//代表cas成功了,获得锁
     // Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
     assert (_recursions == 0   , "invariant") ;
     assert (_owner      == Self, "invariant") ;
     // CONSIDER: set or assert OwnerIsThread == 1
     return ;
  }

  if (cur == Self) {
     // TODO-FIXME: check for integer overflow!  BUGID 6557169.
     //如果cur不等于null,cur == self,那么证明之前获得锁的线程就是它自己,所以这里将获得锁的次数+1,因为synchronized是可重入锁
     _recursions ++ ;
     return ;
  }

MonitorExit

void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
   Thread * Self = THREAD ;
   //判断当前线程是否是正在执行的线程,如果不是,把owner设置为当前线程,并且锁
   //次数为0,如果锁次数为0,则为释放锁
   if (THREAD != _owner) {
     if (THREAD->is_lock_owned((address) _owner)) {
       // Transmute _owner from a BasicLock pointer to a Thread address.
       // We don't need to hold _mutex for this transition.
       // Non-null to Non-null is safe as long as all readers can
       // tolerate either flavor.
       assert (_recursions == 0, "invariant") ;
       _owner = THREAD ;
       _recursions = 0 ;
       OwnerIsThread = 1 ;
     } else {
       // NOTE: we need to handle unbalanced monitor enter/exit
       // in native code by throwing an exception.
       // TODO: Throw an IllegalMonitorStateException ?
       TEVENT (Exit - Throw IMSX) ;
       assert(false, "Non-balanced monitor enter/exit!");
       if (false) {
          THROW(vmSymbols::java_lang_IllegalMonitorStateException());
       }
       return;
     }
   }
// 如果_recursions次数不为0.自减
   if (_recursions != 0) {
     _recursions--;        // this is simple recursive enter
     TEVENT (Inflated exit - recursive) ;
     return ;
   }

Synchronized锁升级

锁升级原理

我们通过前面的笔记已经知道synchronized锁的是对象,而锁优化过后,默认是无锁,再到偏向锁,轻量级锁,重量级锁,那么这是一个过程,而且这个过程是用户态进行的,没有到内核态进行,那么synchronized是如何实现的,因为锁的是对象,那么就是在对象的对象头中实现的,关于对象在内存中的布局在jvm的笔记中已经详细记录,我这里就简单来说,synchronized是通过对象头的markword来进行,那么markword到底是什么样的,我们以64位机器来看:
在这里插入图片描述

图(1)

在这里插入图片描述
图(2)
通过图1和图2可以看到:
lock状态(2bit):
1.01是无锁或者偏向锁;
2.00是轻量级锁;
3.10是重量级锁;
4.11是GC标记,表示可以被GC了,被GC打了标记了。
biased_lock(1bit):是否偏向锁的标志,0=否 ,1=是
age(4bit):对象的分代年龄,占4bit,所以分代年龄的最大年龄是15,设置智能是小于等于15;
unused:表示未使用的
epoch(2bit):表示偏向锁的时间戳
identity_hascode(31bit):对象的hashcode值
ptr_to_lock_record(62bit):轻量级锁状态下,指向对象监视器的monitor的指针
prt_to_heavyweight_monitor:重量级锁状态下,指向对象监视器的monitor指针

总结:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

无锁升级为偏向锁

在JDK1.6过后,默认是开启了偏向锁的,偏向锁的性能较低,偏向锁适用于单线程的环境下,所以要根据具体的业务情况来使用,如果synchronized在第一个线程进入的情况下,默认修改为偏向锁,将当前线程的ID更新到对象头的markword的线程ID中,增加偏向锁的时间戳以及偏向锁的标志修改为1
在这里插入图片描述
比如有几个线程同时访问同步代码块,那么只有一个线程可以进入,那么初始的object markword肯定是无锁的,也就是上图的无锁对象头,那么这个时候线程1把当前线程的ID通过CAS修改到markword中,这个过程中的CAS肯定是能成功的,不成功就不是无锁升级为偏向锁了,还是其他锁升级的过程了;这个时候其他线程是在线程1未退出同步代码块的时候是没有八法进入同步代码块,也就是没有办法获取锁,那么其他获取cpu执行权限的线程会通过CAS修改线程ID为当前线程,但是如果线程1没有退出同步代码块,而后续线程通过CAS进行修改是不能成功的,那么这个时候后续的线程就将synchronized升级为轻量级锁,也就是下一个锁升级过程。

偏向锁升级为轻量级锁

偏向锁升级为轻量级锁是在线程有竞争的情况下,线程1迟迟没有退出同步代码块,而线程2又要竞争这把锁,而线程2通过CAS自适应自旋一直没有成功,这个时候它就升级为轻量级锁,如果在CAS的过程中,线程1退出了同步代码块,那么这个时候线程2CAS成功,是不会升级为轻量级锁,所以偏向锁适用于单线程的环境下
在这里插入图片描述
轻量级锁是在线程有一定的竞争的时候想要进入同步代码块,而如果这个时候之前运行在synchronized的线程退出了,那么不会升级为轻量级锁,还是偏向锁,如果线程2cas结束过后,线程1还没有退出就会进行锁升级,偏向锁升级为轻量级锁,这个升级过程非常消耗性能,所以有很多公司都是禁止出现偏向锁的,因为偏向锁升级为轻量级锁的时候,是需要撤销偏向锁的,撤销偏向锁的过程如下:
1.在一个安全点停止所有拥有锁的线程;
2.遍历线程栈,如果存在锁记录,需要修复锁记录和MarkWord,使其变成无锁的状态;
3.唤醒当前线程,将当前锁升级为轻量级锁;
所以这个过程是非常消耗性能的,所以不适合在多线程的环境下使用偏向锁

轻量级锁升级为重量级锁

在这里插入图片描述
重量级锁在并发非常高的情况下启用,就是锁的竞争非常激励,比如线程1首先将在线程栈上开辟一定的空间来存储mark word,并且相互指向,然后开始执行同步代码,而这个时候很多线程都过来了,那么这些线程也要拷贝markword到线程栈中,然后cas修改lock record与mark word的相互指向,这个时候只有一个线程能够成功,其他线程都需要cas,如果线程1没有同步代码块没有指向完成,其他线程是没有办法自旋成功,那么就就那些锁膨胀,升级为重量级锁,重量级锁升级过后线程的阻塞是由内核进行处理的,所以性能较低。

GC标志

我们知道GC在每次进行的时候其实就是对对象的操作,对象的对象头中的markword进行操作,如果这个对象可以被GC了,那么GC会在在markword的锁状态设置为11,表示新一轮的gc开始了,而对象的age是最大15次,每次gc,age+1,如果达到了15次还存活就移植到老年代;所以这里有个问题就是如果我们的object对象升级为重量级锁了,那么是不是一直是重量级锁呢?我们知道锁的升级是不能降级的,也就是说轻量级锁不能降级为偏向锁,偏向锁不能降级为无锁,那如果说我们的锁升级为重量级锁了,过了很久都没有线程来访问,下一次线程来访问的时候还是重量级锁吗?不是的,JVM没有这么的傻,也就是说在很久没有线程访问的情况下会进行降级,但是降级是直接降级为无锁状态。

降级的目的和过程

因为基本对象锁的实现优先于重量级锁的使用,JVM会尝试在SWT的停顿中堆处于空闲状态重量级锁进行降级操作,这个降级过程是如何实现的呢?我们知道在STW时,所有的JAVA线程都将暂停在安全点SafePoint,此时VMThread通过对所有Monitor的遍历,或者通过对所有依赖于MonitorInUseLists值得当前正在使用中的Monitor子序列进行遍历从而得到哪些是未被使用的Monitor作为降级对象。
可以降级的Monitor对象
重量级锁的降级过程发生在STW阶段,降级对象就是哪些仅仅能被VMThread访问而没有被其他JavaThread访问的Monitor对象。

锁的优缺点

在这里插入图片描述

面试官:说一下Synchronized底层实现,锁升级的具体过程? 介绍 这是我去年7,8月份面试的时候被问的一个面试题,说实话被问到这个问题还是很意外的,感觉这个东西没啥用啊,直到后面被问了一波new Object,Integer对象等作为加锁对象行吗?会出现哪些问题?为啥java6后synchronized性能大幅上升?我彻底蒙蔽了。下面详细总结一下 synchronized使用方式 我们知道并发编程会产生各种问题的源头是可见性,原子性,有序性。 而synchronized能同时保证可见性,原子性,有序性。所以我们在解决并发问题的时候经常用synchronized,当. 阅读详情

相关推荐

synchronized 原理、使用、锁升级过程,写到我要吐血了

本文目录1.锁的分类2.synchronized 含义3.synchronized 三种加锁方式1.修饰实例方法2.修饰静态方法3 修饰代码块1.变量锁2.实例对象锁3.当前类的 Class 对象锁4.synchronized 底层原理1.Monitor 管程对象5.synchronized 锁优化升级过程   多线程编程中,会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可以是:对象、变量、文件等。 共享:资源可以由多个线程同时访问 可变:资源在其生命周期内可以被

扛麻袋的少年的博客 4887

Java中Synchronized锁升级机制详解

当第一个线程尝试获取`synchronized`锁时,对象头将被标记为偏向锁状态。这意味着只有当前线程可以访问该对象,而不需要额外的同步开销。

小电玩 2978

Linux文件操作四剑客

① /^(d{2}|d{4})-((0([1-9]{1}))|(1[1|2]))-(([0-2]([1-9]{1}))|(3[0|1]))$/ // 年-月-日 yyyy-MM-dd / yy-MM-dd 格式。⑥“^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$” //正浮点数。

wuds_158的博客 1611

Synchronized锁优化过程

Synchronized锁优化过程锁优化的升级方向:对象头(以32位虚拟机为例):普通对象头包含信息如下:数组对象:Mark Word 结构如下:重量级锁:多线程竞争锁的过程:轻量级锁轻量级锁加锁过程:锁膨胀过程:自旋锁:偏向锁: 锁优化的升级方向: 在介绍锁优化过程之前需要了解对象头的相关知识。 对象头(以32位虚拟机为例): 普通对象头包含信息如下: Mark Word:由于存储对象自身的运行时数据,包括哈希码、GC分代年龄、锁状态标志等; Klass Word:类型指针,虚拟机通过这个指针来确定

Z1532610063的博客 875

总结-NuerIPS2023-Direct Preference Optimization:Your Language Model is Secretly a Reward Model

对比PPO,DPO的泛化能力更强。

m0_60361484的博客 1608

Qwen3.5在昇腾910B上的vLLM Ascend高性能部署实战

大语言模型推理优化是AI工程落地的核心环节,其本质是计算、内存与通信三者的协同调度。vLLM作为主流推理框架,通过PagedAttention和Continuous Batching显著提升GPU利用率;而昇腾NPU凭借高带宽HBM与专用AI算子,在国产化算力平台中展现出独特优势。当vLLM深度适配昇腾(即vLLM Ascend),不仅保留动态批处理、流式输出等关键能力,更实现KV Cache的HBM感知管理与HCCL直通优化,使中小尺寸模型(如Qwen3.5-35B)在单台Atlas A3服务器上达成百并

weixin_30558697的博客 435

Java并发编程——Synchronized锁升级机制

Synchronized锁升级流程

BUPTZhanggg的博客 2002

深入解析EDID中Display Descriptor Block的配置技巧与实战应用

本文深入解析EDID中Display Descriptor Block的配置技巧与实战应用。通过剖析像素时钟、有效像素、消隐区与同步脉冲等核心参数,并结合1280x720@60Hz的实战配置案例,指导开发者如何正确为定制硬件或非标准显示器编写“身份证”,避免黑屏、分辨率异常等问题,确保显示设备完美点亮。

1a2s3d4f5g的博客 879

Java并发基础(4):Synchronized原理和锁优化升级过程

自旋、偏向锁、轻量级锁、重量级锁区别_pocher的博客-优快云博客 Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级_tongdanping的博客-优快云博客Java中Synchronized的用法_阳光日志-优快云博客_synchronized【Java并发编程之深入理解】Synchronized的使用_青苔小榭-优快云博客_synchronized 深入理解Java并发之synchronized实现原理_zejian的博客-优快云博客_synchro.

mingyuli的博客 2742

Android14到Android16源码移植中的Bluetooth模块规范适配指南

本文详细解析了从Android14到Android16源码移植过程中Bluetooth模块的规范适配挑战,重点介绍了Android.bp配置差异及常见编译报错解决方案。针对代码规范检查、final字段声明、静态方法优化等核心问题提供实用修复方法,帮助开发者高效完成版本升级适配工作。

fire9的博客 254

ros1 ubuntu22.04 安装教程

ROS1 ubuntu22.04 安装教程

autolabor的博客 9197

【java基础】Synchronized 锁升级

Synchronized 锁升级 Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级

sinat_28126573的博客 240

无人驾驶汽车系统入门(三十二)——FEC算法:点云快速欧几里得聚类实战解析

本文深入解析了FEC算法在无人驾驶点云聚类中的高效应用,通过逐点处理策略和体素栅格预分割技术,实现了比传统欧几里得聚类快百倍的速度。文章详细介绍了算法原理、Python实现及参数调优指南,并对比了FEC与传统算法的性能差异,为无人驾驶系统开发提供了实用解决方案。

weixin_29065015的博客 487

介绍一下synchronized锁升级过程

介绍一下synchronized锁升级过程

wy990880的博客 754

【人工智能】Open WebUI 安装指南 ( 基于Docker下载和使用Open WebUI 、通过本地运行项目源码进行搭建Open WebUI)

Open WebUI 是一个可扩展、功能丰富且用户友好的自托管 WebUI,旨在完全离线操作。它支持各种 LLM 运行程序,包括 Ollama 和 OpenAI 兼容的 API。社区:https://openwebui.com/

奶爸的编程之路,也就一周冷个三天 2万+

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

Synchronized 的锁升级过程介绍(无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁 )

Synchronized 的锁升级过程介绍(无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁 )

Java 领域技术分享 5104

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

synchronized锁的升级过程详细介绍,轻量级锁重量级锁偏向锁无锁

synchronized锁的升级过程详细介绍,轻量级锁重量级锁偏向锁无锁

java架构师分享,有问题可以联系我 1017

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

Java 中 synchronized 锁升级的过程

一个synchronized 同步代码块被线程首次进入的时候会获取偏向锁 锁对象就会进入“偏向模式” 在偏向模式下 锁对象就会偏向第一个线程(就像初恋一样)然后 如果说下一次有一个线程来访问如果是刚开始进来的那个线程的话(初恋线程)那么就不用加锁了(不用使用monitorentry、monitoreit )这样的话就节省了加锁的时间 ,如果要进来的不是初恋线程怎么办,要是这样的话“偏向锁”就会升级成轻量锁。大家稍微了解一下即可 主要是为了辅助理解“轻量锁“,接下来咱们开始说“轻量锁”。

qq_57070423的博客 1063

synchronized加锁原理以及锁升级过程

由于synchronized中用到了CAS技术,此处先对CAS做个简单介绍。是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败。CAS(V,A,B)操作中至少包含三个操作数——需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。在。

cxbtdd的博客 1787

synchronized锁升级过程和实现原理

一.synchronized升级过程 在最开始JDK1.0,1.2的时候,synchronized就是重量级锁,后来JDK对synchronized进行了一系列优化,这个优化就是有个升级过程。这个升级过程被markword清晰地进行了记录。 整个锁升级的过程大概分为: new-->偏向锁-->轻量级锁(无锁,自旋锁,自适应自旋)-->重量级锁 我们结合下面这张图具体谈一下 从刚刚new出来的时候,首先上的是偏向锁,出现争用,升级为轻量级锁,竞争状态变得特备激烈,变为重量级锁。

我唤来了星繁,似灭似绚烂 2770

synchronized的锁升级过程和详细的代码演示

在了解这个知识之前先了解Java对象的内容布局 一、synchronized的信息是存储在对象的markword中 Object o = new Object(); System.out.println(ClassLayout.parseInstance(o).toPrintable()); synchronized (o) { System.out.println(ClassLayout.parseInstance(o).toPrintable()); } 在没有加锁之前的

killerofjava的博客 805

synchronized锁、锁升级的过程

总共有四种状态:分别是无锁、偏向锁、轻量级锁和重量级锁。在默认的情况下,首先使用的是偏向锁。JVM会利用CAS操作,在对象头的Mark Word部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期会最多被一个线程锁定,使用偏向锁可以降低无竞争开销。如果有另外的线程尝试锁定某个已经被偏斜过的对象,JVM就需要撤销偏向锁,并切换到轻量级锁的实现。

lin1151211427的博客 748

30分钟彻底弄懂 synchronized 锁升级过程

些线程安全问题,这里面可能会产生一些预想不到的问题,所以下边我整理了一系列关于JDK中锁的问题,帮助大家更加深入地了解它们。

a1405的专栏 8309

synchronized原理_synchronized底层原理以及锁升级过程

点击蓝色“架构殿堂”关注我哟加个“星标”,干货推送!01什么是synchronized?synchronized是Java提供的一个并发控制的关键字,作用于对象上。主要有两种用法,分别是同步方法(访问对象和clss对象)和同步代码块(需要加入对象),保证了代码的原子性和可见性以及有序性,但是不会处理重排序以及代码优化的过程,但是在一个线程中执行肯定是有序的,因此是有序的。02synchr...

weixin_39788386的博客 183

浅析synchronized锁升级的原理与实现 1

浅析synchronized锁升级的原理与实现

水w的博客 1665

Synchronized锁、锁的四种状态、锁的升级(偏向锁,轻量级锁,重量级锁)

Synchronized【对象锁】采用互斤的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。synchronized方法和synchronized块。

m0_64422133的博客 3648
上一篇: Volatile内存屏障&指令重排
下一篇: 并发编程之线程中断&CAS
Life  journey
博客等级 码龄17年 1万+粉丝 80原创
评论 6
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值