首页 > 编程语言 >Java锁

Java锁

时间:2024-07-06 16:20:25浏览次数:19  
标签:Java synchronized 对象 虚拟机 线程 方法 轻量级

Java锁

1.介绍

首先, java 的锁分为两类:

  1. 第一类是 synchronized 同步关键字,这个关键字属于隐式的锁,是 jvm 层面实现,使用的时候看不见;
  2. 第二类是在 jdk5 后增加的 Lock 接口以及对应的各种实现类,这属于显式的锁,就是我们能在代码层面看到锁这个对象,而这些个对象的方法实现,大都是直接依赖 CPU 指令的,无关 jvm 的实现

接下来就从 synchronizedLock 两方面来讲

2.synsychronized

2.1synsychronized的使用

  • 如果修饰的是具体对象:锁的是对象
  • 如果修饰的是成员方法:那锁的就是 this
  • 如果修饰的是静态方法:锁的就是这个对象.class

2.2 Java的对象头和monitor

理解 synchronized 原理之前,我们需要补充一下 java 对象的知识

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

  1. 对象头。Hot Spot 虚拟机对象的对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,如哈希码( Hash Code)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它为“ Mark Word”。

img

对象需要存储的运行时数据很多,其实已经超出了32、64位 Bitmap 结构所能记录的最大限度,但对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间

  1. 实例数据。实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。

