关于Synchronized的偏向锁,轻量级锁,重量级锁,锁升级过程,自旋优化,你该了解这些

目录

前言

synchronized的常见使用方式

修饰代码块(同步代码块)

修饰方法

synchronized不能继承?(插曲)

修饰静态方法

修饰类

Java对象 Mark Word

偏向锁

什么是偏向锁

偏向锁演示

偏向锁原理图解

优势

白话翻译

轻量锁

什么是轻量级锁

轻量级图解

优势

白话翻译

重量级锁

什么是重量级锁

重量级锁原理图解

Monitor源码分析

环境搭建

构造函数

锁竞争的过程

白话翻译

自旋优化

结语

参考资料


前言

毫无疑问,synchronized是我们用过的第一个并发关键字,很多博文都在讲解这个技术。不过大多数讲解还停留在对synchronized的使用层面,其底层的很多原理和优化,很多人可能并不知晓。因此本文将通过对synchronized的大量C源码分析,让大家对他的了解更加透彻点。

本篇将从为什么要引入synchronized,常见的使用方式,存在的问题以及优化部分这四个方面描述,话不多说,开始表演。

synchronized的常见使用方式

修饰代码块(同步代码块)

synchronized (object) {
      //具体代码
}

修饰方法

synchronized void test(){
  //具体代码
}

 

synchronized不能继承?(插曲)

父类A:

