Java锁机制

本文详细介绍了Java中的锁机制,包括对象头结构、同步锁synchronized、Moniter(管程/监视器)、锁状态及升级过程。文章讨论了无锁、偏向锁、轻量级锁和重量级锁的概念,以及它们如何影响并发性能。此外,还涉及了无锁编程、CAS(Compare And Swap)以及其在Java中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

java的锁机制

在并发环境下,多个线程会对同一个资源进行争抢,可能会导致数据不一致的问题。

在JAVA中每一个对象都有一把锁,存放在对象头中,锁中记录了当前对象被哪个线程所占用。

对象和对象头的结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rw7jrSW5-1604552466502)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201104155623859.png)]

填充字节

其中对齐填充字节是为了满足“Java对象的大小必须是8比特的倍数”设计的,为了帮助对象来对齐而填充的一些无用字节,说是能让cpu更快的计算出结果。

实例数据

你在初始化对象时设定的属性和对应的属性值

对象头

对象头存放了一些对象本身的运行时信息,对象头包含了两部分Mark Word和Class Pointer,相较于实例数据,对象头属于一些额外的存储开销,所以被设计的极小来提高效率。

Class Pointer就是一个指针,指向了当前对象类型所在方法区中的类型数据。

Mark Word存储了很多和当前对象运行时状态有关的数据,比如hashcode,指向锁记录的指针,偏向锁id等等。

Mark Word只有32bit,并且是非结构化的,这样在不同的锁标志位下,不同的字段可以重用不同的比特位,能够节省空间。
(mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。)
数组对象的对象头还会存储着一个32bit的数组长度

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ge1OnsDJ-1604552466505)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201104161150582.png)]

对象中的锁就存放在对象头的Mark Word中,Mark Word的最后两位代表了锁标志位,分别是无锁、偏向锁、轻量级锁、重量级锁四种状态。

同步锁synchronized

Java中synchronized关键字可以用来同步线程,synchronized被编译后会生成monitorenter和monitorexit两个字节码指令。

下面的同步锁例子,进行编译和反编译

public class TestSync {
    private int num = 0;

    public void test() {
        for (int i = 0; i < 1000; i++) {
            synchronized (this) {
                System.out.println("thread:" + Thread.currentThread().getId() + ",num:" + num++);
            }
        }
    }
}
public class Main2 {
    public static void main(String[] args) {
        TestSync sync = new TestSync();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                sync.test();
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                sync.test();
            }
        });
        t1.start();
        t2.start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dijg5KoN-1604552466508)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201104165108826.png)]

上面反编译后的字节码可以看到确实出现了monitorenter和monitorexit两个字节码指令

Moniter(管程/监视器)

你可以把它想象为一个房间,这个房间只能容纳一个线程,一个线程进入了moniter那么其他线程只能等待,只有当该线程退出,其他线程才有进入的机会

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GiIPpnZB-1604552466512)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201104165711305.png)]

首先Entry Set中聚集了一些想要进入monitor的线程,它们正处于waiting状态,假如一个名为A的线程成功进入monitor,那么它就处于Active状态,假设此时线程执行途中遇到了一个判断条件,需要它暂时让出执行权,那么它将进入Wait Set,此时线程的状态为Waiting,此时Entry Set中的线程就有机会进入monitor,假设一个线程B进入了monitor,并且执行完了自己的任务,那么它可以通过notify的形式去来唤醒Wait Set中的线程A,让线程A有机会能重新进入monitor,继续完成自己的任务。

这就是synchronized同步机制,但是synchronized可能存在性能问题,因为synchronized被编译后实际上是monitorenter和monitorexit两个字节码指令,而Monitor是依赖操作系统的mutex lock来实现的,Java线程实际上是对操作系统线程的映射,所以每当唤醒和挂起一个线程的时候,都要转换到操作系统内核态,这种操作是重量级的,并且在一些情况下甚至切换时间本身将会超出线程执行任务的时间,使用synchronized将会对程序的性能产生严重的影响。

后来从Java6开始,synchronized进行了优化,引入了偏向锁、轻量级锁,所以锁总共有四种状态,从低到高分别是无锁、偏向锁、轻量级锁、重量级锁,这就分别对应了Mark Word中的四种状态,注意,锁只能升级不能降级

锁状态和锁升级

1、无锁

