纯净、安全、绿色的下载网站

首页|软件分类|下载排行|最新软件|IT学院

当前位置:首页IT学院IT技术

Java synchronized Java synchronized最细讲解

Penguin——科波特   2021-09-10 我要评论
想了解Java synchronized最细讲解的相关内容吗Penguin——科波特在本文为您仔细讲解Java synchronized的相关知识和一些Code实例欢迎阅读和指正我们先划重点:Java,Java,synchronized下面大家一起来学习吧。

前言

线程安全问题的主要诱因有两点一是存在共享数据(也称临界资源)二是存在多条线程共同操作共享数据。

因此为了解决这个问题我们可能需要这样一个方案当存在多个线程操作共享数据时需要保证同一时刻有且只有一个线程在操作共享数据其他线程必须等到该线程处理完数据后再进行这种方式有个高尚的名称叫互斥锁即能达到互斥访问目的的锁也就是说当一个共享数据被当前正在访问的线程加上互斥锁后在同一个时刻其他线程只能处于等待的状态直到当前线程处理完毕释放该锁。

在 Java 中关键字 synchronized可以保证在同一个时刻只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作)同时我们还应该注意到synchronized另外一个重要的作用synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性完全可以替代Volatile功能)这点确实也是很重要的。

synchronized三种作用范围(给对象加锁)

在静态方法上加锁;

在非静态方法上加锁;

在代码块上加锁;

public class SynchronizedSample {
 
    private final Object lock = new Object();
 
    private static int money = 0;
		//非静态方法
    public synchronized void noStaticMethod(){
        money++;
    }
		//静态方法
    public static synchronized void staticMethod(){
        money++;
    }
		
    public void codeBlock(){
      	//代码块
        synchronized (lock){
            money++;
        }
    }
}
作用范围 锁对象
非静态方法 当前对象 => this
静态方法 类对象 => SynchronizedSample.class (一切皆对象这个是类对象)
代码块 指定对象 => lock (以上面的代码为例)

Synchronization实现原理

先理解Java对象头与Monitor

1.对象头:锁的类型和状态和对象头的Mark Word息息相关;

在这里插入图片描述

对象头分为二个部分Mard WordKlass Word

对象头结构 存储信息-说明
Mard Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
Klass Word 存储指向对象所属类(元数据)的指针JVM通过这个确定这个对象属于哪个类

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

锁状态 25bit 4bit 1bit是否是偏向锁 2bit 锁标志位
无锁状态 对象HashCode 对象分代年龄 0 01

在这里插入图片描述

主要分析一下重量级锁也就是通常说synchronized的对象锁锁标识位为10其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联对象与其 monitor 之间的关系有存在多种实现方式如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成但当一个 monitor 被某个线程持有后它便处于锁定状态。在Java虚拟机(HotSpot)中monitor是由ObjectMonitor实现的其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件C++实现的)

