java锁的深度实现理解

越学到后面发现操作系统,底层原理息息相关。这篇文章需要你对其有所理解。

1、操作系统(os)

操作系统结构图:(待补充
在这里插入图片描述
现在市面上的,IOS、安卓、鸿蒙等都是底层都是以linux为基础实现的。
常见概念:
1、中断:外设与内核交互方式,鼠标点击、键盘输入等都会发生中断,让内核知道操作命令。软件一样会触发,叫软中断,内核中固定命令0X80中断,还有时钟中断,它是通过时间来控制线程切换。
2、状态0、1、2、3:状态0是表示内核态,可直接操作任何资源,3表示用户态,访问操作系统内核内存需要经过内核态,从而保证了操作系统的安全。
3、进程、线程:进程分配资源,线程进行执行。
4、为什么和内核态交互就会很慢:
在这里插入图片描述

2、jvm和os的交互

2.1 交互图

jvm对操作系统而言,jvm就是os中的一个进程,这个进程中有多线程需要cpu调度、上下文切换,也有I/O流操作需要操作系统等。

2.2 常识

实际上我们java语言的发展,可以说是linux的发展从而才能发展java。
比如:I/O的发展,最开始的I/O是BIO,是linux内核函数socket,bind,listen,recv,这里的recv是阻塞的,等待响应,直到得到响应结果,所以我们的通常的处理是主线程中新建线程处理任务,从而避免主线程阻塞。
后来java出现了NIO也就是New I/O,它的出现是因为linux出现NONBLACKING,以及select、poll、epoll的出现有了多路复用,这就允许了我们将client经过一次系统调用放入,然后轮询处理及时响应,有结果返回,没结果返回0,拿到这个ready状态去处理当前的信息。

1、kernel百度百科

实时操作系统(RTOS)是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统做出快速响应,调度一切可利用的资源完成实时任务,并控制所有实时任务协调一致运行的操作系统(核心)。提供及时响应和高可靠性是其主要特点。

2、主存和缓存一致协议(MSI)

MESI协议为了保证多个CPU cache中共享数据的一致性。L1分成了指令(L1P)和数据(L1D)两部分,而L2则是指令和数据共存。(volatile 就是为了提示那些需要多线程共享)
在这里插入图片描述

3、CAS(compare and swap)

CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B),CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可”

可以从java中开发,涉及到这个的都是native方法,是由c++实现的。为什么要说这个?
因为正是cas+volatile 实现了java.util.concurent里面的锁,锁的升级密切关系着。
cas的简单理解:

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        //期望值是5,且atomicInteger确实是5,true,写入2020
        System.out.println(atomicInteger.compareAndSet(5, 2020)+",当前: "+atomicInteger.get());
        //期望值是5,但atomicInteger已经是2020,false,不改变值,还是2020
        System.out.println(atomicInteger.compareAndSet(5, 1024)+",当前: "+atomicInteger.get());
    }

JDK源码:在unsafe.cpp中:老实说并看不懂

#define UNSAFE_ENTRY(result_type, header) \
	JVM_ENTRY(result_type, header)
// 具体实现由很多,以下为其中一种
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapLong(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jlong e, jlong x))
  UnsafeWrapper("Unsafe_CompareAndSwapLong");
  Handle p (THREAD, JNIHandles::resolve(obj));
  // 内存地址
  jlong* addr = (jlong*)(index_oop_from_field_offset_long(p(), offset));
  if (VM_Version::supports_cx8())
    // Atomic::cmpxchg原子性比较和替换
    return (jlong)(Atomic::cmpxchg(x, addr, e)) == e;
  else {
    jboolean success = false;
    ObjectLocker ol(p, THREAD);
    if (*addr == e) { *addr = x; success = true; }
    return success;
  }

4、常见的命令

java 运行命令、javac 编译命令、javap 反编译命令。这个后面介绍。

5、管程也叫监视器

百度百科:操作系统中管程在功能上和信号量及PV操作类似,属于一种进程同步互斥工具,但是具有与信号量及PV操作不同的属性。管程 (英语:Monitors,也称为监视器) 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程实现很大程度上简化了程序设计。 管程提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。
jvm也利用了这个特点,每个对象都拥有自己的监视锁Monitor。通过monitor实现同步机制、互斥锁机制。
Monitor是基于C++实现的,由ObjectMonitor实现的,其主要数据结构如下:(拿来分析synchornized)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 用来记录该线程获取锁的次数
    _waiters      = 0,
    _recursions   = 0;// 锁的重入次数
    _object       = NULL;
    _owner        = NULL; // 指向持有ObjectMonitor对象的线程
    _WaitSet      = NULL; // 存放处于wait状态的线程队列
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;// 存放处于等待锁block状态的线程队列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
}

他也提供了enter方法和exit方法
大致过程:
当多个线程同时访问一段同步代码时,首先会进入_EntryList队列中,当某个线程获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程,同时monitor中的计数器_count加1。即获得对象锁。

若持有monitor的线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null,_count自减1,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。

6、对象头

1,Mark Word、指向类的指针、数组长度(只有数组对象才有)
主要看Mark Word,32位时:
在这里插入图片描述
为什么要说这个,这个会拿来分析分析锁以及锁的升级。接下来来分析锁。

3、java锁

java锁,从实现上来synchr说分两种,一是依赖jvm指令实现的synchronized,一是cas+volatile实现的java.util.concurrent

