深入浅出synchronized关键字

synchronized的作用

同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法完成的。

总结: 能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果

synchronized的几种使用方法

  • 对象锁

    1. 修饰静态方法

  • 类锁

    1. 修饰普通方法

    2. 修饰代码块

使用synchronized会碰到的几种情况

  • 多个线程同一时刻访问同一对象的同一普通同步方法

    此时线程之间时串行的关系

  • 多个线程同一时刻访问不同对象的同一普通同步方法

    此时线程之间时并行的关系

  • 同步方法执行过程中发生异常,同时会释放该锁对象

synchronized如何实现线程同步

反编译如下代码:javap -c ***.class

public class SyncDemo{
   int i;
   public  void reduce(){
synchronized(this){
i++;}
  }
}

 

我们可以看到,在反编译后的代码之中出现了monitorentermonitorexit两个指令。所以说synchronized的实现实际上就是对monitor对象的争夺。想要弄清楚monitor是如何实现的,我们先来看一段C++源码:

ObjectMonitor() {
   _header       = NULL;
   _count        = 0;
   _waiters      = 0,
   _recursions   = 0;
   _object       = NULL;
   _owner        = NULL;
   _WaitSet      = NULL;
   _WaitSetLock  = 0 ;
   _Responsible  = NULL ;
   _succ         = NULL ;
   _cxq          = NULL ;
   FreeNext      = NULL ;
   _EntryList    = NULL ;
   _SpinFreq     = 0 ;
   _SpinClock    = 0 ;
   OwnerIsThread = 0 ;
}

在上面的源码我们可以看到中有几个关键属性:

  • _owner:指向持有ObjectMonitor对象的线程

  • _WaitSet:存放处于wait状态的线程队列

  • _EntryList:存放处于等待锁block状态的线程队列

  • _recursions:锁的重入次数 【从这个不难看出synchronized是可重入锁吧】

  • _count:用来记录该线程获取锁的次数

 

所以,总结一下:当一个线程需要访问受保护的数据(即需要获取对象的Monitor)时

  1. 首先在EntryList中进行排序等待(当然,这里不是真正意义上的排序,因为monitor对象的获取不是公平的)

  2. 如果当前没有线程占有monitor对象,则该线程会和其他EntryList中的线程去抢占monitor对象

  3. 抢占成功,_count 加一,开始执行相关方法,若执行过程中调用了wait方法,则该线程会放弃获得到的monitor对象,并加入WaitSet中,等待notify/notifyall方法唤醒。

    注意:被唤醒的线程并不会立即获取到monitor对象,只回去重新加入EntryList与其他线程争夺锁对象

     

上面只说了synchronized修饰代码块时是如何实现的,其实当synchronized修饰方法时,实际上就是添加了一个标识符来表示此方法时同步方法,具体的实现方法和上面差不多。

有细心的小伙伴会发现上面的反编译代码之中出现了两个monitorexit关键字,这是为什么呢??

上面我们说到,同步方法执行过程中发生异常,同时会释放该锁对象,实际上就是因为这个关键字,作用是为了在执行过程中遇到异常能够及时释放锁对象

synchronized锁膨胀【锁升级】

这里我们先来讲一下在jvm队中对象的组成

  • 对象头

    1. 运行时数据

      • 哈希码

      • GC分代年龄

      • MarkWord【锁状态标志】

          1. 类型指针

          2. 数组长度【只有数组才会有】

        • 实例数据

          程序代码中所定义的各种类型的字段

        • 对其填充

          由于HotSpot的自动内存管理要求对象的起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍,对象头的数据正好是8的整数倍,所以当实例数据不够8字节整数倍时,需要通过对齐填充进行补全。

           

          synchronized锁升级主要是通过对象头中的markword来实现的:

          无锁 >> 偏向锁 >> 轻量级锁 >> 重量级锁

        •  

posted @ 2020-08-04 20:32  timelfb  阅读(222)  评论(0)    收藏  举报