//👇图详细介绍重要变量的作用
ObjectMonitor() {
    _header       = NULL;
    _count        = 0;   // 重入次数
    _waiters      = 0,   // 等待线程数
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;  // 当前持有锁的线程
    _WaitSet      = NULL;  // 调用了 wait 方法的线程被阻塞 放置在这里
    _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对象的线程当多个线程同时访问一段同步代码时首先会进入 _EntryList 集合当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1若线程调用 wait() 方法将释放当前持有的monitorowner变量恢复为nullcount自减1同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值以便其他线程进入获取monitor(锁)。如下图所示

在这里插入图片描述

由此看来monitor对象存在于每个Java对象的对象头中(存储的指针的指向)synchronized锁便是通过这种方式获取锁的也是为什么Java中任意对象可以作为锁的原因同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因(关于这点稍后还会进行分析)。

jdk6 之后做了改进引入了偏向锁和轻量级锁:

  • 依赖底层操作系统的 mutex 相关指令实现加锁解锁需要在用户态和内核态之间切换性能损耗非常明显。
  • 研究人员发现大多数对象的加锁和解锁都是在特定的线程中完成。也就是出现线程竞争锁的情况概率比较低。他们做了一个实验找了一些典型的软件测试同一个线程加锁解锁的重复率如下图所示可以看到重复加锁比例非常高。早期JVM 有 19% 的执行时间浪费在锁上。

1.无锁到偏向锁转化的过程

在这里插入图片描述

  • 首先A 线程访问同步代码块使用CAS 操作将 Thread ID 放到 Mark Word 当中;
  • 如果CAS 成功此时线程A 就获取了锁
  • 如果线程CAS 失败证明有别的线程持有锁例如上图的线程B 来CAS 就失败的这个时候启动偏向锁撤销 (revoke bias);
  • 锁撤销流程:
    • 让 A线程在全局安全点阻塞(类似于GC前线程在安全点阻塞)
    • 遍历线程栈查看是否有被锁对象的锁记录( Lock Record)如果有Lock Record需要修复锁记录和Markword使其变成无锁状态。
    • 恢复A线程
    • 将是否为偏向锁状态置为 0 开始进行轻量级加锁流程

2.偏向锁升级轻量级

  • 线程在自己的栈桢中创建锁记录 LockRecord。
  • 线程A 将 Mark Word 拷贝到线程栈的 Lock Record中
  • 将锁记录中的Owner指针指向加锁的对象(存放对象地址)
  • 将锁对象的对象头的MarkWord替换为指向锁记录的指针。
  • 这时锁标志位变成 00 表示轻量级锁

其实就是撤销偏向锁后当前线程栈中会分配锁记录并拷贝Mark Word到锁记录中。然后两个线程用CAS的方式去修改Mark Word中的指针指向自己假如说第一个线程修改成功了然后将锁升级为轻量级锁去执行同步语句块中的内容。

3.轻量级到重量级

修改失败的第二个线程会进入自旋状态自旋结束后会继续去尝试CAS修改指针指向自己。如果自旋失败超过一定次数的时候(这个次数会动态进行调整)会请求JVM将此时的锁状态升级为重量级锁这是依赖于底层操作系统的调度库来实现的。接着将Mark Word指向重量级锁Monitor的指针然后挂起当前第二个线程(被放在Monitor的_EntryList中)。等一个线程执行完毕后会查看当前Mark Word中的指针是否仍然指向自己如果是自己的话就释放锁否则不是自己的话说明此时已经升级成了重量级锁除了释放锁之后还会唤醒阻塞的线程进行新一轮的锁竞争。在此之后该锁就一直会是重量级锁存在了

ps:为什么设计自旋数超过一定限制设置为重量级锁?

一般来说同步代码块内的代码应该很快就执行结束这时候修改失败的第二个线程自旋一段时间是很容易拿到锁的但是如果不巧没拿到自旋其实就是死循环很耗CPU的因此就直接转成重量级锁咯这样就不用了线程一直自旋了。

源码才学疏浅只了解到:

synchronized 在代码块上是通过 monitorenter 和 monitorexit指令实现在静态方法和 方法上加锁是在方法的flags 中加入 ACC_SYNCHRONIZED 。JVM 运行方法时检查方法的flags遇到同步标识开始启动前面的加锁流程在方法内部遇到monitorenter指令开始加锁。

总结

本篇文章就到这里了希望能够给你带来帮助也希望您能够多多关注的更多内容!


相关文章

猜您喜欢

  • OpenCV构建叠加层 超详细注释之OpenCV构建透明的叠加层

    想了解超详细注释之OpenCV构建透明的叠加层的相关内容吗程序媛一枚~在本文为您仔细讲解OpenCV构建叠加层的相关知识和一些Code实例欢迎阅读和指正我们先划重点:OpenCV构建叠加层,python构建叠加层下面大家一起来学习吧。..
  • java dom4j 解析xml文件 Java中使用DOM4J生成xml文件并解析xml文件的操作

    想了解Java中使用DOM4J生成xml文件并解析xml文件的操作的相关内容吗小王写博客在本文为您仔细讲解java dom4j 解析xml文件的相关知识和一些Code实例欢迎阅读和指正我们先划重点:java,dom4j,解析xml文件,java,dom4j,生成xml文件下面大家一起来学习吧。..

网友评论

Copyright 2020 www.Shellfishsoft.com 【贝软下载站】 版权所有 软件发布

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 点此查看联系方式