深入理解Synchronized关键字

12 篇文章 0 订阅
7 篇文章 0 订阅

同步器的意义

临界资源:多个线程同时访问同一个共享,可变的资源;
临界资源可以是对象,变量,文件等;

引出的问题:
多个线程对共享资源的读写,写写问题

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

实际上,所有并发模式在解决线程安全问题时,都是采用序列化访问临界资源。
即同一时刻,只能有一个线程访问临界资源
,也称同步互斥访问;

Java中,实现同步互斥访问的方式

  • synchronized
  • lock

同步器的本质就是加锁;
加锁的目的:序列化访问临界资源;
多个线程执行同一个方法时,方法内定义的局部变量是线程安全,每个线程的栈中都会创建一个自己的局部变量,属于私有的。

synchronized原理详解

  • synchronized内置锁是一种对象锁。
    锁的是对象而非引用;
  • 也是一种可重入锁。
    在一个线程持有该锁时,可以访问该锁锁住的同步代码;
  • 也是一种隐式锁。
    是一个关键字,加锁解锁都由底层JVM帮我们完成;

加锁的方式:

  • 修饰实例方法,锁的是当前实例对象;
  • 修饰静态方法,锁的是当前类对象( XXX.class);
  • 修饰代码块,锁的是括号里面的对象;

synchronized底层原理

  • synchronized是基于JVM内置锁实现的,通过内部对象Monitor(监视器锁)实现。

  • 同步方法和代码块都是基于进入和退出的Monitor对象实现的。

  • Monitor的实现依赖与底层操作系统的Mutex Lock(互斥锁)实现,是一个重量级锁,性能低。
    在这里插入图片描述

  • synchronized代码编译成字节码后,会被翻译为monitorenter和monitorexit两条指令;
    在这里插入图片描述

  • 每个同步对象都有一个自己的Monitor,加锁过程如下图
    在这里插入图片描述

Monitor监视器锁

任何一个对象都有一个Monitor与之关联当且一个Monitor被持有后,他将处于锁定状态;
Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。

  1. monitorenter
    线程执行该指令会尝试获取monitor的所有权;
    1.1:
    如果monitor的进入数为0,则该线程可以获取到monitor,然后将counter进入数设置为1,该线程即为monitor的所有者。
    1.2:
    如果当前线程已经占有了该monitor,重新进入,则monitor的进入数+1;这种情况为可重入锁;
    1.3:
    如果monitor的进入不为0,且占有该monitor的线程不是自己,则线程进入阻塞队列或者CAS循环尝试获取锁,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
  • monitorexit
    执行该指令的线程必须是持有monitor的所有者。
    指令执行时,monitor的进入数-1,如果-1后为0,那线程退出monitor,不再是monitor的所有者。其他被这个monitor阻塞的线程会尝试获取这个monitor的所有权;

举例

同步方法

package it.yg.juc.sync;
public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}

字节码
在这里插入图片描述

  • 好像没有通过指令 monitorenter 和 monitorexit 来完成(理论上其实也可以通过这两条指令来实现),其实常量池中flags多了 ACC_SYNCHRONIZED 标示符。
  • JVM通过该标识符实现方法同步的。
  • 当方法调用时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果被设置了,线程将会获取monitor,获取成功之后,才能执行方法体;在方法执行期间,其他任何线程无法再获得同一个monitor对象

注意:

  • monitorenter和monitorexit的执行是JVM通过调用操作系统原语mutex来实现的,被阻塞的线程将会被挂起,等待重新调度。
  • 由于调用操作系统原语需要切换到内核态,会影响性能;

什么是monitor?

  • 可以理解为一个同步工具,也可以描述为 一种同步机制,它通常被描述为一个对象。
  • 所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象带了一把看不见的锁,它叫做内部锁或者Monitor锁。
  • 也就是Synchronized的对象锁,MarkWord锁标识位为10重量级锁),其中指针指向的是Monitor对象的起始地址。

ObjectMonitor

在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的对象列表,每个等待monitor的线程都会被封装成ObjectWaiter对象,_owner指向持有锁的线程,当多个线程访问一段同步代码:

  1. 首先进入EntryList集合,当线程获取到对象的Monitor后,将_owner变量设置为当前线程,同时count计数器值+1;
  2. 若线程调用wait()方法,将释放持有的monitor,owner的值变为null,count计数值-1;
  3. 若当前线程执行完毕,也将释放Monitor并复位count的值,以便其他线程进入获取monitor;

同时

  • monitor对象存在于每个Java对象的对象头Mark Word中(存储指针的指向),Synchronized就是通过这种方式获取锁的。
  • notify/wait/notifyAll会使用到Monitor对象,所以必须在同步代码块中使用。

对象记录锁状态

对象是如何记录锁状态的呢?答案是锁状态是被记录在每个对象的对象头(Mark Word)中。
对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

对象头

包括hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等。Java对象头一般占有2个机器码等;如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
在这里插入图片描述
32位虚拟机对象头结构图如下:
在这里插入图片描述
现在我们虚拟机基本是64位的,而64位的对象头有点浪费空间,JVM默认会开启指针压缩,所以基本上也是按32位的形式记录对象头的。
在这里插入图片描述

锁的膨胀升级过程

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。在这里插入图片描述

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了减少同一线程获取锁的代码,引入偏向锁;
核心思想:如果一个线程获得了锁,锁进入偏向锁状态,Mark Word结构变为偏向锁结果,当这个线程再次请求锁时,无需再做任何获取锁的过程,这样省去了申请锁的操作,提高了程序性能。

场景:对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。
如果锁竞争激烈,偏向锁失效,每次申请锁的线程不相同,这种场合不适合用偏向锁。

轻量级锁

偏向锁失效后,会升级为轻量级锁。Mark Word改为轻量级锁的结构。
提升性能的依据:对绝大多数的锁,在整个同步期间不存在竞争。
场景:线程交替执行同步代码的场合。

如果存在同一时间访问同一锁的情况,轻量级锁失效,升级为重量级锁。

自旋锁

申请轻量级锁失败后,JVM避免线程被操作系统挂起,会进行自旋锁操作。

其他线程持有锁的时间不会太长,如果直接挂起,操作系统层面的线程会得不偿失。
因为JVM到操作系统实现线程切换需要从用户态切换到内核态,这个状态转化更耗费时间,自旋锁假设不就后,当前线程就可以获得锁,因此虚拟机会让获取锁的线程执行几个空循环,可能是50次循环或者100次。
如果还是获取不到锁,将线程在操作系统层面挂起,并升级为重量级锁。

锁消除

Java虚拟机在编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。通过这种方式消除没有必要的锁。锁消除的依据是逃逸分析的数据支持。

轻量级锁获取HashCode
public class T0_ObjectSize {

    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
//        synchronized (o){
//            System.out.println(ClassLayout.parseInstance(o).toPrintable());
//        }
    }
}

在这里入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值