public class A {
    synchronized void test() throws Exception {
        try {
            System.out.println("main 下一步 sleep begin threadName="
                    + Thread.currentThread().getName() + " time="
                    + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("main 下一步 sleep end threadName="
                    + Thread.currentThread().getName() + " time="
                    + System.currentTimeMillis());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

子类B:(未重写test方法)

public class B extends A {

}

子类C:(重写test方法)

public class C extends A {

    @Override
     void test() throws Exception{
        try {
            System.out.println("sub 下一步 sleep begin threadName="
                    + Thread.currentThread().getName() + " time="
                    + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("sub 下一步 sleep end threadName="
                    + Thread.currentThread().getName() + " time="
                    + System.currentTimeMillis());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
} 

 

线程A:

public class ThreadA extends Thread {
    private A a;

    public void setter  (A a) {
        this.a = a;
    }

    @Override
    public void run() {
        try{
            a.test();
        }catch (Exception e){

        }
    }
}

线程B:

public class ThreadB extends Thread {
    private B b;
    public void setB(B b){
        this.b=b;
    }

    @Override
    public void run() {
        try{
            b.test();
        }catch (Exception e){

        }
    }
} 

线程C:

public class ThreadC extends Thread{
    private C c;
    public void setC(C c){
        this.c=c;
    }

    @Override
    public void run() {
        try{
            c.test();
        }catch (Exception e){

        }
    }
}

测试类test:

public class test {
    public static void main(String[] args) throws Exception {
        A a = new A();
        ThreadA A1 = new ThreadA();
        A1.setter(a);
        A1.setName("A1");
        A1.start();
        ThreadA A2 = new ThreadA();
        A2.setter(a);
        A2.setName("A2");
        A2.start();
        A1.join();
        A2.join();

        System.out.println("=============");
        B b = new B();
        ThreadB B1 = new ThreadB();
        B1.setB(b);
        B1.setName("B1");
        B1.start();
        ThreadB B2 = new ThreadB();
        B2.setB(b);
        B2.setName("B2");
        B2.start();
        B1.join();
        B2.join();
        System.out.println("=============");

        C c = new C();
        ThreadC C1 = new ThreadC();
        C1.setName("C1");
        C1.setC(c);
        C1.start();
        ThreadC C2 = new ThreadC();
        C2.setName("C2");
        C2.setC(c);
        C2.start();
        C1.join();
        C2.join();
    }
}

运行结果:

子类B继承了父类A,但是没有重写test方法,ThreadB仍然是同步的。子类C继承了父类A,也重写了test方法,但是未明确写上synchronized,所以这个方法并不是同步方法。只有显式的写上synchronized关键字,才是同步方法。

所以synchronized不能继承这句话有歧义,我们只要记住子类如果想要重写父类的同步方法,synchronized关键字一定要显示写出,否则无效。

修饰静态方法

synchronized static void test(){
   //具体代码
}

修饰类

synchronized (Example2.class) {
    //具体代码
 }

 

Java对象 Mark Word

在JVM中,对象在内存中的布局分为三块区域:对象头,实例数据和对齐数据,如下图:

其中Mark Word值在不同锁状态下的展示如下:(重点看线程id,是否为偏向锁,锁标志位信息)

在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节。Talk is cheap. Show me the code. 咱来看代码。

  •  我们想要看Java对象的Mark Word,先要加载一个jar包,在pom.xml添加即可。
<dependency>
	<groupId>org.openjdk.jol</groupId>
	<artifactId>jol-core</artifactId>
	<version>0.9</version>
</dependency>
  • 新建一个对象A,拥有初始值为666的变量x。
public class A {
    private int x=666;
}
  • 新建一个测试类test,这涉及到刚才加载的jar,我们打印Java对象。

import org.openjdk.jol.info.ClassLayout;

public class test {
    public static void main(String[] args) {
        A a=new A();
        System.out.println( ClassLayout.parseInstance(a).toPrintable());
    }
}
  • 我们发现对象头(object header)占了12个字节,为啥和上面说的16个字节不一样。

  • 其实上是默认开启了指针压缩,我们需要关闭指针压缩,也就是添加-XX:-UseCompressedOops配置。

  • 再次执行,发现对象头为16个字节。

偏向锁

什么是偏向锁

JDK1.6之前锁为重量级锁(待会说,只要知道他和内核交互,消耗资源),1.6之后Java设计人员发现很多情况下并不存在多个线程竞争的关系,所以为了资源问题引入了无锁偏向锁轻量级锁重量级锁的概念。先说偏向锁,他是偏心,偏袒的意思,这个锁会偏向于第一个获取他的线程。

偏向锁演示

  • 创建并启动一个线程,run方法里面用了synchronized关键字,功能是打印this的Java对象。
public class test {
    public static void main(String[] args) {
         Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (this){
                    System.out.println(ClassLayout.parseInstance(this).toPrintable());
                }
            }
        });
        thread.start();
    }
}

标红的地方为000,根据之前Mark Word在不同状态下的标志,得此为无锁状态。理论上一个线程使用synchronized关键字,应为偏向锁。

  • 实际上偏向锁在JDK1.6之后是默认开启的,但是启动时间有延迟,所以需要添加参数-XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动。

  • 重新运行下代码,发现标红地方101,对比Mark Word在不同状态下的标志,得此状态为偏向锁。

偏向锁原理图解

  • 在线程的run方法中,刚执行到synchronized,会判断当前对象是否为偏向锁和锁标志,没有任何线程执行该对象,我们可以看到是否为偏向锁为0,锁标志位01,即无锁状态。

  • 线程会将自己的id赋值给markword,即将原来的hashcode值改为线程id,是否是偏向锁改为1,表示线程拥有对象锁,可以执行下面的业务逻辑。如果synchronized执行完,对象还是偏向锁状态;如果线程结束之后,会撤销偏向锁,将该对象还原成无锁状态。

  • 如果同一个线程中又对该对象进行加锁操作,我们只要对比对象的线程id是否与线程id相同,如果相同即为线程锁重入问题。

优势

加锁和解锁不需要额外的消耗,和执行非同步方法相比只有纳秒级的差距。

白话翻译

线程1锁定对象this,他发现对象为无锁状态,所以将线程id赋值给对象的Mark Word字段,表示对象为线程1专用,即使他退出了同步代码,其他线程也不能使用该对象。

同学A去自习教室C,他发现教室无人,所以在门口写了个名字,表示当前教室有人在使用,这样即使他出去吃了饭,其他同学也不能使用这个房间。

轻量锁

什么是轻量级锁

在多线程交替同步代码块的情况下,线程间没有竞争,使用轻量级锁可以避免重量级锁引入的性能消耗。

轻量级图解

  • 在刚才偏向锁的基础上,如果有另外一个线程也想错峰使用该资源,通过对比线程id是否相同,Java内存会立刻撤销偏向锁(需要等待全局安全点),进行锁升级的操作。

  • 撤销完轻量级锁,会在线程1的方法栈中新增一个锁记录,对象的Mark Word与锁记录交换。

优势

竞争的线程不会阻塞,提高了程序的响应速度。

白话翻译

在刚才偏向锁的基础上,另外一个线程也想要获取资源,所以线程1需要撤销偏向锁,升级为轻量锁。

同学A在使用自习教室外面写了自己的名字,所以同学B来也想要使用自习教室,他需要提醒同学A,不能使用重量级锁,同学A将自习教室门口的名字擦掉,换成了一个书包,里面是自己的书籍。这样在同学A不使用自习教室的时候,同学B也能使用自习教室,只需要将自己的书包也挂在外面即可。这样下次来使用的同学就能知道已经有人占用了该教室。

重量级锁

什么是重量级锁

当多线程之间发生竞争,Java内存会申请一个Monitor对象来实现。

重量级锁原理图解

在刚才的轻量级锁的基础上,线程2也想要申请资源,发现锁的标志位为00,即为轻量级锁,所以向内存申请一个Monitor,让对象的MarkWord指向Monitor地址,并将ower指针指向线程1的地址,线程2放在等待队列里面,等线程1指向完毕,释放锁资源。

Monitor源码分析

环境搭建

我们去官网http://openjdk.java.net/找下open源码,也可以通过其他途径下载。源码是C实现的,可以通过DEV C++工具打开,效果如下图:

构造函数

我们先看下\hotspot\src\share\vm\runtime\ObjectMonitor.hpp,以.hpp结尾的文件是导入的一些包和一些声明,之后可以被.cpp文件导入。

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;//线程重入次数 
    _object       = NULL;//存储该monitor的对象 
    _owner        = NULL;//标识拥有该monitor的线程 
    _WaitSet      = NULL;//处于wait状态的线程,会加入到_waitSet 
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;//多线程竞争锁时的单项列表 
    FreeNext      = NULL ;
    _EntryList    = NULL ;//处于等待锁lock状态的线程,会被加入到该列表 
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

 

锁竞争的过程

我们先看下\hotspot\src\share\vm\interpreter\interpreterRuntime.cppIRT_ENTRY_NO_ASYNC即为锁竞争过程。


//%note monitor_1
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) { //如果可以使用偏向锁,即进入fast_enter
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {//如果不可以使用偏向锁,即进行slow_enter
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
         "must be NULL or an object");
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

 

 

slow_enter实际上调用的ObjectMonitor.cpp的enter 方法

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 ;
  }
  //如果_owner等于当前线程,重入数_recursions加1,直接返回
  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 ;
  }

 //如果未抢到锁,则进行自旋优化,如果还未获取锁,则放入到list里面
  // We've encountered genuine contention.
  assert (Self->_Stalled == 0, "invariant") ;
  Self->_Stalled = intptr_t(this) ;

  // Try one round of spinning *before* enqueueing Self
  // and before going through the awkward and expensive state
  // transitions.  The following spin is strictly optional ...
  // Note that if we acquire the monitor from an initial spin
  // we forgo posting JVMTI events and firing DTRACE probes.
  if (Knob_SpinEarly && TrySpin (Self) > 0) {
     assert (_owner == Self      , "invariant") ;
     assert (_recursions == 0    , "invariant") ;
     assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
     Self->_Stalled = 0 ;
     return ;
  }

  assert (_owner != Self          , "invariant") ;
  assert (_succ  != Self          , "invariant") ;
  assert (Self->is_Java_thread()  , "invariant") ;
  JavaThread * jt = (JavaThread *) Self ;
  assert (!SafepointSynchronize::is_at_safepoint(), "invariant") ;
  assert (jt->thread_state() != _thread_blocked   , "invariant") ;
  assert (this->object() != NULL  , "invariant") ;
  assert (_count >= 0, "invariant") ;

  // Prevent deflation at STW-time.  See deflate_idle_monitors() and is_busy().
  // Ensure the object-monitor relationship remains stable while there's contention.
  Atomic::inc_ptr(&_count);

  EventJavaMonitorEnter event;

  { // Change java thread status to indicate blocked on monitor enter.
    JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);

    DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt);
    if (JvmtiExport::should_post_monitor_contended_enter()) {
      JvmtiExport::post_monitor_contended_enter(jt, this);
    }

