浅谈Java锁

最近被遇到很多Java锁的问题,实在是有点头痛,什么互斥锁、自旋锁、偏向锁、轻量锁、重量锁、公平非公平……今天小小的总结一下Java里的那些锁。

各种锁一览

首先总结一下锁的种类,具体如下:

(xmind用免费版不会找我吧)

在这里插入图片描述

乐观锁&悲观锁

乐观锁

乐观锁可以从字面理解,就是一把乐观的锁,他觉得他遇不到并发的情况,在每次操作数据的时候,认为别人不会动他的数据(修改数据),所以也不上锁,但也不是什么也不干,在更新的时候会判断一下版本号,具体方法是,写数据操作时先读取当前版本号,然后更新时确定版本号是否一致,若一致则更新,否则报错 或者 自动重试;还可以通过CAS实现,后面会讲到。

悲观锁

悲观锁也可以一样理解,就是觉得别人会动他的数据,每次获取到数据就上一把锁,确保别人动不了(确保数据不会被别的线程修改)。Java中常见的悲观锁实现有synchronized关键字和Lock

适用场景

乐观锁适合读多写少的场景,不加锁意味着效率高,可以提高吞吐量。

悲观锁适合写多读少的场景,加锁保证操作数据不会有并发问题。我们常见的传统关系数据库中的行锁、表锁都是操作前就上锁了。

乐观锁的两种实现方式

乐观锁在Java中主要是采用无锁编程实现,最常用的是CAS算法。

版本号机制

在数据表中加入版本号,如果数据被修改,则版本号++,每次更新数据时比较当前版本号和之前的版本号是否一致,一致更新,反之重试操作。

CAS(浅谈)

什么是CAS,CAS(compareAndSet)–>比较并交换。简单的来说就是比较当前工作内存中的值和主存中的值,如果这个值是我期望的,那就执行操作,如果不是,就一直循环,知道我想要的来了为止。

举例:比如变量i初始值为0,AB两个线程同时执行i++操作,显然两者有一个会先执行,假设A先执行带有CAS的自增操作,发现当前的i和预期的值一致,就完成++操作,此时i=1;然后B再执行CAS操作,发现i和预期值(之前取的值)不一致了,则此次操作失败,重新取值,重新CAS,第二次操作成功,i++,两个线程执行两次使得i增加两次,符合我们的预期。

作用:利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。而整个J.U.C都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。

缺点:

1、经典的ABA问题。比如当一个内存位置v被A读取两次,两次读取具有相同的值,但值相同不能代表v未发生改变,因为B可以在两次读取间对v做任何操作,然后再改回A读到的初始值,这样A就会被骗,这就产生了问题。如何解决?加入版本号就行啦。

2、每次都要等到想要的东西来啊,等啊等显然很消耗系统资源,这里有一个自旋的词,解释一下,自旋就是CAS的一个操作周期,如果一个线程特别倒霉,比如上面的AB,A一直加,不给B操作的机会,那B就一直傻等,对CPU的开销十分大,所以要尽可能避免。

悲观锁的实现(重要)

synchronize和Lock,偷偷说一声面试问这两个最多,必须掌握。

synchronized

synchronized可以保证方法或者代码块在运行时同一时刻只有一个线程能进入临界区,同时保证共享变量对其他线程的可见性。在Java中每一个对象都可以作为锁,这也是synchronized实现同步的基础。常见的有以下三种使用方式:

1、修饰代码块,即同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。

2、修饰普通方法,即同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。

3、修饰静态方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。

这里不再展开,常见的有8锁问题

具体使用方法还可以看

Lock方法

与synchronized的,Lock是一个接口类,使用时需要new一个实现了其接口的ReentrantLock。

常用方法有lock(),unlock(),tryLock(),lockInterruptibly()等.

Synchronized和Lock区别

1、Synchronized 内置的Java关键字, Lock 是一个Java类

2、Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁

3、Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁

4、Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去;

5、Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以自己设置);

6、Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码。

这里还有一个Lock讲的比较好链接

自旋锁&适应性自旋锁

自旋锁

所谓自旋锁,就是让某线程进入已被其它线程占用的同步代码时等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。这里等待的方式就是执行一段无意义的循环。

适应性自旋锁

适应性自旋锁是自旋锁的优化版。某个线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,直接阻塞线程,以免浪费处理器资源。

无锁&偏向锁

无锁

无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

偏向锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

有点偏心的意思,它的主要目标就是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗

偏向锁在JDK6级以后是默认启用的。如果真想去操心偏向锁的开关的,可以去这里

偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,那就违背偏向锁的初衷了。

重量级锁&轻量级锁

轻量级锁

是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

重量级锁

自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,重量级锁就是java最原始的同步锁,在这个状态下,未抢到锁的线程都会被阻塞,等待被激活。

公平锁&非公平锁

公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

可重入锁&非可重入锁

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

非可重入锁

不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。不可重入锁,可能存在被当前线程所持有,且无法释放的死锁问题。

共享锁&排它锁

排它锁和共享锁同样是一种概念。

共享锁

共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

排他锁

排他锁也叫独享锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

AQS详见

常见面试题

1、synchronized和java.util.concurrent.locks.Lock的异同。见下

2、java中锁和同步的区别(其实就是第一题)

用法不同:

synchronized既可以加载方法上,也可以加在特定的代码块上,而lock需要显示的指定起始位置和终止位置。

synchronized是在JVM内实现的,而lock是通过代码实现的,lock比synchronized有着更加精确的语义。

性能上的不同:

lock接口的实现类ReentrantLock不具有synchronized共同的并发性和内存语义,还多了超时获取、定时、等待、中断等。

在竞争不激烈的情况下,synchronized的性能优于ReentrantLock,竞争激烈下synchronized的性能会显著下降,而ReentrantLock几乎不受影响。

锁机制的不同:

synchronized获取锁和释放锁的方式都在块结构中,当获取多个锁时,必须以相反的顺序释放,并且是自动解锁。而Lock则需要开发人员手动释放,并且必须在finally中释放,否则会引起死锁。

3、synchronized的可重入锁如何实现?

每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。

4、为什么说synchronized是非公平锁?

非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁 ,这样做的目的是为了提高执行性能,缺点是可能会产生 线程饥饿现象 。

5、什么是锁消除和锁粗化?

锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。程序员怎么会在明知道不存在数据竞争的情况下使用同步呢?很多不是程序员自己加入的。

锁粗化:原则上,同步块的作用范围要尽量小。但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁地进行互斥同步操作也会导致不必要的性能损耗。锁粗化就是增大锁的作用域。

6、讲讲三个线程同步器

点我

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

上上签i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值