面试JAVA基础锁
Posted ClawHub的技术分享
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试JAVA基础锁相关的知识,希望对你有一定的参考价值。
1、锁状态
锁的状态只能升级不能降级。
- 无锁
没有锁对资源进行锁定,所有线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。其他修改失败的线程会不断重试,直到修改成功,如CAS原理和应用是无锁的实现。 - 偏向锁
偏向锁是指一段同步代码一直被一个线程访问,那个该线程会自动获取锁,降低获取锁的代价。 - 轻量级锁
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。通过cas操作和自旋来解决加锁问题,自旋超过一定的次数或者已经有一个线程在自旋,又来一个线程获取锁时,轻量级锁会升级为重量级锁。 - 重量级锁
升级为重量级锁,等待锁的线程都会进入阻塞状态。
2、乐观锁与悲观锁
- 乐观锁,每次拿数据的时候认为别人都不会修改,在更新的时候再判断在此期间有没有更新数据,可以使用版本号等机制,适合读取多场景,提高性能。
悲观锁,每次拿数据都认为别人会修改,都会上锁,可以使用synchronized、独占锁Lock、读写锁等机制,适合写多的场景,保证写入操作正确。
3、自旋锁与适应性自旋锁
- 自旋锁:指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断判断锁是否能获取成功,直到获取到锁才退出循环。
优点:线程不进行上下文切换,减少了上下文切换的时间。
存在的问题:如果线程持有锁的时间较长,其他线程进入循环,消耗cpu。 - 自适应自旋锁:指的是自旋的时间不固定,由前一个在同一个锁上自旋的时间和锁拥有者的状态来决定。如果在同一个对象上,刚刚通过自旋成功获取过锁,且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋很有可能再次成功。反之自旋操作很少成功获取锁,那么后面获取这个锁可能直接省略掉自旋的过程,直接阻塞线程。
4、公平锁与非公平锁
- 公平锁是指多个线程按照申请锁的顺序直接进入队列排队,队列中的第一个线程才能获取锁。
- 非公平锁是指线程先尝试获取锁,获取不到进入队列中排队,如果能获取到,则无需阻塞直接获取锁。
5、重入锁与非重入锁
重入锁:同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,前提是锁对象是相同的。
6、共享锁与排他锁
- 共享锁是指一个锁可以被多个线程锁持有。
- 排它锁或者叫独享锁或者互斥锁 指锁一次只能被一个线程所持有。
7、读写锁
- 读锁是共享的,写锁是独占的。
- 读读之间不会互斥,读写互斥,写写互斥,读写锁提高了读的性能。
8、CAS
CompareAndSwap比较与交换,是一种无锁算法,原子类使用了CAS实现了乐观锁。
带来的问题:
ABA问题
解决思路在变量前面加版本号,每次变量更新的时候都将版本号 1,每次更新的时候要求版本>=当前版本(AtomicStampedReference)- 循环时间长开销大,CAS操作如果长时间执行不成功,会导致其一直自旋,cpu消耗大。
只能保证一个共享变量的原子操作。
可以把多个变量放在一个对象里面进行CAS操作。
9、锁优化
9.1、锁升级
- 偏向锁的升级
线程A获取锁对象时,会在java对象头和栈帧中记录偏向的线程A的id,线程A再次获取锁时,只需要比较java头中的线程id与当前Id是否相等,如果一致则无需通过cas加锁解锁。如果不一致,说明有线程B来获取锁,那么要判断java头中偏向锁的线程是否存活,如果没有存活,锁对象被置为无锁状态,线程B可将锁对象置为B的偏向锁。如果存活,则查看A是否还需要继续持有对当前锁,如果不需要持有,则将锁置为无锁状态,偏向新的线程,如果还继续持有锁对象,则暂停A线程,撤销偏向锁,将锁升级为轻量级锁。 轻量级锁的升级
线程A获取轻量级锁时会把锁的对象头复制到自己的线程栈针中,然后通过cas把对象头中的内容替换为A所记录的地址。此时线程B也想获取锁,发现A已经获取锁,那么线程B就自旋等待。等到自旋次数到了或者线程A正在执行,线程B自旋等待,此时来了线程C来竞争锁对象,这个时候轻量级锁就会膨胀为重量级锁。重量级锁会把未获得到锁对象的线程全部变为阻塞状态该,防止cpu空转。9.2、锁粗化
将多个连续的加锁,解锁操作连接在一起,扩展成为一个范围更大的锁,避免频繁的加解锁操作。
9.3、锁消除
通过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁。
10、synchronized底层实现
- synchronized通过Monitor实现同步,Monitor依赖于底层操作系统互斥锁来实现线程同步。
- java对象头是由markword(标记字段)和klass point(类型指针)组成。markword存储对象的hashcode,分代年龄和锁标志位信息。Klass point 指向对象元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例。
- synchronized修饰同步代码块,是使用monitorenter和monitorexit来控制的,通过java对象头中的锁计数器。
- 修饰方法时会将方法标识为ACCSYNCHRONIZE,JVM通过这个标志来判断方法是不是同步方法。
11、synchronized与ReentrantLock的区别
- 两者都是悲观锁,可重入锁。
- ReentrantLock 可中断,可以实现公平锁,可以绑定多个条件。
- ReentrantLock需要显示的调用锁和释放锁,synchronized属于java关键字,不需要显式的释放。
12、volatile关键字
- 保证变量内存可见。
- 禁止指令重排序。
volatile和synchronized的区别:
- volatile不会阻塞,synchronized会阻塞。
- volatile保证数据的内存可见性但不能保证原子性,synchronized两者都能保证。
- volatile主要解决变量在线程之间的可见性,而synchronized主要解决多线程访问资源的同步性。
13、Atomic原子类实现
使用cas操作 volatile native方法保证同步。
14、AQS
AQS(AbstractQueuedSynchronizer)内部维护的是一个FIFO的双向同步队列,如果当前线程竞争锁失败,AQS会把当前线程以及等待状态信息构造成一个Node加入到同步队列中,同时在阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点线程。使用内部的一个state来控制是否获取锁,当state=0时表示无锁状态,state>0时表示已经有线程获取了锁。
15、AQS的组件
- semaphore 可指定多个线程同时访问某个共享资源。
- countDownLatch 一个线程A等待其他线程执行完成之后才继续执行。
- cyclicBarrier 一组线程等待至某个状态之后同时执行。
countDownLatch和CyclicBarrier的区别
- countDownLatch是一个线程等一组线程执行完成之后才执行, cyclicBarrier是一组线程互相等待至某个状态之后,同时执行。
- countDownLatch不能复用,cyclicBarrier可以重用。
16、锁降级
锁降级是指将写锁降级为读锁,这个过程就是当前线程已经获取到写锁的时候,再获取到读锁,随后释放写锁的过程,这么做的目的为的就是保证数据的可见性。
17、逃逸分析
- 逃逸分析就是分析对象的动态作用域,当一个对象在方法中被定义后,他可能被外部方法所引用,作为参数传递到其他方法中,成为方法逃逸,赋值给类变量或者可以被其他线程访问的实例变量成为线程逃逸。
- 使用逃逸分析,编译器可以对代码做优化。比如:同步省略(锁消除),将堆分配转化为栈分配,标量替换。
- 使用逃逸分析的缺点,没法保证逃逸分析的性能一定高于其他性能。极端的话经过逃逸分析后,所有的对象都逃逸了,那么逃逸分析的过程就浪费了。
以上是关于面试JAVA基础锁的主要内容,如果未能解决你的问题,请参考以下文章