Java锁机制总结

1. 什么是线程安全问题?

多线程操作公共对象时,如何保证对象数据不变脏。

2. synchronized和ReentrantLock锁的区别?

synchronized,在写法上变现为原生语法级别,是非公平锁,可重入锁,java 1.6版本前性能较差,

reentranLock, 也是可重入锁,写法上变现为API级别的锁,相对synchronized有更多高级的功能,主要有一下三个:

可实现公平锁:可以按照申请锁的时间顺序来获取锁

等待可中断:持有锁的线程长期不释放锁的情况下,等待的线程可以选择放弃,改为处理其他事情)。如果是等待可中断,就避免了死锁情况的发生。使用tryLock方法

锁可以绑定多个条件:可同时绑定多个conditon对象。可以像synchronize,wait,notity一样实现生产者消费者机制。

性能方面:java 1.6版本后 ,synchronized和reenTranlock相比,性能差不多

可重入锁可以解决的场景:比如现在有个方法A,它调用了方法B,并且这个两个方法又加了同一把锁。如果锁是不可重入的,那在执行方法B,线程想要再次获取该锁就会被阻塞,方法B也就执行不了啦。

synchronized如果加在方法上,锁的目标其实是这个对象,而不是对象的方法。举个例子,如果一个对象a有非公共方法A和方法B,且A和B都加了synchronized修饰。那么执行a.A的执行与a.B的执行就是互斥的,因为它们都需要争抢锁a。

我们知道sleep方法和wait方法都是可以使得线程阻塞的。让线程阻塞的考虑无非就是想控制代码执行的时间点,来达到控制执行速度或者是线程间配合执行的效果。sleep关键字,想要实现的效果是控制线程执行的时间点,不太考虑与其它线程的配合,所以它的设计也是不释放锁资源的。而wait不一样,从字面上看,wait也是在等待某件事的发生。ok,那么它的设计思路很明显倾向于线程间的配合执行,所以wait也一般和notify配合使用。这也解释了为什么wait是要释放锁资源的,因为如果执行wait的线程不释放锁资源,那与其配合的线程也无获取锁执行代码,从而也无法唤醒自己。

3. java中的锁有什么样的特性?

自旋特性:java1.6版本后,自旋默认开启,阻塞的线程在一段时间内会不断的尝试取获取锁资源,在尝试一定次数之后,如果还不能获取到锁资源,就将此线程挂起。当再次分配到cpu资源时,可以再次启动。

锁消除特性:不可能存在共享的数据,即使使用了加锁,也会被锁消除机制消灭掉。

锁粗化:如果有一系列操作对某个对象反复进行加锁和解锁的操作,会导致很多不必要的性能损耗。虚拟机会对这种场景进行优化,只保留一个加锁,加锁范围扩展到从第一加锁范围开水,到最后一次加锁范围结束

4. java中锁的级别?

轻量锁:从对象头中存放锁的标志位来看,对象被加轻量锁时,标志位是00。轻量锁时具备自旋的特性。

重量锁:从对象头中存放锁的标志位来看,对象被加重量锁时,标志位是10。如果有两条以上的线程争用一个锁,那锁就会从轻量锁升级为重量锁

偏向锁:优化无竞争条件下同步语句的性能,不会通过CAS操作去改变锁标记位,所以此时的锁标记位置还是01(和未加锁时一样),只会偏向第一个获取锁对象的线程,当有另外一个线程尝试去获取锁时,偏向模式结束。java 1.6版本后默认开启。与无加锁的状态对比,加上偏向锁后,对象头会绑上对应的线程id。

将锁分成轻量锁,重量锁,是为了能够实现什么新的特性吗?还是说能有什么性能上的提升?

我们知道,锁是有自旋特性的,获取不到锁的线程会不断尝试若干次去获取锁资源,那如果依然获取不到,才会将线程挂起。这是为了减少上下文切换带来的性能损耗。  但是如果,我们提前知道某个资源竞争非常激烈,现在去获取它肯定是抢不到的,是不是可以减少自旋带来的cpu消耗呢。这就是我理解为什么要将锁的状态分成偏向,轻量,重量的原因。如果无锁,线程可以直接获取到锁资源,并升级为轻量锁;如果是轻量锁,甚至节省不再需要通过多次cas的方式去设置threadID修改偏向锁状态,只需检测是否有指向该线程的偏向锁就可以;如果是偏向锁,其实表明已经有线程持有该锁了,但是没有其它的竞争者了,我再尝试几次获取锁,有可能可以拿到;那如果是重量锁,就表明锁的竞争比较激烈,可以先挂起线程,不用浪费cpu资源去自旋了。

