面试——Synchronized

面试——Synchronized

如何解决线程并发安全问题

在多个线程操作同一共享变量时,在对临界资源操作时,容易出现线程安全问题。因此需要同步机制来解决线程安全问题。

CAS乐观锁机制相同,Synchronized也能实现上锁,但Synchronized实现的是悲观锁

Synchronized也称为内置锁隐式锁,因为其加锁的方式很Lock不同,用了隐式上锁的方式。

学习Synchronized,我们重点关注以下几点:

  • Synchronized在jdk1.6版本之前性能较差,1.6及之后使用了锁的膨胀升级
  • Synchronized的底层实现逻辑

Synchronized应用场景

Synchronized一般用在以下这几种场景:

  • 修饰实例方法,对当前**实例对象(this)**加锁
    public synchronized void lockMethod(){
        System.out.println("lock method");
    }
  • 修饰静态方法,对当前**类对象(Class对象)**加锁
    public static synchronized void lockStaticMethod(){
        System.out.println("lock static method");
    }
  • 修饰代码块,指定对某个对象进行加锁
    public void lockObject(){
        synchronized (object){
            System.out.println("lock object");
        }
    }

根据锁的粒度来选择使用哪一种

  • 比如使用静态方法上锁,锁的粒度是整个Class对象,如果大量线程都在使用Class对象作为锁对象,那么锁的粒度很大。
  • 比如System.out.println()这种方式底层是对PrintStream上锁,但PrintStream又是单例的,因此在代码中如果大量使用了System.out.println(),性能会受影响。
    /**
     * Prints a String and then terminate the line.  This method behaves as
     * though it invokes <code>{@link #print(String)}</code> and then
     * <code>{@link #println()}</code>.
     *
     * @param x  The <code>String</code> to be printed.
     */
    public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

Synchronized锁的膨胀升级过程

Synchronized在1.6版本之前性能较差,在并发不严重的情况下,因为Synchronized依然对象上锁,每个对象需要维护一个Monitor管程对象,管程对象需要维护一个Mutex互斥量对象。Mutex是由操作系统内部的pthread线程库维护的。上锁需要通过JVM从用户态切换到内核态来调用底层操作系统的指令,这样操作的性能较差。

AQS框架中的ReentrantLock锁通过Java语言编写,实现了可重入锁和公平锁,且性能比Synchronized要好太多

JDK1.6版本为了弥补Synchronized的性能缺陷,设计了Synchronized锁的膨胀升级。也就是根据当前线程的竞争激烈程度,设计了不同效果的锁。

对象头

在对象的创建的过程中,涉及到以下过程:
在这里插入图片描述

其中为对象设置对象头信息,对象头信息包含以下内容:类元信息、对象哈希码、对象年龄、锁状态标志等。其中锁状态标志,就是当前对象属于哪一种锁。

  • 对象头中的Mark Word 字段(32位)

在这里插入图片描述

  • 对象头中的类型指针(Klass Pointer)

类型指针用于指向元空间当前类的类元信息。比如调用类中的方法,通过类型指针找到元空间中的该类,再找到相应的方法。

开启指针压缩后,类型指针只用4个字节存储,否则需要8个字节存储。

膨胀升级

  • 无锁状态:当对象锁被创建出来时,在线程获得该对象锁之前,对象处于无锁状态。
  • 偏向锁:在大多数情况下,不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。

偏向锁的核心思想是,⼀旦有线程持有了这个对象,标志位修改为1,就进⼊偏向模式,同时会把这个线程的ID记录在对象的Mark Word中。当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

  • 轻量级锁:对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

  • 重量级锁:通过自旋,让线程在等待时不会被挂起。自旋次数默认是10次,可以通过-XX:PreBlockSpin进行修改。如果自旋失败到达阈值,则将升级为重量级锁。
    在这里插入图片描述

  • 什么是轻量级锁

  1. 如果对象是无锁的,JVM会在当前线程的栈帧中建立一个Lock Record(锁记录)的空间,用来存放对象的Mark Work拷贝,然后把Lock Record中的owner属性指向当前对象
  2. 接下来JVM会利用CAS尝试把对象原本的Mark Word更新回Lock Record的指针成功就说明加锁成功,于是改变锁标志位,执行相关同步操作
  3. 如果失败了,判断当前对象的Mark Word是否指向当前线程的栈帧,如果是就表示当前线程已经持有该对象锁。如果不是,说明当前对象锁被其他线程持有,于是进行自旋在这里插入图片描述
  • 什么是自旋锁
  1. 线程通过不断的自旋尝试上锁
  2. 为什么要自旋?因为如果线程被频繁挂起,也就意味着系统在用户态和内核态之间频繁的切换。——我们所有的程序都在⽤户空间运⾏,进⼊⽤户运⾏状态也就是(⽤户态),但是很多操作可能涉及内核运⾏,⽐如I/O,我们就会进⼊内核运⾏状态(内核态)。
  3. 通过自旋,让线程在等待时不会被挂起。自旋次数默认是10次,可以通过-XX:PreBlockSpin进行修改。如果自旋失败到达阈值,则将升级为重量级锁。

注意,锁的膨胀升级只能升不能降,也就是说升级过程不可逆

Synchronized的底层实现逻辑

同步代码块的上锁逻辑

先来看一个Java例子:

