前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >jvm源码解析(五)synchronized和ReentrantLock

jvm源码解析(五)synchronized和ReentrantLock

作者头像
JathonKatu
发布2020-10-27 14:39:37
3540
发布2020-10-27 14:39:37
举报
文章被收录于专栏:JathonKatu

一、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 初始状态,正常状态(默认)
代码语言:javascript
复制
public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

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

1、lock:

代码语言:javascript
复制
public void lock() {
    sync.acquire(1); // 调用AbstractQueuedSynchronizer.acquire
}

1.1、AbstractQueuedSynchronizer.acquire:

代码语言:javascript
复制
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

代码语言:javascript
复制
// 保护被注解的方法,通过添加一些额外的空间,防止在多线程运行的时候出现栈溢出
@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

代码语言:javascript
复制
@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:

代码语言:javascript
复制
public void unlock() {
    sync.release(1); // 调用AbstractQueuedSynchronizer.release
}

2.1、AbstractQueuedSynchronizer.release:

代码语言:javascript
复制
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

代码语言:javascript
复制
@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

代码语言:javascript
复制
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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 JathonKatu 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档