sychronized与lock锁的区别

       相信经历了春招的同学对sychronized与lock应该不会陌生了吧,在面试过程中被问及sychronized与lock锁相关的问题已经算是很常规了,确实作为一个Java开发工程师,了解应用高并发是不可避免的,而高并发则无疑是基于多线程的,至于多线程,与之密切相关的则必然是锁了。正因为锁相关问题在面试与日常学习中的重要性,所以今天就和大家一起来分享一下多线程中关于锁的问题吧~

       从并发的角度,我们一般常见的锁可以分为两种,一种是悲观锁,它是通过sychronized实现的,另一种则是乐观锁,它是通过lock实现的,首先我们来看一下它们有什么区别......

类别synchronizedLock
存在层次Java的关键字,在jvm层面上是一个类
锁的释放1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁在finally中必须释放锁,不然容易造成线程死锁
锁的获取假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待
锁状态无法判断可以判断
锁类型可重入 不可中断 非公平可重入 可判断 可公平(两者皆可)
性能少量同步大量同步

知道了这两种锁的区别,我们继续看它是怎样实现的吧~

       synchronized锁是通过映射成字节码指令时增加两个指令:monitorenter和monitorexit。当一条线程进行执行的遇到monitorenter指令的时候,它会去尝试获得锁,如果获得锁那么锁计数+1(为什么会加一呢,因为它是一个可重入锁,所以需要用这个锁计数判断锁的情况),如果没有获得锁,那么阻塞。当它遇到monitorexit的时候,锁计数器-1,当计数器为0,那么就释放锁。至于Lock锁则是JDK 5之后新增的锁机制,不同于内置锁,Lock锁必须显式声明,并在合适的位置释放锁。Lock是一个接口,其由三个具体的实现:ReentrantLock、ReetrantReadWriteLock.ReadLock 和 ReetrantReadWriteLock.WriteLock,即重入锁、读锁和写锁。增加Lock机制主要是因为内置锁存在一些功能上局限性。比如无法中断一个正在等待获取锁的线程,无法在等待一个锁的时候无限等待下去。内置锁必须在释放锁的代码块中释放,虽然简化了锁的使用,但是却造成了其他等待获取锁的线程必须依靠阻塞等待的方式获取锁,也就是说内置锁实际上是一种阻塞锁,而新增的Lock锁机制则是一种非阻塞锁。

       下面大家来看一下J.U.C中locks包下的lock接口中定义了哪些方法:

package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}
  • lock():获取锁,如果锁被暂用则一直等待;

  • unlock():释放锁;

  • tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true;

  • tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间;

  • lockInterruptibly():用该锁的式,如果线程在获取锁的阶段进入了等待,那么此线程则可以被中断。   

       今天就跟大家简单介绍一下lock接口,关于lock的实现也是特别重要的,这部分内容我会再用一篇博文详细介绍,这里重在讲一下Lock锁与sychronized的区别~

       那么在实际应用中我们究竟应该使用哪一种锁呢,答案当然是根据情况适当选择啦,毕竟存在即合理哈,每种锁都有自己的优点,众所周知,在jdk1.7版本对sychronized做了很大的优化,所以我们首先来看一下具体做了哪些优化再来分析它们的优劣。

1)线程自旋和适应性自旋
       我们知道,java线程其实是映射在内核之上的,线程的挂起和恢复会极大的影响开销。并且jdk官方人员发现,很多线程在等待锁的时候,在很短的一段时间就获得了锁,所以它们在线程等待的时候,并不需要把线程挂起,而是让他无目的的循环,一般设置10次。这样就避免了线程切换的开销,极大的提升了性能。而适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋10次一下。他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。

2)锁消除
       什么叫锁消除呢?就是把不必要的同步在编译阶段进行移除。 那么有同学又要迷糊了,我自己写的代码我会不知道这里要不要加锁?我加了锁就是表示这边会有同步呀? 并不是这样,这里所说的锁消除并不一定指代是你写的代码的锁消除,我打一个比方: 在jdk1.5以前,我们的String字符串拼接操作其实底层是StringBuffer来实现的(这个大家可以用我前面介绍的方法,写一个简单的demo,然后查看class文件中的字节码指令就清楚了),而在jdk1.5之后,那么是用StringBuilder来拼接的。我们考虑前面的情况,比如如下代码:

String str1="qwe";
String str2="asd";
String str3=str1+str2;

       底层实现会变成这样:

StringBuffer sb = new StringBuffer();
sb.append("qwe");
sb.append("asd");

       我们知道,StringBuffer是一个线程安全的类,也就是说两个append方法都会同步,通过指针逃逸分析(就是变量不会外泄),我们发现在这段代码并不存在线程安全问题,这个时候就会把这个同步锁消除。

3)锁粗化
       在用synchronized的时候,我们都讲究为了避免大开销,尽量同步代码块要小。那么为什么还要加粗呢?
我们继续以上面的字符串拼接为例,我们知道在这一段代码中,每一个append都需要同步一次,那么我可以把锁粗化到第一个append和最后一个append(这里不要去纠结前面的锁消除,我只是打个比方)

4)轻量级锁

       synchronized会在对象的头部打标记,这个加锁的动作是必须要做的,悲观锁通常还会做许多其他的指令动作,轻量级锁希望通过CAS实现,它认为通过CAS尝试修改对象头部的mark区域的内容就可以达到目的,由于mark区域的宽度通常是4~8字节,也就是相当于一个int或者long的宽度,是否适合于CAS操作。

5)偏向锁
       简单的说,就是在JVM内部,如果一个对象作为synchronized的锁对象,当一个线程获取这个锁时,会将线程id保存到这个锁对象的对象头上,当紧接着下一次申请获取这个锁的线程还是之前的线程时,只需要比较对象头中的线程id,而不需要做其他锁相关的操作。

       基于上述的jdk7的优化再来讨论,sychronizedsynchronized的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程的出列速度;当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优化,而Lock则完全依靠系统阻塞挂起等待线程。当然Lock比synchronized更适合在应用层扩展,可以继承AbstractQueuedSynchronizer定义各种实现,比如实现读写锁(ReadWriteLock),公平或不公平锁;同时,Lock对应的Condition也比wait/notify要方便的多、灵活的多。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值