无锁,就是没有对资源进行锁定,所有线程都能访问同一资源,这就可能出现两种情况:

  1. 无竞争

    某个对象不会出现在多线程环境下或者说即使出现在了多线程环境下也不会出现竞争的情况,这样就无需对这个对象进行任何保护,这种情况让对象能被其他线程随意调用就可以了

  2. 存在竞争

    资源会被竞争但是我不想对资源进行锁定,不过还是想通过一些机制来控制多线程,比如说,如果有多个线程想要修改同一个值,我们不通过锁定资源的方式,而是通过其他方式来限制,同时,只有一个线程能修改成功,而修改失败的线程不断地去尝试修改,直至修改成功,这就是CAS(Compare and swap),CAS通过操作系统中的一条指令来实现,所以能保证操作的原子性,通过诸如CAS这种方式,我们可以进行无锁编程。

上面也分析了依赖操作系统mutex lock导致性能低下的原因,所以在大部分情况下无锁的效率是很高的,但是并不能意味着无锁能够全面代替锁

2、偏向锁

假如一个对象被加锁了,但在实际运行时只有一个线程会获取这个对象锁,那么最理想的方式就是不通过线程状态切换,也不要通过CAS来获得锁,因为这多多少少还是会耗费一些资源。

设想的是,最好对象能够认识这个线程,只要是这个线程过来,线程就会把锁交出去,“锁偏爱这个线程”称为偏向锁。

偏向锁实现:在Mark Word中,当锁标志位是01时,那么判断倒数第3个bit是否为1。如果是1,那么代表当前对象的锁状态为偏向锁,否则为无锁,如果当前锁状态为偏向锁,于是再去读Mark Word前23个bit的线程ID,通过线程ID来确认当前想要获取对象锁的这个线程是不是之前的线程。

假如情况发生了变化,对象发现目前不只有一个线程而是有多个线程在竞争锁,那么偏向锁将会升级为轻量级锁,当锁的状态还是偏向锁时,是通过Mark Word中的线程ID来找到占有这个锁的线程

3、轻量级锁

当升级为轻量级锁的时候,Mark Word已经不存放线程ID字段了,而是将前30位bit变为了指向线程栈中锁记录(Lock Record)的指针。

当一个线程想要获取某个对象的锁时,假如看到锁标志位为00那么就知道它是轻量级锁,这时线程会在自己的虚拟机栈中开辟一块被称为Lock Record的空间中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ddccvtd4-1604552466514)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201104221052170.png)]

Lock Record中存放的是对象头中的Mark Word的副本以及owner指针,线程通过CAS尝试获取锁,一旦获取那么将会复制该对象头中的Mark Word到Lock Record中,并将Lock Record中的owner指针指向该对象,另一方面对象的Mark Word的前30个bit会形成一个指针,指向线程虚拟机栈中的Lock Record,这样一来,就实现了线程和对象锁的绑定,互相知道了对方的存在。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vYMu5gPl-1604552466515)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201104222204396.png)]

这时这个对象已经被绑定了,获取了这个对象锁的线程就可以去,指向一些任务,其他的线程将会自旋等待,自旋可以理解为一种轮询,线程自己在不断地循环,尝试着去看一下目标对象的锁有没有被释放,如果释放了那么就获取,如果没有释放那么就进行下一轮循环,这种方式区别于被操作系统挂起,因为如果对象的锁很快就会被释放的话,自旋就不需要进行系统中断和现场恢复,所以效率更高,但是自旋相当于CPU在空转,如果长时间自旋将会浪费CPU资源。

于是出现了一种叫“适应性自旋”的优化,简单来说,自旋的时间不在固定了,而是由上一次在同一个锁上的自选时间以及锁状态,这两个条件决定,例如在同一个锁上,当前正在自旋等待的线程刚刚已经成功获得过锁,但是锁目前是被其他线程占用,那么虚拟机就会认为这次自旋很有可能会再次成功,进而它将允许更长的自旋时间,这就是适应性自旋

4、重量级锁

当有多个锁进行自旋,或者是自旋次数达到一定次数,轻量级锁将升级成为重量级锁,也就是使用moniter对线程进行管控。

无锁编程

AQS、CAS、JUC源码

假设多个线程想要操作同一个资源对象,怎么确保每次操作的正确性?大部分人都会使用同步锁,但是同步锁是“悲观”的,“悲观”的意思简单来说就是操作系统会悲观的认为,如果不严格同步线程调用那么一定会产生异常错误,所以互斥锁会将资源锁定,只供一个线程调用,而阻塞其他线程,因此这种同步机制也叫做悲观锁,但是不是任何时候都适合用悲观锁的。

比如在一些情况下,大部分调用可能都是读操作,那么就没有必要在每次调用的时候都锁定资源,或者在一些情况下,同步代码块执行的耗时远远小于线程切换的耗时,这样就有点本末倒置了,所以在这些情况下,我们不想让操作系统那么悲观,我们不想过度使用互斥锁。