参考:不可不说的Java“锁”事 - 美团技术团队

5. java中的读写锁工具包

ReentrantReadWriteLock,利用ReentrantReadWriteLock对象获取到读锁对象和写锁对象,读操作使用读锁加锁,写操作使用写锁加锁。

6. 什么是死锁? 多线程相互死锁的场景是什么样的?怎么解决死锁?

死锁:线程之间相互持有对方需要的锁资源,导致线程永远处于等待资源的状态。

多线程死锁场景:假设现在有A,B,C三个线程,当前状况是A线程持有锁1,B线程持有锁2,C线程持有锁3。下一步的动作是A想要去获取锁2,B想要去获取锁3,C想要去获取锁1。那么当前线程就是处于相互死锁的状态。

思路:在加锁的时候,指定加锁的时长,时间到了自动释放锁资源;再尝试获取锁资源时,也可以指定等待时长,获取不到锁资源则报错提示。

解决方案:

a) 不同的方法使用不同的对象加锁

b) 如果两个方法必须使用多个相同的对象加锁,那么请加锁的顺序请保持一致。如有线程A,B,有对象m,n。A线程的加锁顺序是m,n,B线程的加锁顺序就一定要是m,n

c) 使用lock.tryLock(时长,单位),尝试一段时间去获取锁,获取到了返回true,获取不到返回false,在finally中将所有的锁释放掉。这样先执行的线程有一端逻辑会执行失败,后执行的线程可以执行成功。(使用具有等待可中断特性的锁,这样获取不到锁资源时,可以中断线程)

案例:db死锁报错,批量更新数据的操作,可能会发生死锁。如果两个线程批量更新的数据存在重叠,并且顺序不同。

7.Object.wait和Thread.sleep有什么不同。

这两个都可以在synchronized代码块中使用。wait会释放线程持有的锁资源,sleep不会释放线程持有的锁资源。

8. 说说Thread中join,yeild,interrupt的作用?

join: 在A线程中调用B线程的Thread.join(), 那么A线程就需要等待B线程执行完毕之后才可执行。控制线程的执行顺序。

yield:只是告诉操作系统可以让其他线程先运行,但是自己可以仍是运行态。执行yield的线程会主动让出cpu时间,但是它可能又立即抢到了cpu时间,所以yield的效果不稳定。

interrupt系列: 这几个方法配合使用,才可以达到中断线程的效果,它们分别是:

interrupt(): 通过thread.interrupt()调用,可以改变线程中断的标志状态。

interrupted(), 通过Thread.interrupted()调用,重制线程的状态。调用之后状态位,会被重新置为false。

isInterrupt(), 通过thread.isInterrupted调用, 可以获取线程的中断状态,中断为true,未中断范围false。调用后不会重置状态位。

8. 线程的5种状态?

新建状态: 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
就绪状态: 当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
运行状态: 如果就绪状态的线程获取 CPU 资源,就可以执行
run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态: 如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞 状态。
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
死亡状态: 一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

在java代码中有通过枚举类来定义这几个状态:

New:线程新建后,但还有start。

RUNNABLE:线程start了,但是还没拿cpu资源

BLOCKED:线程拿不到锁资源

WAITING:调用了object.wait, thread.join, 线程等待状态

TIMED_WAITING:有等待时间的waiting状态,如调用了Thread.sleep()

TERMINED: 线程执行完成。

其中被synchronized阻塞,线程会处于Blocked状态

wait, join, await, sleep , LockSupport.park 不加时间时 线程会处于waiting状态

wait, await, sleep , LockSupport.park 线程将进入timed_waiting状态

这里也可以从主动和被动的角度来理解:

像通过主动的方法调用,比如Thread.sleep,Object.wait ,thread.join 的方式,这样的方式造成线程阻塞,对应java中的线程状态时waiting,如果有加时间参数的话就是timed_waiting.

被动阻塞:其实就是线程执行遇到需要获取锁的情况,但是因为获取不到锁资源,需要暂时挂起。像遇到synchronized,reenterLock.lock这种。

9. 什么是逃逸分析?

可以从两个方向来说:

方法逃逸:一个对象在方法中被定义之后是否会被其他的方法引用,例如作为参数传入其他方法中,称为方法逃逸。

线程逃逸:有一个线程中的变量可以被其他的线程访问到,那就是线程逃逸。

逃逸分析可以帮助做下面3个方面的优化,虽然这个应用还不太成熟。

