synchronized

线程安全是并发编程中重要的一部分,在Java语言中,有一个关键字——synchronized可以保证线程的安全,它可以保证在同一时刻只有一个线程执行某方法,还可以让一个线程中的共享数据的变化被其他线程所看到。

synchronized的应用

1、修饰实例方法

作用于当前实例加锁,进入同步代码前要获得当前实例的锁

synchronized void method(){
    // 业务代码
}

2、修饰静态方法

给当前类加锁,会作用于类的所有对象实例,进入同步代码前需要获得当前class的锁。因为静态成员不属于任何一个实例对象,是类成员。所以,如果一个线程A调用实例对象的静态synchronized方法,而线程B需要调用这个实例对象所属类的非静态synchronized方法是允许的,不会发生互斥现象。

synchronized static void method(){
    // 业务代码
}

3、修饰代码块

指定加锁对象,对给定对象/类加锁。synchronized( this / object )表示进入同步代码块前要获得给定对象的锁。synchronized(类.class)表示进入同步代码块前要获得当前class的锁。

synchronized(this/oject/类.class){
    // 业务代码
}

sychronized原理

sychronized是Java提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁。使用synchronized之后,在编译之后同步代码块前后会加上monitorentermonitorexit字节码指令,它依赖操作系统底层互斥锁实现,主要作用是实现原子性操作和解决共享变量的内存可见性问题。

执行monitorenter指令时会尝试获取对象锁,锁的计数器+1,此时其他竞争锁的线程则会进入等待队列中。执行monitorexit指令时则会把计数器-1,当计数器值为0时,释放锁,处于等待队列中的线程继续竞争锁。

synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或唤醒时会从用户态切换到内核态,这种转换非常消耗性能。从内存语义来说,加锁的过程会清除工作内存中的共享变量,再从主内存读取,而释放锁的过程则是将工作内存中的共享变量写回主内存。

深入源码来说,synchronized实际上有两个队列waitSetentryList。

1.当多个线程进入同步代码块时,首先进入entryList。

2.有一个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1。

3.如果线程调用wait方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryLisy竞争锁。

4.如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null。





Java对象头

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充数据。

实例数据:存放类的属性数据信息,包括父类的属性信息,如果时数组,实例部分还包括数组长度,这部分内存按4字节对齐。

填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅为了字节对齐。

Java对象头实现synchronized的锁对象的基础,synchronized使用的锁对象是存储在Java对象头里的,采用2个机器码来存储对象头(如果对象是数组则会分配3个机器码,多出来的1个机器码记录的是数组长度),其主要结构如下:

虚拟机位数对象头结构说明
32/64bitMark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息
32/64bitClass Metadata Address类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例

Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等,32位JVM的Mark Word默认存储结构如下:

锁状态25bit4bit1bit 是否是偏向锁2bit 锁标志位
无锁状态对象HashCode对象分代年龄001

synchronized对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都有一个monitor与其关联,monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它就处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下:

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 ;
  }

ObjectMonitor中有两个队列,_WaitSet _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,

1.进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count+1。

2.若线程调用wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count-1,同时该线程进入 WaitSet集合中等待被唤醒。

3.若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。

synchronized修饰代码块

定义一个synchronized修饰的同步代码块,在代码块中操作共享变量i:

public class Test{

   public int i;

   public void method(){
       synchronized (this){
           i++;
       }
   }
}

编译上述代码并使用javap反编译后得到字节码如下:

3: monitorenter  //进入同步方法
//......  
15: monitorexit   //退出同步方法
16: goto          24
//......
21: monitorexit //退出同步方法

根据字节码中可知使用的是monitorentermonitorexit指令实现同步语句块,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得monitor,并将计数器值设置为1,取锁成功。如果当前线程已经拥有objectref的monitor的持有权,那它可以重入这个 monitor,重入时计数器的值也会加1。若其他线程已经拥有objectref的monitor的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放monitor(锁)并设置计数器值为0,其他线程将有机会持有monitor 。

编译器将会确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都有执行其对应monitorexit 指令,无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

synchronized修饰方法

方法级的同步不需要通过字节码指令来控制,它实现在方法调用和返回操作之中,JVM根据方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED访问标志区分一个方法是否是同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor, 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

public class Test{

   public int i;

   public synchronized void method(){
           i++;
   }
}

javap反编译后的字节码:



public synchronized void method();
    descriptor: ()V
    //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED代表该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10


synchronized修饰的方法没有monitorenter指令和monitorexit指令,取得代之的是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值