package com.qf.intro;

 
public class LockOnObjectDemo {

    public static Object object = new Object();

    private Integer count = 10;

    public void decrCount(){
        synchronized (object){
            --count;
            if(count <= 0){
                System.out.println("count小于0");
                return;
            }
        }
    }
}

使用javap -c LockOnObjectDemo.class命令来看其中的信息:

  public void decrCount();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: getstatic     #4                  // Field object:Ljava/lang/Object;
         3: dup
         4: astore_1
         5: monitorenter
         6: aload_0
         7: aload_0
         8: getfield      #3                  // Field count:Ljava/lang/Integer;
        11: invokevirtual #5                  // Method java/lang/Integer.intValue:()I
        14: iconst_1
        15: isub
        16: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        19: putfield      #3                  // Field count:Ljava/lang/Integer;
        22: aload_0
        23: getfield      #3                  // Field count:Ljava/lang/Integer;
        26: invokevirtual #5                  // Method java/lang/Integer.intValue:()I
        29: ifgt          43
        32: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        35: ldc           #7                  // String count小于0
        37: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        40: aload_1
        41: monitorexit
        42: return
        43: aload_1
        44: monitorexit
        45: goto          53
        48: astore_2
        49: aload_1
        50: monitorexit
        51: aload_2
        52: athrow
        53: return
      Exception table:
         from    to  target type
             6    42    48   any
            43    45    48   any
            48    51    48   any
      LineNumberTable:
        line 14: 0
        line 15: 6
        line 16: 22
        line 17: 32
        line 18: 40
        line 20: 43
        line 21: 53
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      54     0  this   Lcom/qf/intro/LockOnObjectDemo;
      StackMapTable: number_of_entries = 3
        frame_type = 252 /* append */
          offset_delta = 43
          locals = [ class java/lang/Object ]
        frame_type = 68 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #9                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: putstatic     #4                  // Field object:Ljava/lang/Object;
        10: return
      LineNumberTable:
        line 9: 0
}
SourceFile: "LockOnObjectDemo.java"
  • Synchronized内置锁是一种对象锁,作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。
    具体的实现逻辑是通过**内部对象Monitor(监视器锁)**来实现。
  • 监视器锁的实现依赖底层操作系统的Mutex Lock(互斥锁)实现。互斥锁是一个重量级锁,且性能较低。
  • Synchronized关键字被编译成字节码后,会被翻译成monitorentermonitorexit两条指令。这两条指令中间的代码会被上锁。

Monitor监视器锁

任何一个对象都有一个Monitor与之关联,当对象的Monitor被持有后,该对象处于被锁定状态。具体过程如下:

  • 当我们进⼊⼀个⽅法的时候,执⾏monitorenter,就会获取当前对象的⼀个所有权,这个时候monitor进⼊数为1,当前的这个线程就是这个monitor的owner。
  • 如果你已经是这个monitor的owner了,你再次进⼊,就会把进⼊数**+1**.
  • 当执⾏完monitorexit,对应的进⼊数就**-1**,直到为0,才可以被其他线程持有。

所有的互斥,其实在这⾥,就是看你能否获得monitor的所有权,⼀旦你成为owner,就是获得锁者

在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现):

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状态的线程,加入到_EntryList
  _SpinFreq = 0 ;
  _SpinClock = 0 ;
  OwnerIsThread = 0 ;
}

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

  • 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1
  • 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为nullcount自减1,同时该线程进入WaitSet集合中等待被唤醒
  • 若当前线程执行完毕,也将释放monitor(锁)复位count的值,以便其他线程进入获取monitor(锁);

同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。

同步方法的上锁逻辑

先看这个例子:

package com.qf.intro;

public class LockOnMethodDemo {
    public static Object object = new Object();

    private Integer count = 10;

    public synchronized void decrCount() {
        --count;
        if (count <= 0) {
            System.out.println("count小于0");
            return;
        }

    }
}

查看代码指令后:

  public synchronized void decrCount();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: aload_0
         2: getfield      #3                  // Field count:Ljava/lang/Integer;
         5: invokevirtual #4                  // Method java/lang/Integer.intValue:()I
         8: iconst_1
         9: isub
        10: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        13: putfield      #3                  // Field count:Ljava/lang/Integer;
        16: aload_0
        17: getfield      #3                  // Field count:Ljava/lang/Integer;
        20: invokevirtual #4                  // Method java/lang/Integer.intValue:()I
        23: ifgt          35
        26: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        29: ldc           #6                  // String count小于0
        31: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        34: return
        35: return
      LineNumberTable:
        line 13: 0
        line 14: 16
        line 15: 26
        line 16: 34
        line 19: 35
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      36     0  this   Lcom/qf/intro/LockOnMethodDemo;
      StackMapTable: number_of_entries = 1
        frame_type = 35 /* same */

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #8                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: putstatic     #9                  // Field object:Ljava/lang/Object;
        10: return
      LineNumberTable:
        line 8: 0
}

在同步方法里有一个标志位ACC_SYNCHRONIZED

同步⽅法的时候,⼀旦执⾏到这个⽅法,就会先判断是否有标志位,然后,ACC_SYNCHRONIZED会去隐式调⽤刚才的两个指令:monitorenter和monitorexit。所以归根究底,还是monitor对象的争夺

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值