Synchronized详细介绍之锁升级过程

前言

我们在并发编程过程中,会有一些资源或者操作是必须要进行序列化访问的,就是线程之间不能并发的访问,必须要进行串行访问,所以就引入了锁的概念,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对象。

锁的优缺点

在这里插入图片描述

  • 11
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值