Java多线程中sysncronized加锁和CAS

2023-07-15

锁->syncronized->Monitor->CAS



 

# 什么是锁

锁其实就是线程共享的一个值,锁存在的意义就是 "当前锁有没有被别人使用"。


 

对于锁只有两种操作,读、写。当多个线程来想获取锁(读到锁的值为0,然后改为1 这里有两个操作)时只有一个能成功,能成功的前提是线程A读取并修改值为1后,其他线程如线程B能立即读取到值为1,而不是两个线程都能读到0。

所以就需要保证`读写`这一套操作具有原子性。

在java中由syncronized对锁进行*加锁*


 

# syncronized

syncronized代码块的字解码出入口分别有monitorenter和monitorexit这两条指令。

当两个线程都monitorenter的时候就需要保证只有一个能monitorenter成功,这需要JVM和和编译器共同协作实现。

这个具体由Monitor来实现。


 

# Monitor

每个java对象都能关联一个Monitor对象,当syncronized给一个对象加锁后,ObjectHeader的MarkWord就被设置成指向一个Monitor的指针。

Monitor的结构如下

```

Owner

EntryList[]

WaitList[]

```

起初Owner的值为null,线程A monitorenter时,会先判断Owner是否为空,为空再Owner设置为线程A。

此时如果线程B、线程C也来加锁,那么将会进入EntryList。

如果线程A执行过程中调用了wait()方法,那么就会进入WaitList。


 

Monitor是由JVM来具体实现的,对象源码为ObjectMonitor.hpp。

所以要monitorenter线程安全,只需要保证`先判断Owner是否为空,为空再Owner设置为线程A`这个操作线程安全就好了。JVM是通过操作系统提供的互斥锁Mutex Lock来实现的。

 

# CAS compare and swap

```gpt

> 你好,计算机中CAS的原子性如何保证?

>CAS(Compare-and-Swap)是一种基本的原子性操作,常用于多线程编程和并发控制中。CAS操作可以确保在多线程环境下对共享变量的原子性修改。

CAS操作通常包含三个参数:要修改的内存地址、原始值和新值。CAS操作的执行步骤如下:

读取内存地址中的原始值;

比较原始值和预期值(即CAS操作的第二个参数),如果相等,则将内存地址中的值更新为新值(即CAS操作的第三个参数),并返回true;

如果不相等,则不更新内存地址中的值,返回false。

如果多个线程同时执行CAS操作,只有一个线程能够成功执行CAS操作,并且其他线程会在CAS操作返回false的情况下重新尝试。

CAS操作的原子性是由硬件提供的,通常是通过处理器的原子指令来实现的。在执行CAS操作时,处理器会锁定内存总线,以确保在CAS操作过程中不会有其他线程对共享变量进行修改。这样就可以保证CAS操作的原子性,即使在多线程环境下也不会出现竞态条件。

此外,CAS操作还可以通过操作系统提供的原子性操作来实现。在这种情况下,操作系统会使用类似的机制来保证CAS操作的原子性,即在执行CAS操作时锁定相关的内存地址,以确保在CAS操作过程中不会有其他线程对共享变量进行修改。

```

> CAS是一种操作,包含两个动作。比较、交换。有三个参数,内存地址V(想更新这里的值),预期的原值A,新的值B。

> CAS由硬件设计来保证。通过处理器的原子指令来实现。

> 比较指一直比较V和A的值,如果一致则更新B的值到V。如果V和A的值不一致则说明其他线程已经更新过V了,那当前线程就放弃更新,啥也不做,然后返回当前真实的值(其他线程更新过的值)

> 通过以上原理,当多个线程同时CAS操作一个变量的时候,只有一个线程能更新成功,其他线程都会失败。如果处理得当,不挂起其他线程只是告知它这次操作失败了,然后让它继续尝试,就可以在没有

## 分析 AtomicInteger 类的 getAndIncrement()

    //AtomicInteger
    private static final Unsafe unsafe= Unsafe.getUnsafe();
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

    //Unsafe.java
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
 

 在 unsafe.cpp 这个文件的 Unsafe_CompareAndSwapInt 这个方法里最终调用的是 Atomic:: cmpxchg 方法,找到 atomic_linux_x86.inline.hpp.其中有一句 LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
MP 的意思是 multi processor,意思是在多核 cpu 上,要锁一下这个指令。
到这里,结论是,最终调用了一条汇编指令:lock cmpxchg 指令,来实现底层 cas 的。
也就是 cpu 中有一条 cmpxchg 指令。
但是这条指令不是原子的,也就是拿出来和比较是两个操作,中间有可能被别人打断。
所以需要在这个过程加上 lock,意思是,我在对这个内存操作的过程中,不允许被别人打断。
可以简单理解为把内存总线锁住,别人不允许修改这块内存。

 

## 为什么说CAS是乐观锁?

对于每一次CAS操作,都抱着"V和A一致,更新操作能完成"的期待。


 

## ABA问题

问题描述:

比如线程A从内存位置V中取出1,此时线程B也取出1。且线程A做了一次CAS将值改为了0,然后又做了一次CAS将值改回了1。此时线程B做CAS发现内存中还是1,则线程A操作成功。这个时候实际上A值已经被其他线程改变过,这与设计思想是不符合的。

这个过程问题出在哪?

(1).如果只在乎结果,101不介意0的存在(ABA不介意B的存在),没什么问题

(2).如果0的存在会造成影响,需要通过AtomicStampReference,加时间戳解决。



 

参考:

ChatGPT

深入理解Java虚拟机-JVM高级特性与最佳时间 周志明