当面试官怼你 synchronized 性能差时,你拿这篇文章吊打他(ReentrantLock 与 synchronized 的前世今生)

一天,你进入了一个大厂面试。坐立不安之中,一个秃头中年男子,穿着一个发灰了的格子衬衫,戴着一副镜片厚9mm的眼镜,稳如磐石突然朝着你说到:“就是你这个小毛头来面试吧。”

心里一惊,这怕不是神仙级架构师。但还是故作镇定:“面试官您好,我是xxx…”

面试过程中……面试官随手抛来一句:“简单说说 synchronized 关键字吧”。

简单说说???嗯,面试官人还不错。
再加上我面试前的精心准备,和饱读诗书的才华,和挥斥方遒的潇洒帅气,一定能深深折服他。
于是:“ 这很简单,

synchronized 关键字可以用在方法上,也可以用()括出一个对象作为方法体,保证方法内部的代码是线程安全的,是一种互斥锁。”

面试官再问:

  • synchronized 是重量级锁对吗
  • sync 可重入吗
  • sync 公平吗
  • sync 锁影响性能的原因
  • ReentrantLock 是重量级锁对吗
  • ReentrantLock 与 synchronized 有什么不同知道吗
  • 是不是多行程应该用轻量级锁替换掉 synchronized

这都啥?????
“你回去等通知吧。”

<瑟瑟发抖>

下面进入正题,如果上面的题你都理解,能够熟练回答,那对于 java 并发底层原理你了解得已经比较透彻了。
可能大部分情况程序员习惯于直接对方法加上 synchronized 关键字,来保证安全,但是由于对其底层不了解,往往会忽略性能。

首先聊聊 ReentrantLock 与 synchronized 的故事,你大概就能明白“锁”之间的性能差异
1、sync 被吐槽

在 jdk1.6 版本之前。

  • sync 每次执行都会调用操作系统的锁来保证线程安全
  • 也就是每次执行代码块,都要涉及 “用户态” 到 “内核态” 的转变
  • 所以 sync 就被广大程序猿吐槽,龟速代码

假设有一个方法

public void function() {
    System.out.println("Hello world!");
    // 各种乱起八遭的代码
}

假设它现在在一台服务器上运行平稳,老板觉得很满意。
但是有一天,业务发生了变化,有超多线程来访问它,它变得线程不安全。
一个码农便修改了这段代码,加上了一个 synchronized 关键字,让它线程安全,来防止被老板炒鱿鱼。
但是这个方法,有时候会有多线程访问,可大多数时候都是只有一个线程,时不时触碰一下它的底线。
然后由于 synchronized 的存在,每次运行都如同蜗牛爬行。
最后这位码农就被炒了鱿鱼。

sun 公司于是接到了无数 java 流浪汉的投诉,说 synchronized 效率太低,导致代码龟速爬行,自己被炒了鱿鱼。
在唾沫星子的淹没之中,sun 公司发觉情况不妙,于是专门派人潜入市场查探情况。
据收集到的情报看,一段代码

  • 它有时候会多线程执行
  • 但是大多数时候,都是单线程在执行

所以很多时候其实用不到锁,但是 sync 还是会严重影响性能
所以程序猿们就希望有一种锁,可以在没有线程竞争的情况下,更加轻量。

2、ReentrantLock 横空出世

在 jdk1.6 之前,由于 sync 性能堪忧
于是就有位同学看不惯,他就写了一套技术,他叫

  • 道艮·李(Doug Lea)

他又写了很多类,juc 包下的很多类都是他写的
(juc 不知道的回去补基础)
其中就有大名鼎鼎的 ReentrantLock

  • 速度就要比 sync 要快

加锁这种事情,为什么要去麻烦底层的操作系统呢?
ReentrantLock 于是被他开发出来,去替代 sync 关键字
但是为什么没有替代成功呢?

  • 因为 sync 是 sun 公司亲儿子

但是不得不承认 ReentrantLock 是非常优秀的
在现在看来,也和 sync 不相上下

  • ReentrantLock 快无非就是尽可能在 java 层面解决锁
  • 而不是去劳烦操作系统

比如下面的非公平锁的加锁代码