CAS(Compare And Swap)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6AugeTlQ-1604552466516)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105100939810.png)]

为资源对象设置一个状态值,当资源对象的状态值为0的一瞬间A、B两个线程都读到了,此时这两条线程认为资源对象当前的状态值是0,于是它们将各自产生两个值,Old value代表之前读到的资源对象的状态值,New value代表想要将资源对象的状态值更新后的值。这里对A、B线程来说,Old value都是0,New value都是1,此时A、B线程争抢着去修改资源对象的状态值,然后占用它,假设A线程的运气比较好,率先获得了时间片,它将old value与资源对象的状态值进行compare发现一致,于是将资源对象的状态值swap为new value,而B线程因为迟了一步,此时资源对象的状态值已经被A线程修改成了1,所以B线程在compare的时候,发现Old value与预期的不相符,所以放弃swap操作。

但是在实际应用中,我们不会让B线程就这么放弃,通常会使其进行自旋,自旋就是使其不断的重试CAS操作,通常会配置自旋次数来防止死循环。例如A线程在成功修改了资源对象状态值之后占用着资源对象,B线程会时不时关注资源对象的状态值,假如某一瞬间状态值变成了0,那么B线程将会重新去争抢资源对象。

CAS多线程下难保证一致性

CAS函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nP0X4C6b-1604552466516)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105100919989.png)]

比如这个CAS没有进行任何的同步操作,说明还是线程不安全的。比如当A线程看到了资源的状态是0时,准备修改的一瞬间,很有可能B线程突然抢到了时间片,将资源状态修改成1,但是A线程对此是不知情的,也就资源状态修改成了1,这就出现了问题,A、B线程同时获得资源。(多条线程同时占用资源对象)

所以compare和swap必须“被绑定”成为一个原子性操作,也就是对资源对象的状态值进行compare和swap,在某一时刻只能有一条线程进行操作。

CAS要是原子性的

各种不同架构的CPU都提供了指令级别的CAS原子操作,比如在x86架构下,通过cmpxchg指令支持cas。在ARM架构下,通过LL/SC来实现CAS。也就是说,不需要通过操作系统的同步原语(比如mutex),CPU已经原生地支持CAS,上层进行调用即可,这样就能够不再依赖锁来进行线程同步,但是不是说无锁能代替锁。

乐观锁

通过CAS来实现同步的工具,由于不会锁定资源,而且当线程需要修改共享资源的对象时,总是乐观地认为,对象状态值没有被其他线程修改过,而是每次自己都会主动尝试去compare状态值,相较于上面的悲观锁,这种同步机制也被称为乐观锁(实现上并没有用到锁,而是一种无锁的同步机制)。

CAS在java中的应用

现在假如想要用3条线程,将一个值,从0累加到100

先来看不使用任何同步操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kyYFeNgM-1604552466517)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105103542281.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kgBfxoGh-1604552466518)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105103609267.png)]

可以看到多条线程打印了相同的值,说明线程之间没有进行正确通信,常规的话,可以通过互斥锁来进行同步

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AJGEefbV-1604552466520)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105120041011.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-feOp682M-1604552466521)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105120112139.png)]

可以看到是顺序执行的

无锁同步(乐观锁)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FMHYuMrw-1604552466522)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\CAS7.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-56rK04yc-1604552466523)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105121025042.png)]

可以看到每个数字只输出了一次,线程正确同步

接下来看源码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8juJneqq-1604552466524)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105121350767.png)]

AtomicInteger类主要的成员变量就是一个unsafe类型的实例和一个long类型的offset,是使用unsafe的cas操作对值进行更新,进一步看incrementAndGet()方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QVNyw5mB-1604552466526)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\CAS8.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xXx6qKL5-1604552466527)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\CAS9.png)]

可以看到直接调用了unsafe的getAndAddInt方法,确实就是调用了unsafe的compareAndSwapInt方法,从名字上看也可以知道它是一个CAS的操作,可以看到这里出现了一个循环,也就是自旋。

假如这边CAS操作一直失败,也不会一直死循环下去,实际上自选的次数可以通过启动参数来配置,如果不配置默认是10。

Unsafe类

Unsafe类主要是用来执行一些底层的和平台相关的方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qt0csl1j-1604552466528)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105123559751.png)]

上面的例子,就是用到了unsafe调用底层cas的能力

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IXeKgsUT-1604552466529)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105124229374.png)]

可以看到compareAndSetInt修饰符是vative,说明它是一个本地方法,和具体的平台实现相关,如果你的cpu是x86架构,那么事实上这个本地方法将会调用系统的cmpxchg指令

在openJDK源码中的两个目录的文件中可以看到对应的本地方法
20201105125442928

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值