a. 栈上分配:如果一个对象不会发生方法逃逸,那么这个对象的内存分配是否可以在栈中完成,因为堆中进行内存分配和垃圾回收会比较消耗时间。

b. 同步消除:如果一个对象不会发生线程逃逸的话,那么线程中的加锁操作其实是可以去掉的,因为加锁解锁的操作是会消耗线程资源的。

c. 标量替换:标量是无法分解的数据,如果一个对象不会发生方法或者线程逃逸的话,是否可以将这个对象拆成基本的标量,在栈中完成内存的分配。

10. synchronized的原理是什么样的?或者说synchronized是怎么实现加锁的?

以synchronized使用对象加锁来距举例,在对象的对象头中有个叫锁标记位的东西。

在对象未被加锁,或者处于偏向锁状态的时候是01

轻量级锁:00

重量级锁:10

GC状态:  11

加锁过程:

如果一个锁对象第一次被线程获取的话,采用的是偏向锁的模式,虚拟机会使用CAS操作将线程id记录在对象头中。当有另外一个线程尝试取获取锁的时候,偏向锁模式结束,可进一步升级为轻量级锁。

当线程进入同步代码块的时候,发现对象头的标记位是01,对象没有被锁定。这时虚拟机会在当前线程的栈帧中建立一个锁记录(lock record)的空间,该空间存放了该对象mark world的拷贝。

然后虚拟机会尝试使用CAS操作,将对象的Mark Word更新为指向Lock Record的指针。如果这个操作成功的话,就表示这个线程拥有了这个对象的锁,并且这个时候锁标记位会变成00。

当有两条以上的线程在争抢同一个锁的时候,这个锁会升级为重量级锁,锁标记位变成10。

解锁过程 

将Lock Record中的Displaced Mark Word和对象头中的Mark Word进行交换。交换成功的话就是解锁成功,替换失败说明其它线程在尝试获取锁,在释放锁的同时,需要唤醒挂起的线程。 

11. reentrantLock的实现原理

其原理大致为:当某一线程获取锁后,将state值+1,并记录下当前持有锁的线程,再有线程来获取锁时,判断这个线程与持有锁的线程是否是同一个线程,如果是,将state值再+1,如果不是,阻塞线程。 当线程释放锁时,将state值-1,当state值减为0时,表示当前线程彻底释放了锁,然后将记录当前持有锁的线程的那个字段设置为null,并唤醒其他线程,使其重新竞争锁。

深入剖析ReentrantLock实现原理 - 知乎

12. 使用分布式锁,是否可能存在A线程加锁,但是B线程去释放锁?

分布式锁组件如果设计的不好的话是可能会出现这种情况的。所以如果你要使用redis去设计一个分布式锁,需要注意这个问题。

redisson是如何解决这个问题的。首先redisson使用的redis脚本去进行加锁和解锁的。在加锁时除了带上key外,还会带上一个唯一参数,唯一参数客户端id+线程id构成。解锁时,也会带上唯一id作为条件,如果未唯一id匹配不上是解锁不了的。当然判断锁是否存在时,是不会加上唯一id的。

那等待唤醒的能力是如何实现的。redisson使用了redis的publish/subscribe机制来实现。持有锁的线程在解锁时,通过广播的机制告知订阅者线程解锁了,这时订阅的线程会去重新尝试获取这个锁。

参考:https://www.cnblogs.com/huangwentian/p/14622441.html​​​​​​

13. ReentrantReadWriter 如何实现的读写锁?

首先,写写场景,读写场景都是互斥的。然后它其实是通过状态判断,来实现的互斥。首先当一个线程尝试获取写锁时,它会去判断当前写锁是否已经被持有了,以及读锁已经被持有了,如果是的话,获取锁就不成功。而当获取读锁时,也会判断是否写锁已经被持有,如果有,则获取不成功。

具体内容可看另一篇文章:本地锁(reentrantLock)与分布式锁(reddison)的底层实现-CSDN博客

14. 如何使用synchronized实现读写锁。谁他妈出的傻逼题目。。。。

如何实现读读不互斥:使用map结构,key为线程id,value加锁次数。不同线程id不一样,不会互斥。

如何实现读写互斥:写锁只有一个,如果已经有线程获取到了写锁,写锁回去记录线程id和增加加锁次数。如果一个线程想去获取写锁,需要判断写锁的加锁状态,如果加锁,持有锁的线程id和自己的线程id是否一样。

如何实现读写互斥。获取读锁时去判断写锁的状态,获取写锁时也去判断读写的状态。

然后因为获取锁的过程,并不是一个原子性的动作,会存在并发问题,所以需要synchronized修饰,保障没有并发问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值