Java synchronized最细讲解

目录
  • 前言
  • Synchronization实现原理
    • 先理解Java对象头与Monitor
      • 1.对象头:锁的类型和状态和对象头的Mark Word息息相关;
  • jdk6 之后做了改进,引入了偏向锁和轻量级锁:
    • 1.无锁到偏向锁转化的过程
    • 2.偏向锁升级轻量级
    • 3.轻量级到重量级
  • 总结

前言

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

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

在 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() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减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指令开始加锁。

总结

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

(0)

相关推荐

  • 有关Java中的BeanInfo介绍

    目录 1.JavaBean介绍 2.JavaBean的自省 3.JavaBean内省工具Introspector 4.JavaBean内省结果BeanInfo 5.内省结果BeanInfo的类型 6.Spring的BeanUtils.copyProperties 7.BeanUtils并发问题优化 8.BeanUtils Setter属性识别优化 9.BeanUtils 性能测试 1.JavaBean介绍 维基百科JavaBean的定义:JavaBeans是Java中一种特殊的类,可以将多个对象

  • Java SpringBoot开发小技巧详解

    目录 一.SpringBoot开发小技巧 1.1 Lombok 1.2 dev-tools 1.3 Spring Initializr 总结 一.SpringBoot开发小技巧 1.1 Lombok 作用:在程序编译的时候,自动帮我们生成setter和getter方法以及我们的toString方法和我们的全参和无参构造器等等. 那么,怎么用呢?很简单,用下边这四个注解就行了: 1.@Data:自动生成setter和getter方法. 2.@ToString:自动生成toString方法. 3.@

  • Java并发编程之阻塞队列(BlockingQueue)详解

    目录 队列 阻塞队列 ArrayBlockingQueue 重要属性 构造方法 添加元素 add(e) offer(e) put(e) offer(e,time,unit) 移除元素 take() dequeue() LinkedBlockingQueue 重要属性 构造方法 添加元素 offer(e) put(e) 移除元素 poll() take() 对比 总结 大家好,我是小黑,一个在互联网苟且偷生的农民工. 队列 学过数据结构的同学应该都知道,队列是数据结构中一种特殊的线性表结构,和平时

  • Java timezone设置和mybatis连接数据库时区设置方式

    目录 Java timezone设置和mybatis连接数据库时区设置 JVM时区设置 Mybatis Mybatis timezone问题 解决方法 Java timezone设置和mybatis连接数据库时区设置 JVM时区设置 springboot工程运行时,需要指定时区,这样获取的时间才会和系统时间相同.以下介绍方法: 1.查看当前时区 centos7以前(不含centos7) cat /etc/sysconfig/clock centos7以后(包含centos7) timedatec

  • Java SpringBoot+vue+实战项目详解

    目录 1.<锋迷商城>业务流程设计-接⼝规范 1.1 前后端分离与单体架构流程实现的区别 1.1.1单体架构 1.1.2 前后端分离架构 1.2 接口介绍 1.2.1接口概念 1.2.2接口规范 1.3 Swagger 1.3.1作用 1.3.2 Swagger整合 1.3.3 Swagger注解说明 1.3.4 Swagger-ui 插件 1.4 RESTful 总结 1.<锋迷商城>业务流程设计-接⼝规范 在企业项⽬开发中,当完成项⽬的需求分析.功能分析.数据库分析与设计之后,

  • Java SpringBoot快速集成SpringBootAdmin管控台监控服务详解

    目录 1.初识SpringBootAdmin 2.搭建服务端--POM文件中添加相关依赖 3.修改服务端application启动类 4.配置security安全信息 5.启动server服务端 6.搭建client客户端 总结 SpringBootAdmin是一个针对 Spring Boot 的 Actuator 接口进行 UI 美化封装的监控工具,它可以在列表中浏览所有被监控 spring-boot 项目的基本信息.详细的 Health 信息.内存信息.JVM 信息.垃圾回收信息.各种配置信

  • Java synchronized最细讲解

    目录 前言 Synchronization实现原理 先理解Java对象头与Monitor 1.对象头:锁的类型和状态和对象头的Mark Word息息相关: jdk6 之后做了改进,引入了偏向锁和轻量级锁: 1.无锁到偏向锁转化的过程 2.偏向锁升级轻量级 3.轻量级到重量级 总结 前言 线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据. 因此为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在

  • Java超详细透彻讲解static

    目录 1. 引入 2. 理解 3. 使用 3.1 使用范围 3.2 static修饰属性 3.2.1 设计思想 3.2.2 分类 3.2.3 注意 3.2.4 举例 3.2.5 类变量内存解析 3.3 static修饰方法 3.3.1 设计思想 3.3.2 理解 3.3.3 使用 3.3.4 注意 3.3.5 举例 4. 注意 5. 单例 (Singleton)设计模式 5.1 概述 5.2 优点 5.3 单例设计模式-饿汉式 5.4 单例设计模式-懒汉式 5.5 应用场景 1. 引入 当我们编

  • Java synchronized与CAS使用方式详解

    目录 引言 synchronized synchronized的三种使用方式 synchronized的底层原理 JDK1.6对synchronized的优化 synchronized的等待唤醒机制 CAS 引言 上一篇文章中我们说过,volatile通过lock指令保证了可见性.有序性以及“部分”原子性.但在大部分并发问题中,都需要保证操作的原子性,volatile并不具有该功能,这时就需要通过其他手段来达到线程安全的目的,在Java编程中,我们可以通过锁.synchronized关键字,以及

  • MongoDB快速入门笔记(八)之MongoDB的java驱动操作代码讲解

    MongoDB的Java驱动是线程安全的,对于一般的应用,只要一个Mongo实例即可,Mongo有个内置的连接池(池大小默认为10个). 下面代码给大家介绍MongoDB的java驱动操作,具体代码如下所示: import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; import org.bson.Document; import com.mongodb.MongoClient; impo

  • java synchronized同步静态方法和同步非静态方法的异同

    java synchronized 详解 synchronized关键字有两种用法,一种是只用于方法的定义中,另外一种是synchronized块,我们不仅可以使用synchronized来同步一个对象变量,你也可以通synchronizedl来同步类中的静态方法和非静态方法. synchronized块的语法如下: public void method() { synchronized(表达式) { } } public void method() { synchronized(表达式) {

  • 基于Java ActiveMQ的实例讲解

    所需引入Jar包: jms-1.1.jar activemq-all-5.15.0.jar 生产者 package com.mousewheel.demo; import javax.jms.Connection; import javax.jms.ConnectionFactory; import javax.jms.Destination; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.Me

  • java synchronized加载加锁-线程可重入详解及实例代码

    java synchronized加载加锁-线程可重入 实例代码: public class ReGetLock implements Runnable { @Override public void run() { get(); } public synchronized void get() { System.out.println(Thread.currentThread().getId()); set(); } public synchronized void set() { Syste

  • Java MD5加密(实例讲解)

    MD5 Message Digest Algorithm MD5(中文名为消息摘要算法第五版)为计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护.该算法的文件号为RFC 1321(R.Rivest,MIT Laboratory for Computer Science and RSA Data Security Inc. April 1992). MD5即Message-Digest Algorithm 5(信息-摘要算法5),用于确保信息传输完整一致.是计算机广泛使用的杂凑算法之

  • Java ArrayList 实现实例讲解

     ArrayList概述:  ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类. ArrayList实现了Serializable接口,因此它支持序列化,能

  • hadoop中实现java网络爬虫(示例讲解)

    这一篇网络爬虫的实现就要联系上大数据了.在前两篇java实现网络爬虫和heritrix实现网络爬虫的基础上,这一次是要完整的做一次数据的收集.数据上传.数据分析.数据结果读取.数据可视化. 需要用到 Cygwin:一个在windows平台上运行的类UNIX模拟环境,直接网上搜索下载,并且安装: Hadoop:配置Hadoop环境,实现了一个分布式文件系统(Hadoop Distributed File System),简称HDFS,用来将收集的数据直接上传保存到HDFS,然后用MapReduce

随机推荐