3.1 synchronized

synchronized锁方法与锁代码块:

测试代码:

package com.zy.test;

public class Demo {
    public static void main(String[] args) {
        print0();
        print1();
    }

    public static synchronized void print0() {
        System.out.println(0);
    }

    public static void print1() {
        synchronized (Demo.class) {
            System.out.println(1);
        }
    }
}
G:\project\account\src\main\java\com\zy\test>javac Demo.java

G:\project\account\src\main\java\com\zy\test>javap -c -v Demo.class

只复制了主要的部分:

3.1.1 锁方法

 public static synchronized void print0();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: iconst_0
         4: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
         7: return
      LineNumberTable:
        line 10: 0
        line 11: 7

很明显,这里的关键字是ACC_SYNCHRONIZED,官网的说明:jdk8
翻译过来大致是:
同步方法是隐式的。一个同步方法会在运行时常量池中的method_info结构体中存放ACC_SYNCHRONIZED标识符。当一个线程访问方法时,会去检查是否存在ACC_SYNCHRONIZED标识,如果存在,则先要获得对应的monitor锁,然后执行方法。当方法执行结束(不管是正常return还是抛出异常)都会释放对应的monitor锁。如果此时有其他线程也想要访问这个方法时,会因得不到monitor锁而阻塞。当同步方法中抛出异常且方法内没有捕获,则在向外抛出时会先释放已获得的monitor锁
1、同步方法是隐式的。一个同步方法会在运行时常量池中的method_info结构体中存放ACC_SYNCHRONIZED标识符。

flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED

你需要了解class文件的结构:官网ClassFile
在这里插入图片描述
这里面的知识,自己去补,去跟字节码才能看懂,之前的博客有讲的很细。

invocation is wrapped by a monitor use

上面已经讲过了监视器monitor,过程也如上。

3.1.2 锁代码块

 public static void print1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: ldc           #6                  // class com/zy/test/Demo
         2: dup
         3: astore_0
         4: monitorenter
         5: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: iconst_1
         9: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        12: aload_0
        13: monitorexit
        14: goto          22
        17: astore_1
        18: aload_0
        19: monitorexit
        20: aload_1
        21: athrow
        22: return
      Exception table:
         from    to  target type
             5    14    17   any
            17    20    17   any
      LineNumberTable:
        line 14: 0
        line 15: 5
        line 16: 12
        line 17: 22
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 17
          locals = [ class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

关于monitorenrter和monitorexit字节码指令的解释

monitorenrter:
每个对象都会与一个monitor相关联,当某个monitor被拥有之后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor。步骤如下:
每个monitor维护着一个记录着拥有次数的计数器。未被拥有的monitor的该计数器为0,当一个线程获得monitor(执行monitorenter)后,该计数器自增变为 1 。
当同一个线程再次获得该monitor的时候,计数器再次自增;
当不同线程想要获得该monitor的时候,就会被阻塞。
当同一个线程释放 monitor(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。monitor将被释放,其他线程便可以获得monitor。

monitorexit:
当线程执行monitorexit指令时,会去讲monitor的计数器减一,如果结果是0,则该线程将不再拥有该monitor。其他线程就可以获得该monitor了。

3.2 cas+volatile实现的java.util.concurrent

首先,java实现的源码:ReentrantLock为例:
抽离一下大致如下:
public class ReentrantLock implements Lock, java.io.Serializable
有如下属性:

private final Sync sync;

abstract static class Sync extends AbstractQueuedSynchronizer

static final class NonfairSync extends Sync {
	final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
}

static final class FairSync extends Sync {
	 final void lock() {
            acquire(1);
        }
}

3.2.1 核心一:AbstractQueuedSynchronizer:

大致抽离结构如下:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
static final class Node {

        static final Node SHARED = new Node();
		
        static final Node EXCLUSIVE = null;

        static final int CANCELLED =  1;

        static final int SIGNAL    = -1;

        static final int CONDITION = -2;
 
        static final int PROPAGATE = -3;

        volatile int waitStatus;

        volatile Node prev;

        volatile Node next;

        volatile Thread thread;

        Node nextWaiter;

        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

可以看到,这个就是一个queue队列,你也可以说是双链表。而且上面大多属性volatile,这就是上面讲主存和缓存一致协议

3.2.2 核心二:公平锁和非公平锁lock的差距:

if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());

非公平锁:
当线程访问同步代码是,自旋条件为true,换句话说:设置当前拥有独占访问权限的线程。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

如果不是,则加入AQS,并中断当前线程,后面这一不是两个都有的。

代码上的效果如下:
在这里插入图片描述

那么到此CountDownLatch都是大同小异了

public class CountDownLatch {
    /**
     * Synchronization control For CountDownLatch.
     * Uses AQS state to represent count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {

重点就在于这个count怎么实现的:


private volatile int state;

public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }
Sync(int count) {
            setState(count);
        }

protected final void setState(int newState) {
        state = newState;
    }

一眼明了,就是Sync的实现中有个字段再记录这个数量的变化。

4 锁升级过程

有了上面的两个核心,锁的升级也很好解释了:
此外你要理解除了锁本来的目的,涉及频繁系统调用,为了避免频繁系统调用,出现了CAS自旋等待+锁升级。

当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;

如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。

但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值