    OSThreadContendState osts(Self->osthread());
    ThreadBlockInVM tbivm(jt);

    Self->set_current_pending_monitor(this);

    // TODO-FIXME: change the following for(;;) loop to straight-line code.
    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);
  }

  Atomic::dec_ptr(&_count);
  assert (_count >= 0, "invariant") ;
  Self->_Stalled = 0 ;

  // Must either set _recursions = 0 or ASSERT _recursions == 0.
  assert (_recursions == 0     , "invariant") ;
  assert (_owner == Self       , "invariant") ;
  assert (_succ  != Self       , "invariant") ;
  assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;

  // The thread -- now the owner -- is back in vm mode.
  // Report the glorious news via TI,DTrace and jvmstat.
  // The probe effect is non-trivial.  All the reportage occurs
  // while we hold the monitor, increasing the length of the critical
  // section.  Amdahl's parallel speedup law comes vividly into play.
  //
  // Another option might be to aggregate the events (thread local or
  // per-monitor aggregation) and defer reporting until a more opportune
  // time -- such as next time some thread encounters contention but has
  // yet to acquire the lock.  While spinning that thread could
  // spinning we could increment JVMStat counters, etc.

  DTRACE_MONITOR_PROBE(contended__entered, this, object(), jt);
  if (JvmtiExport::should_post_monitor_contended_entered()) {
    JvmtiExport::post_monitor_contended_entered(jt, this);
  }

  if (event.should_commit()) {
    event.set_klass(((oop)this->object())->klass());
    event.set_previousOwner((TYPE_JAVALANGTHREAD)_previous_owner_tid);
    event.set_address((TYPE_ADDRESS)(uintptr_t)(this->object_addr()));
    event.commit();
  }

  if (ObjectMonitor::_sync_ContendedLockAttempts != NULL) {
     ObjectMonitor::_sync_ContendedLockAttempts->inc() ;
  }
} 

 

白话翻译

同学A在使用自习教室的时候,同学B在同一时刻也想使用自习教室,那就发生了竞争关系。所以同学B在A运行过程中,加入等待队列。如果此时同学C也要使用该教室,也会加入等待队列。等同学A使用结束,同学B和C将竞争自习教室。

自旋优化

自旋优化比较简单,如果将其他线程加入等待队列,那之后唤醒并运行线程需要消耗资源,所以设计人员让其空转一会,看看线程能不能一会结束了,这样就不要在加入等待队列。

白话来说,如果同学A在使用自习教室,同学B可以回宿舍,等A使用结束再来,但是B回宿舍再来的过程需要1个小时,而A只要10分钟就结束了。所以B可以先不回宿舍,而是在门口等个10分钟,以防止来回时间的浪费。

结语

唉呀妈呀,终于结束了,累死了。终于将synchronized写完了,如果有不正确的地方,还需要各位指正。如果觉得写得还行,麻烦帮我点赞,评论哈。

参考资料

Java中System.out.println()为何会影响内存可见性

别再问什么是Java内存模型了,看这里!

JVM---汇编指令集

Java中Synchronized的使用

synchronized同步方法(08)同步不具有继承性

Thread--synchronized不能被继承?!?!!!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章