Synchronized详解

151 阅读4分钟

这篇文章是结合网上的多篇博客以及jvm虚拟机这本书写的,仅供自己复习用

synchronized的三种使用方法

  • 修饰实例方法,进入代码同步块之前要先拿到对象的锁
  • 修饰static方法,进入代码同步块之前要先拿到该类的锁
  • 修饰代码块,指定要锁住的对象,进入同步块要拿到指定对象的锁

java对象头

 在java对象内存中可分为三块

  • 对象头
  • 实例变量
  • 填充数据
    对象头是用来存储对象的hashcode,持有锁,gc年代等信息。
    实例变量用来存储对象的属性方法以及父类的信息,如果是数组还包括数组长度。按四字节对其
    填充数据紧紧是用来字节对齐的。
    而对象持有锁的关键就在于对象头,对象头的结构如下
锁状态25bit4bit1bit2bit
对象hashcode对象分代年龄是否是偏向锁锁标志位(重量锁标志位为10)

moniter

分析synchronized对象锁时,锁标识为10时指向的是moniter对象的启示地址。每一个对象都有与之对应的moniter,moniter与对象的关系有很多种方式,但当一个moniter被某个线程持有了,moniter就会处于锁定状态

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

当多个线程同时访问一个对象时,首先会进入entrylist,当线程出来之后会拿到对象,此时_owner会变为线程的名字,count+1,如果调用wait()方法,会进入waitset集合中,会释放当前的moniter,count-1,_owner重置,同时该线程进入 WaitSet集合中等待被唤醒。
若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示image

java虚拟机对锁对象的优化

偏向锁

偏向锁适用于锁竞争不强的场合。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。
对于锁竞争不强的场景,偏向锁是合适的,但如果锁竞争强偏向锁可能会是负优化

轻量锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋锁

如果轻量锁失效,还有种拯救方式叫自旋锁。
当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

锁消失

java虚拟机在编译时会上下文扫描,如果发现锁没有必要的时候会执行锁消失,通过这种方法去消除没有必要的锁
如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

public class StringBufferRemoveSync {

    public void add(String str1, String str2) {
        //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
        //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }

    public static void main(String[] args) {
        StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
        for (int i = 0; i < 10000000; i++) {
            rmsync.add("abc", "123");
        }
    }

}