jvm源码解析(五)synchronized和ReentrantLock

时间:2022-07-28
本文章向大家介绍jvm源码解析(五)synchronized和ReentrantLock,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一、Synchronized和ReentrantLock是怎么实现的,他们有什么区别

synchronized属于独占式悲观锁,通过jvm隐式实现,只允许同一时刻只有一个线程操作资源。

java中,每个对象都隐式包含一个monitor(监视器)对象

加锁的过程其实就是竞争monitor的过程

当线程进入字节码monitorenter指令之后

线程将持有monitor对象,执行monitorexit时释放monitor对象

当其他线程没有拿到monitor对象时,则需阻塞等待,获取该对象

ReentrantLock是Lock的默认实现方式之一

是基于AQS(Abstract Queued Synchronizer,队列同步器)实现的,默认是通过非公平锁实现的

内部有一个state的状态字段,用于表示锁是否被占用

如果是0则表示锁未被占用,此时线程就可以把state改成1,并成功获得锁

而其他未获得锁的线程只能排队等待获取锁的资源

区别如下:

synchronized是jvm隐式实现的,而ReentrantLock是Java语言提供的API;

ReentrantLock可设置成公平锁,而synchronized不行

ReentrantLock只能修饰代码块,而synchronized可以修饰方法,代码块等

ReentrantLock需要手动加锁和释放锁,如果忘了释放就会造成资源永久使用

synchronized则无需手动释放锁

ReentrantLock可以知道是否获得了锁,而synchronized不行

两者都提供了锁的功能,具备互斥性和不可见性,在jdk5中,synchronized的性能远远低于ReentrantLock,但在jdk6之后synchronized的性能只是略低于ReentrantLock

MarkWord的字节码:

公平锁与非公平锁:

线程需要按照请求的顺序来获得锁,

非公平锁则允许“插队”的情况存在

插队:线程在发送请求的同时,该锁的状态恰好变成了可用,那么此线程就可以跳过队列中所有排队的线程,直接拥有锁。

频繁的挂起和恢复会造成一定的开销,所以公平锁的性能不如非公平锁,所以ReentrantLock和Synchronized默认都是非公平锁来实现的。

二、大厂高频面试题

ReentrantLock的实现细节是什么

先解释waitStatus的值有哪些,后面用得到

(源码里的注释太长,就不放了)

  • CANCELLED =1 线程被取消了(超时或者interrupt引起)
  • SIGNAL =-1 释放资源后需唤醒后继节点(如果节点为这个状态,后续节点是挂起状态)
  • CONDITION = -2 等待condition唤醒(位于condition队列,不在同步队列中,需要从condition队列中拿出来)
  • PROPAGATE = -3 (共享锁)状态需要向后传播
  • 0 初始状态,正常状态(默认)
public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock是通过lock来加锁,通过unlock来释放锁。

1、lock:

public void lock() {
    sync.acquire(1); // 调用AbstractQueuedSynchronizer.acquire
}

1.1、AbstractQueuedSynchronizer.acquire:

public final void acquire(int arg) {
    // 调用ReentrantLock.NonfairSync或FairSync.tryAcquire
    if (!tryAcquire(arg) &&
        // 独占锁中,获取锁失败:// 将当前线程包装进Node
            // 若当前线程是head节点的后置节点,且head状态为cancelled
            // 则尝试获取锁,若成功,则将head节点设置为null,帮助原来的head节点gc(此时等待队列没有Node了)
            // 否则尝试挂起当前线程
                // 如果前驱节点的状态为Signal,则挂起当前线程
                // 如果前驱节点被cancelled,则尝试找到非cancelled状态的节点,并将他的next设置成当前节点
                // 否则(前驱节点的waitStatus<0),则将前驱节点的状态设置成0
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 调用Thread.currentThread().interrupt();停止线程
        selfInterrupt(); 
}

NonfairSync.tryAcquire调用的是Sync的nonfairTryAcquire,所以我们直接分析nonfairTryAcquire和Fair.tryAcquire,公平锁比非公平锁多了一行!hasQueuedPredecessors() ,用来查看等待队列是否有已经在排队的线程

1.1.1、Sync.nonfairTryAcquire

// 保护被注解的方法,通过添加一些额外的空间,防止在多线程运行的时候出现栈溢出
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取当前锁状态
    int c = getState();
    // 没有线程持有锁
    if (c == 0) {
    // cas尝试将锁状态换成1
        if (compareAndSetState(0, acquires)) {
         // 如果cas成功,则将当前线程设置成当前锁的持有线程
            setExclusiveOwnerThread(current);
            return true; // 返回上锁成功
        }
    }
    // 如果有线程持有锁,且持有线程是当前线程
    else if (current == getExclusiveOwnerThread()) {
        // 锁状态 + 1 可重入锁--可重入的含义
        int nextc = c + acquires;
        // 重入次数溢出,超过Integer.MAX_VALUE导致变成负数
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 设置锁状态
        setState(nextc);
        // 返回获取锁成功
        return true;
    }
    // 已经有线程获取锁,且非当前线程,返回获取锁失败
    return false;
}