这部分的存储顺序会受到虚拟机分配策略参数 (-XX: Fields Allocation Style参数) 和字段在Java源码中定义顺序的影响。Hot Spot 虚拟机默认的分配顺序为 longs/doubles、ints、shorts/chars、bytes/booleans、oops( Ordinary Object Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果 Hotspot 虚拟机的 XX: Compact Fields 参数值为 true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间

  1. 对齐填充。并不是必然存在的,由于 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全

介绍完了对象的内容,和锁相关的显然就是对象头里存储的那几个内容:

  • 其中的重量级锁也就是通常说 synchronized 的对象锁,其中指针指向的是 monitor 对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,monitor 是由ObjectMonitor 实现的,C++实现
  • 注意到还有轻量级锁,这是在 jdk6 之后对 synchronized 关键字底层实现的改进

2.3 synchronized 原理

我们已经知道 synchronized 和对象头里的指令有关,也就是我们以前大概的说法:

Java虚拟机可以支持方法级的同步和方法内部一段指令序列(代码块)的同步,这两种同步结构都是使用管程( Monitor,更常见的是直接将它称为“锁”) 来实现的

现在我们讲讲原理

因为对于 synchronized 修饰方法(包括普通和静态方法)、修饰代码块,这两种用法的实现略有不同:

2.4.synchronized 修饰方法

我们测试一个同步方法:

public class Tues {
    public static int i ;
    public synchronized static void syncTask(){
        i++;
    }
}

然后反编译 class文件,可以看到:

img

其中的方法标识:

  • ACC_PUBLIC 代表public修饰
  • ACC_STATIC 表示是静态方法
  • ACC_SYNCHRONIZED 指明该方法为同步方法。

这个时候我们可以理解《深入理解java虚拟机》里,对于同步方法底层实现的描述如下:

方法级的同步是隐式的。 无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否被声明为同步方法。(静态方法也是如此)

  • 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程(Monitor),然后才能执行方法,最后当方法完成 (无论是正常完成还是非正常完成)时释放管程。
  • 在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。
  • 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。

2.5.synchronized修饰代码块

测试一段同步代码:

public class Tues {
   public int i;
   public void syncTask(){
       synchronized (this){
           i++;
       }
   }
}

然后反编译 class 文件:

img

可以看到,在指令方面多了关于 Monitor 操作的指令,或者和上一种修饰方法的区别来看,是显式的用指令去操作管程(Monitor)了

同理,这个时候我们可以理解《深入理解java虚拟机》里的描述如下:

同步一段指令集序列的情况。Java虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义。(monitorenter 和 monitorexit 两条指令是 C 语言的实现)正确实现 synchronized 关键字需要 Javac 编译器与 Java 虚拟机两者共同协作支持。Monitor的实现基本都是 C++ 代码,通过JNI(java native interface)的操作,直接和cpu的交互编程

早期synchronized的问题

早期的 synsychronized 的实现就是基于上面所讲的原理,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因

更具体一些的开销,还涉及 java 的线程和操作系统内核线程的关系

前面讲到对象头里存储的内容的时候我们也留了线索,那就是 jdk6 之后多出来轻量级的锁,来改进 synchronized 的实现

我的理解,这个改进就是:从加锁到最后变成以前的那种重量级锁的过程里,新实现出状态不同的锁作为过渡

改进后的锁

偏向锁->自旋锁->轻量级锁->重量级锁。按照这个顺序,锁的重量依次增加

  • 偏向锁。他的意思是这个锁会偏向于第一个获得它的线程,当这个线程再次请求锁的时候不需要进行任何同步操作,从而提高性能。那么处于偏向锁模式的时候,对象头的Mark Word 的结构会变为偏向锁结构

研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁的代价而引入偏向锁。那么显然,一旦另一个线程尝试获得这个锁,那么偏向模式就会结束。另一方面,如果程序的大多数锁都是多个线程访问,那么偏向锁就是多余的

public class Example {
    private final Object lock = new Object();

    public void execute() {
        for (int i = 0; i < 1000; i++) {
            synchronized (lock) {
                // 临界区代码
                doSomething();
            }
        }
    }

    private void doSomething() {
        // 执行某些操作
    }
}

在这个例子中,假设方法 execute() 总是由同一个线程调用,那么这个线程在循环中会频繁地获取和释放 lock。在这种情况下,锁总是由同一个线程多次获得

为了优化这种场景下的性能,Java引入了偏向锁(Biased Locking)。偏向锁的机制是,当一个线程第一次获得锁时,锁会“偏向”这个线程,后续该线程再次获取锁时,可以直接进入临界区,而不需要进行复杂的锁竞争操作。这种优化减少了同一线程获取锁的代价,提高了性能
  • 轻量级锁。当偏向锁的条件不满足,亦即的确有多线程并发争抢同一锁对象时,但并发数不大时,优先使用轻量级锁。一般只有两个线程争抢锁标记时,优先使用轻量级锁。 此时,对象头的Mark Word 的结构会变为轻量级锁结构

轻量级锁是和传统的重量级锁相比较的,传统的锁使用的是操作系统的互斥量,而轻量级锁是虚拟机基于 CAS 操作进行更新,尝试比较并交换,根据情况决定要不要改为重量级锁。(这个动态过程也就是自旋锁的过程了)

  • 重量级锁。重量级锁即为我们在上面探讨的具有完整Monitor功能的锁
  • 自旋锁。自旋锁是一个过渡锁,是从轻量级锁到重量级锁的过渡。也就是CAS

CAS,全称为Compare-And-Swap,是一条CPU的原子指令,其作用是让CPU比较后原子地更新某个位置的值,实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM 只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。

注意:Java中的各种锁对程序员来说是透明的: 在创建锁时,JVM 先创建最轻的锁,若不满足条件则将锁逐次升级.这四种锁之间只能升级,不能降级

上面说的锁都是基于 synchronized 关键字,以及底层的实现涉及到的锁的概念,还有一些别的角度的锁分类:

按照锁的特性分类:

  1. 悲观锁:独占锁,会导致其他所有需要所的线程都挂起,等待持有所的线程释放锁,就是说它的看法比较悲观,认为悲观锁认为对于同一个数据的并发操作,一定是会发生修改的。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。比如前面讲过的,最传统的 synchronized 修饰的底层实现,或者重量级锁。(但是现在synchronized升级之后,已经不是单纯的悲观锁了)
  2. 乐观锁:每次不是加锁,而是假设没有冲突而去试探性的完成操作,如果因为冲突失败了就重试,直到成功。比如 CAS 自旋锁的操作,实际上并没有加锁

按照锁的顺序分类:

  1. 公平锁。公平锁是指多个线程按照申请锁的顺序来获取锁。java 里面可以通过 ReentrantLock 这个锁对象,然后指定是否公平
  2. 非公平锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。使用 synchronized 是无法指定公平与否的,他是不公平的

独占锁(也叫排他锁)/共享锁:

  1. 独占锁也叫排他锁,是指该锁一次只能被一个线程所持有。对 ReentrantLock 和 Sychronized 而言都是独占锁
  2. 共享锁:是指该锁可被多个线程所持有。对 ReentrantReadWriteLock 而言,其读锁是共享锁,其写锁是独占锁。读锁的共享性可保证并发读是非常高效的,读写、写读、写写的过程都是互斥的

独占锁/共享锁是一种广义的说法,互斥锁/读写锁是java里具体的实现

标签:Java,synchronized,对象,虚拟机,线程,方法,轻量级
From: https://www.cnblogs.com/zhangyf1121/p/18287391

相关文章

  • Java基础-接口与实现
    (创作不易,感谢有你,你的支持,就是我前行的最大动力,如果看完对你有帮助,请留下您的足迹)目录Java接口什么是接口?声明接口实现接口继承接口  接口的多继承标记接口Java接口什么是接口?Java中的接口定义了一个引用类型来创建抽象概念。接口由类实现以提供概念的实现......
  • “只讲干货!!”{java入门篇} 勇闯java的勇士们 别问我java行不行 不行也是你不行,不努力
    面向对象编程(Object    Oriented    Programing)神速熟悉面向对象        学完本节,如果还有点糊涂,很正常,本节仅是你的“初恋对象”。本节仅仅是为了方便大家入门,更快的了解面向对象。后面,才是真正开始“面向对象”,真正为了“结婚”、为了“开......
  • Vue联调Java后台操作性强教程
    本人详解作者:王文峰,参加过CSDN2020年度博客之星,《Java王大师王天师》公众号:JAVA开发王大师,专注于天道酬勤的Java开发问题中国国学、传统文化和代码爱好者的程序人生,期待你的关注和支持!本人外号:神秘小峯山峯转载说明:务必注明来源(注明:作者:王文峰哦)Vue联调Java......
  • 快速排序c++&&java代码实现
    快速排序的思想(基于分治法): 每次选一个基准元素x,通过一次遍历将排序表划分为独立的两部分a[l,k-1],a[k+1,r];其中左边的元素<=x,右边的1元素>x,然后递归下去,直到每个块的大小为1;c++#include<bits/stdc++.h>usingnamespacestd;voidquickSort(vector<int>&q,int......
  • java学习——基础语法篇(适合已经学过c++的人群)
    本篇适合之前学过c和c++语言,现在要学java的人进行学习。由于之前学习过c++同为面向对象的程序语言,基础语法在相对比之下学习,对比c++与java的差异,会快速掌握基本语法。学编程语言,语法不是重点,用时即查,编程思路才是重点。1.注释、标识符、关键字、数据类型,变量定义,运算符与c++基本......
  • Java面试题系列 - 第4天
    题目:深入理解Java泛型与类型擦除背景说明:Java泛型是JavaSE5引入的一种新特性,它允许在编译时检查类型安全,并且所有的强制转换都是自动和隐式的,提高了代码的重用率。然而,Java泛型的实现背后有一个重要的概念——类型擦除,理解这一点对于深入掌握泛型编程至关重要。问题要求:解......
  • 如何使用javadoc?为什么需要这个源代码文档工具
    目录一、什么是javadoc二、javadoc为什么会找不到路径三、如何解决javadoc一直找不到路径的问题一、什么是javadocJavadoc是一种用于生成Java源代码文档的工具,它可以帮助开发者生成易于阅读和理解的文档。Javadoc通过解析Java源代码中的注释,提取其中的文档信息,然后......
  • Java 方法中循环调用具有事务的方法
    在Java中,循环调用一个具有事务的方法时,需要特别注意事务的边界和管理。通常,事务的边界是由框架(如Spring)来控制的,确保方法执行时数据的完整性和一致性。然而,在循环中调用事务方法时,每个调用都可以被视为独立的事务,除非特别配置以允许跨多个方法调用共享同一事务。1.Java方法......
  • 基于java+springboot+vue实现的图书商城管理系统(文末源码+Lw)283
     摘 要现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本图书商城管理系统就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞大的数据信息,使用这种软件工具可以帮助管理人员提高事务处理......
  • 基于java+springboot+vue实现的旅游推荐系统(文末源码+Lw)280
    摘 要传统办法管理信息首先需要花费的时间比较多,其次数据出错率比较高,而且对错误的数据进行更改也比较困难,最后,检索数据费事费力。因此,在计算机上安装旅游推荐系统软件来发挥其高效地信息处理的作用,可以规范信息管理流程,让管理工作可以系统化和程序化,同时,旅游推荐系统的有效......