final void lock() {
    if (compareAndSetState(0, 1)) // CAS将0改为1,可以一步加锁成功
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

(要是这段代码看不懂的先去补补基础吧)
在默认用非公平锁的实现下
根据代码可以发现在只有一个线程执行时

  • 进入 lock 方法,直接将标志位从 0 替换为 1,然后就能直接 lock 成功
  • 一步到位,期间不涉及任何操作系统的内核切换,在 java 层面就已经加锁完成,性能比原来的 sync 蹭蹭蹭不知道快了多少倍

再来看看它的公平锁实现部分代码
(如果你比较懒,可以不用看,直接看我的解释)

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) { // 如果标志位0,说明没线程持有锁
        if (!hasQueuedPredecessors() && // 如果没有线程在排队等锁
            compareAndSetState(0, acquires)) { // CAS改标志位
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

公平锁和非公平锁的区别就是公平锁多了两个判断

  • 判断有没有线程持有(标志位是否为0)
  • 有没有线程在队列等待

然后就 CAS 加锁
如果是单线程的话就直接两个 if,加一个 CAS 就加锁成功了。
同样

  • 一步到位,期间不涉及任何操作系统的内核切换,在 java 层面就已经加锁完成,性能比原来的 sync 蹭蹭蹭不知道快了多少倍

道艮·李(Doug Lea)瞬间被人们封为神坛(太牛了)

sync 优化

sync 被吐槽,ReentrantLock 被吹捧
这时候 sun 公司肯定坐不住了

  • 你要知道,sync 可是 sun 公司的亲儿子
  • 是 sun 公司一手拉扯大的
  • 而现在半路出家的 ReentrantLock 被人吹捧,sync 被人丢弃

sun 公司想着这样下去肯定不行,于是对 sync 进行了大量的优化。
于是 jdk1.7 之后,sync 的性能也得到了提升
sun 公司又开始重用它的亲儿子

  • 后来在 jdk1.8 又将 ConcurrentHashMap 中原本采用 ReentrantLock 改成了 sync 关键字

sync 改动有这么多
(以下内容来自于《深入理解 java 虚拟机》)

  1. 锁消除
    比如
public String concatString(String s1,String s2,String s3){
    return s1+s2+s3; 
}
// 上面的代码会被转化为下面的代码
public String concatString(String s1,String s2,String s3){ 
	StringBuffer sb = new StringBuffer();
	sb.append(s1);
	sb.append(s2);
	sb.append(s3);
	return sb.toString();
}

StringBuffer 的方法是带有 sync 的
(不知道的回去补课)
在执行时候,由于在这段代码中不涉及线程安全
所以会自动优化为
不加锁!

  1. 锁粗话(你看代码就懂)
    (看不懂回去补课)
// 其实就是上面的代码
public String concatString(String s1,String s2,String s3){ 
	StringBuffer sb = new StringBuffer();
	sb.append(s1);
	sb.append(s2);
	sb.append(s3);
	return sb.toString();
}

每一个 append 就是一个 sync
所以会被优化为(看懂就好,只是为了你理解)

sync
	public String concatString(String s1,String s2,String s3){ 
		StringBuffer sb = new StringBuffer();
		sb.append(s1); // 这里的 sync 没啦
		sb.append(s2); // 这里的 sync 没啦
		sb.append(s3); // 这里的 sync 没啦
		return sb.toString();
	}
sync
  1. 偏向锁

偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得 它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程 将永远不需要再进行同步

当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束

  1. 自旋锁
    如果物理机能让两个或以上的线程并行执行,我们可以让后面的线程 “稍等一会” ,暂不放弃处理器的执行时间,而是在一个次数较少的 while 循环中尝试几次加锁,如果很快几次之后加锁成功了,就不用涉及到线程的挂起和恢复,只是稍微花了一点点 CPU。
    自旋次数默认是 10。
    如果一个线程自旋加锁成功,JVM 会觉得这个线程下次自旋加锁成功的概率也会很大,就会稍稍提升一下自旋次数
  2. 锁膨胀
    如果轻量级加锁无法实现,那就只好成为重量级锁了。

文章到这里就结束啦,我还是大学生哦,喜欢学习的小伙伴可以评论交流,或者加关注,一起学习更轻松。
素质三连!

  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值