浅谈ReentrantLock加锁原理以及AQS队列

java对象布局

  • 组成

    • 对象头

    • 数据

    • 填充(如果正好到8的倍数了就不用对其)

  • 64位机器中有12个字节的对象头,对象的大小是8的倍数

什么是java的对象头?

对象的第一个部分,是所有对象都有的

虚拟机位数 头对象结构 说明
32/64bit Markword 存储对象的hashcode、锁信息、分代年龄或gc标记信息
32/64bit klass pointer/Class Metadata Address 类型指针指向对象的类元数据,JVM通过这个指针确定是哪个类的实例

java 对象头由什么组成?

Markword + Class Metadata Address = 12 byte

当前使用的虚拟机是什么? (cmd > java -version 查看)

什么是JVM? 什么是HotSpot?

  • JVM 可以理解成一种 标准/规范
  • HotSpot 可以理解成一种 产品/实现
  • openjdk ------ 项目 / C++ 开发, 可以这样理解openjdk就是 HotSpot 的开源项目

gc 从 survive —> old 要来回折腾16次,也就是数值达到15 因为在对象头中对应4位 0~15

Markword -------- 64bit

Class Metadata Address -----32bit 有的也写成64bit 因为有的VM开启了指令压缩(32bit) 有的没开启(64bit)

两个加在一起不就正好是96bit = 12byte了么

对象状态?

  • 无状态 new出来的时候
  • 偏向锁
  • 轻量
  • 重量锁
  • gc标记

并发编程当中的锁-------ReentrantLock(同步)

为什么要用多线程呢?

因为我们要保证互斥,也就是说我们在开发业务的时候,写在Controller 里面,假如两个客户同时访问我们的网站的某一个变量i,这个时候我们就需要加锁,否则的话我们看到的结果就会不一样。

synchronized (同步) 关键字

synchronized 是一个重量级锁,需要调用OS(在1.6之前)

在jdk1.6之前 原理是一个重量锁

也就是说当我们调用这个线程方法的时候,jvm会调用一个native方法,然后会调用到操作系统函数(mutex),当下一个线程想进来的时候会先阻塞,就是操作系统中讲的那个临界区问题

注:这里的native是本地的意思,本地方法,用c++写的不是用java写的

在jdk1.7以后就完全变成了jvm来实现,不会再涉及到操作系统

ReentrantLock

自旋锁

其实就是操作系统里面讲的那个有一个临界变量,还是用代码写一下吧

volatile int status = 0;

void lock(){
    //compareAndSet的意思就是当是0的时候就变成1
    while(!compareAndSet(0,1)){
        
    }
}

void unlock(){
    status =0;
}

缺点:当线程拿不到锁的时候会在那里空转,也消耗cpu的资源效率太低,当当前的进程数目很多的时候就会产生这样的结果

yield + 自旋

volatile int status = 0;

void lock(){
    //compareAndSet的意思就是当是0的时候就变成1
    while(!compareAndSet(0,1)){
        yield()
    }
}

void unlock(){
    status =0;
}

使用yield的方法还是虽然可以让出CPU,但是CPU调度线程是随机的啊,很可能下一个调用的还是这个线程,但是其他的线程并没有释放锁,所以还是造成了浪费

可以使用park()+自旋的方式

volatile int status = 0;
Queue parkQueue;

void lock(){
    //compareAndSet的意思就是当是0的时候就变成1
    while(!compareAndSet(0,1)){
        park();
    }
    
    unlock();
}

void unlock(){
    status =0;
    lock_notify();
}

void park(){
    //将当前线程加入到等待队列中
    parkQueue.add(currentThread);
    //释放cpu 阻塞
    releaseCpu();
}

void lock_notify(){
    //得到需要唤醒的线程
    Thread t = parkQueue.hreader();
    //唤醒等待线程
    unpark();
}

ReentrantLock

  • 公平
    • 就是当前线程拿着锁,其余的线程来到的时候会在一个队列里面排队,等待这个t1释放锁。
  • 非公平 ReentrantLock 如果你调用的是默认的方法,那么它默认就是非公平锁
    • 就是当前线程拿着锁,其余的线程来到的时候会在一个队列里面排队,等待这个t1释放锁。但是如果当t1释放锁了之后(这里假设t2,t3,t4都在队列中排着队)这个时候t5到达了,那么这个时候我就执行t5

在这里强调一下,这个队列是只有在竞争情况下才有的队列,交替执行的情况下是不存在队列的

交替:t1执行完了!! t2执行

竞争:当t1执行的时候,t2来了,这个时候叫竞争

也就是说只有竞争的时候才会涉及到队列,涉及到了队列我们才会涉及到调度,那么就会用OS内核来调度

ReentrantLock的加锁的源码以及重入锁的体现

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //判断状态
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //比较这个线程是不是持有锁
            //同时也表示重入,也就是说当这个线程是拿到锁的线程,那么我就+1最有的值代表重入几次
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

AQS的队列的实现

注:AQS队列的每一个都是node对象,并且第一个node中的thread是null

下面是jdk的源码

    private Node addWaiter(Node mode) {
        Node node = new Node(mode);

        for (;;) {
            Node oldTail = tail;
            if (oldTail != null) {
                node.setPrevRelaxed(oldTail);
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return node;
                }
            } else {
                initializeSyncQueue();
            }
        }
    }

   public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
原创文章 57 获赞 9 访问量 5952

猜你喜欢

转载自blog.csdn.net/Cscprx/article/details/103955459