1.1.2、Fair.tryAcquire

@ReservedStackAccess
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;
        }
    }
    // 如果有线程持有锁,且持有线程是当前线程
    else if (current == getExclusiveOwnerThread()) {
        // 锁状态 + 1
        int nextc = c + acquires;
        // 重入次数溢出,超过Integer.MAX_VALUE导致变成负数
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 设置锁状态
        setState(nextc);
        // 返回获取成功
        return true;
    }
    // 返回获取失败
    return false;
}

2、unlock:

public void unlock() {
    sync.release(1); // 调用AbstractQueuedSynchronizer.release
}

2.1、AbstractQueuedSynchronizer.release:

public final boolean release(int arg) {
    // 调用ReentrantLock.Sync.tryRelease(1)
    if (tryRelease(arg)) {
        // 获取等待队列的head
        Node h = head;
        // 如果等待队列有node,且线程并不是处于初始状态
        if (h != null && h.waitStatus != 0)
            //调用AbstractQueuedSynchronizer.unparkSuccessor,尝试唤醒等待队列头部线程
            unparkSuccessor(h);
        // 返回解锁成功
        return true;
    }
    // 返回失败
    return false;
}

2.2、ReentrantLock.Sync.tryRelease

@ReservedStackAccess
protected final boolean tryRelease(int releases) {
    // 持有锁状态 - 1(放一次锁,但不一定保证是把锁直接放掉,有可能重入了)
    int c = getState() - releases;
    // 如果当前线程不是锁持有者,则报错
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 设置返回的状态,标识着锁是否完全释放
    boolean free = false;
    // 锁状态为0(完全不持有)
    if (c == 0) {
        // 返回值设置为已释放锁,且锁持有线程设置为null
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 设置锁状态
    setState(c);
    // 返回锁是否已经没有线程持有
    return free;
}

2.3、AbstractQueuedSynchronizer.unparkSuccessor

private void unparkSuccessor(Node node) {
    // 获取等待状态
    int ws = node.waitStatus;
    // 等待状态小于0,则设置为初始状态
    if (ws < 0)
        node.compareAndSetWaitStatus(ws, 0);
    // 获取下一个node(队列中下一个线程)
    Node s = node.next;
    // node为空或者状态为cancelled
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 循环找到下一条能执行的线程
        for (Node p = tail; p != node && p != null; p = p.prev)
            if (p.waitStatus <= 0)
                s = p;
    }
    // 如果有下一个能执行的线程
    if (s != null)
        // 给node增加调用凭证
        // 调用线程的时候会判断,如果凭证不为0则挂起
        // 凭证只能有1个,所以unpark多次也是一样的效果
        LockSupport.unpark(s.thread);
}

JDK1.6时锁做了哪些优化

自适应式自旋锁,锁升级

JDK1.6引入自适应自旋锁,意味着自旋时间不再固定

比如在同一个锁对象上,如果通过自旋等待成功获取了锁,那么虚拟机就会认为,它下一次很有可能也会成功(通过自旋获取到锁),因此自旋等待的时间会比较长,相反,则比较短,甚至直接忽略自旋,避免浪费cpu资源。

锁升级就是从偏向锁,到轻量级锁,再到重量级锁的升级过程。是JDK1.6提供的优化功能,也称为锁膨胀。

偏向锁是指在无竞争的情况下设置的一种锁状态,意思是他会偏向第一个获取它的线程,当锁对象第一次被获取到之后,会在此对象头中设置01表示偏向锁模式,并且在对象头中记录此线程ID。偏向锁可以提高带有同步,但无竞争的程序性能。但如果在多数锁总会被不同线程访问时,偏向锁模式就比较多余。可以通过-XX:-UseBiasedLocking来禁用偏向锁以提高性能。

轻量锁是相对重量锁而言的。

在JDK1.6之前,Synchronized是通过操作系统的互斥量(mutex Lock)实现的,这种实现方式需要在用户态和核心态之间做转换,有很大的性能消耗,这种传统实现锁的方式被称为重量锁

轻量锁是通过比较并交换(CAS,Compare and Swap)来实现的,它对比的是线程和对象的Mark Word,如果更新成功,则表示当前线程成功拥有此锁,如果失败,虚拟机会先检查对象的MarkWord是否指向当前线程的栈帧。如果是,则说明当前线程已经拥有了此锁,否则,则说明此锁已经被其他线程占用,当有两个以上的线程竞争锁时,锁就会膨胀,升级成重量锁。

有两个锁写的不错的相关博客,可以参考

https://www.cnblogs.com/deveypf/p/11406932.html

https://www.jianshu.com/p/73b9a8466b9c