Java有关锁的面试题

什么是自旋锁?它可能引起什么问题?使用方面有哪些主页实现

自旋锁(SpinLock): 当一个线程在获取锁的时候,如果锁已经被其他的线程获取,那么该线程就会进入循环等待的状态,然后在不停的询问是否成功的获取锁的资源,一直到获取锁的才会退出循环。通常在CAS(Compare and swap)算法中会使用到自旋锁。
适应性自旋锁: JDK1.6之后对自旋锁进行了改进,引入了自适应自旋锁,随着程序的运行和性能的监控,JVM会对锁的情况进行预测,从而给出合适的自旋时间和更加智能.

它可能引起什么问题:1、如果某个线程持有锁的时间过长,就会导致其他等待的获取锁的线程进入循环等待,消耗CPU,使用不当会造成CPU的使用率极高;2、非公平的情况下,无法满足等待时间最长的线程优化获取锁,不公平的锁会造成线程饥饿问题

使用自旋锁的注意问题:1、自旋锁使CPU处于忙碌状态,因此临界区的执行的时间应该尽量短。2、在高并发写,竞争激烈的场景下,资源的冲突概率大,一般少使用自旋锁。

什么是乐观锁,什么是悲观锁:

问题:悲观锁会造成什么劣势
1、造成阻塞,唤醒的现象,从而造成性能劣势
2、可能造成永久阻塞,也就是死锁
3、阻塞的优先级越高,持有锁的优先级越低,从而导致优先级反转问题
问题:什么是乐观锁,什么是悲观锁
悲观锁:在对数据或线程发生更改的时候就会将数据进行加锁,这种加锁其他操作无法访问该数据或线程
lock锁和synchronized锁
乐观锁:在对数据或线程发生更改的时候不会对数据进行加锁,其他操作和数据也可以访问此数据或线程
示例:悲观锁:select *** for update
乐观锁:git操作的push操作
将悲观锁转化为乐观锁:添加版本号
乐观锁的元始开销比乐观锁的小
适用场景:
乐观锁:并发写入小,大部分的操作是读操作
悲观锁:并发写入多,(可以避免大量的自旋锁等消耗)
临界区有竞争激烈,临界区代码复杂,临界区有IO操作

独占锁和共享锁有何区别?ReadWriteLock的独占锁,共享锁是什么?ReentrantLock呢?

独占锁:只能一个线程获得锁
共享锁:多个线程同时获得锁
synchronized,reentrantlock是独占锁,readwritelock的写锁是独占锁,读取是共享锁
semaphore(信号量),countdownlatch都是共享锁
reentrantlock,semaphore,countdownlatch,reentrantreadwritelock都使用AQS(AbstractQueued Synchronizer)实现独占锁和共享锁。

java对象在hotSpot VM的布局包括哪几个部分:

HotSpot VM是sun JDK自带的虚拟机,也会死目前世界上使用范围最广的java虚拟机,java对象在HotSpotVM中包含对象头,实例数据和补齐填充三部分。
java对象的对象头包含:Mork Word和class pointer
数组对象的对象头包含:Mork Word和class pointer,还有length。
Java有关锁的面试题_第1张图片

对象头:

Mark Word 存储对象的hashCode,锁等信息,比如轻量级锁的标定位,偏向锁标记位等等;在32位系统中占用4个字节,在64位系统中占用8个字节。(我们常说的锁,hashCode都存储在MW)
Klass Pointer(Class pointer):用来指向对应的class对象(其对应的元数据对象)的内存储地址;在32位系统中占用4个字节,在64位系统中占用8个字节。(对象的存储地址在KP当中)
length:如果是数据对象,还有一个保存数组的长度单位,在32位系统和64位系统中都只占用4个字节。

实例数据:

实例数据是对象真正存储的有效信息,就是程序中所定义的各种数据类型和和字段的内容,无论是否从父类继承下来,还是在子类定义的都需要记录下来。

补齐填充:

补齐填充并不是必然存在的,也没有特别的含义,它的作用仅仅是用来作为占位符使用,保证对象头的长长度始终是8字节的倍数。

多线程锁有哪些状态,如何标记锁的状态:

锁有四种状态:无锁,偏向锁,轻量级锁,重量级锁
锁的状态是通过对象的监视器在对象头中的字段来表示的,四种锁会随着竞争的情况逐渐升级,而且该过程不可逆的过程,及该过程是不可降级的。
状态切换是根据竞争激烈的程度进行的,在几乎无竞争的条件下会使用偏向锁,在轻度竞争的条件下,会将偏向锁升级为轻量级锁,在重度竞争的情况下,升级为重量级锁。
Java有关锁的面试题_第2张图片

无锁

没有任何的锁

偏向锁

在大多数的技术场景下,一般由同一个线程多次获取数据,如果一个线程多次获取释放锁,带来了很多不必要的性能开销,为了解决这一问题,java引入了偏向锁。
所谓偏向锁,即锁会偏向于第一个获得它的线程。如果接下来的执行过程中,该锁没有被其他的线程获取,那么持有偏向锁的的线程无需在进行同步,当锁的竞争情况很少出现时,偏向锁会提高性能。

轻量级锁

自旋锁的目标是降低线程切换的成本。如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。

重量级锁

内置锁在Java中被抽象为监视器锁(monitor)。在JDK 1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。

公平锁和非公平锁的区别?为何一般默认设置为非公平锁:

公平锁

加锁前检查是否有排队等待的过程,优先排队等待的过程,先来先得

非公平锁

加锁前不检查是否有排队等待的过程,直接尝试获取锁,获取不到则等待

区别

公平锁的优点是等待锁的线程不会出现线程饥饿而饿死,缺点是吞吐效率相对非公平锁要低。
非公平锁的缺点是等待锁的线程因为长时间不会获取锁而造成饿死现象,或者会等很久才能获取到锁,优点是,整体的吞吐效率较高,减少线程的开销。

显示锁和内置锁在用法上有什么的区别?

显示锁

使用ReentrantLock修饰的锁是显示锁

内置锁

使用synchronized修饰的锁是内置锁

区别:
1、 内置锁使用简单,一切交给JVM处理,不需要显示释放。显示锁在功能上更加丰富,它具有可重入,可中断,可限时,公平锁等特点。
2、 优先考虑使用内置锁。它能够解决大部分的需要同步的场景,只有在需要额外的灵活性,才需要考虑显示锁,比如课定时,可中断,多等待队列等新特性。
3、 显示锁虽然灵活,但是需要进行显示的申请和释放,并且释放一定要放在finally块中进行释放,否则会因为异常导致锁永远的无法释放。

java虚拟机对锁的性能优化有哪些?

自旋锁:并发逻辑
Java有关锁的面试题_第3张图片
锁消除:虚拟机的编译器在运行时,通过运行上下文的扫描,去掉不可能存在的共享资源竞争的锁,可以节省毫无意义的请求锁的时间,提高程序的性能。
锁粗化:把很多次锁的请求合并成一个请求,以降低短时间内大量的锁请求,同步提高性能。
轻量级锁:
偏向锁:

可重入锁reentrantLock加锁和释放锁的底层原理是什么?

ReenTrantLock主要是利用CAS+AQS队列来实现的,它支持公平锁和非公平锁,两者的实现类似,AQS使用volatile变量(命名为state)来维护同步状态。
加锁: 通过ReenTrantLock的加锁方法lock来进行加锁、AQS的底层方法以及CAS自旋等操作实现加锁
释放锁: 通过ReenTrantLock的释放锁的方法unlock进行解锁、AQS底层实现来解锁。

ReentrantReadWriteLock和stampedLock有啥联系和区别?

ReentrantReadWriteLock和stampedLock都是读写锁,后者是前者的完善,后者的性能比前者好
1、增加乐观读功能,减少写线程饥饿的现象出现;
2、stampedLock比ReentrantReadWriteLock损耗小;
3、stampedLock增加了更多的无锁操作,使线程间阻塞减少到最小。
ReentrantReadWriteLock

ReentrantReadWriteLock的核心方法是writeLock,和readLock,
ReentrantReadWriteLock使得多个线程同时持有读锁(只要写锁未被占用),而写锁是独占的,读锁会阻塞写锁,是悲观锁的策略;
容易造成线程饥饿现象,读线程非常多,写线程很少的情况下会造成“写”线程饥饿。

stampedLock

进一步的优化ReentrantReadWriteLock读操作的功能,引入StampedLock
stampedLock的内部实现是基于CLH锁(一种自旋锁,且没有线程饥饿的FIFO(FIFO:First Input First Output的缩写,先入先出队列,这是一种传统的按序执行方法,先进入的指令先完成并引退,跟着才执行第二条指令。))
StampedLock控制锁有三种方式,读,写,乐观读,一个StampedLock状态是由版本和模式组成

linkedBlockingQueue内部的锁设计有何特点?

1、基于链表的实现队列,容量可选,如果不设置为int的最大值、
2、维持两把锁和两个条件,同一时间允许两个线程在两端操作,但是同一时间一个线程只能在一段操作
3、使用链表来完成队列操作的阻塞队列(注意:是单链表不是双链表)
4、采用了两锁队列算法,读只用于对头操作,写只用于队尾操作;
5、对put和offer采用putLock,对poll和take使用takelock,即写锁和读锁
6、对于生产者端和消费者端分别采用了独立的锁来控制数据同步,避免了读写时相互竞争锁的问题

ArrayBlockingQueue内部锁的设计有何特点?

1、基于定长的数组,容量有限制
2、内部实现:只有一把锁和两个条件,同一时间只能有一个线程在一段操作。
3、是一个阻塞队列,底层使用数据机构来实现,按照先进先出(FIFO)的原则对元素进行排序;
4、是一个线程安全的集合,通过ReentrantLock锁来实现,在并发的情况下保证数据的一致性;
5、容量是有限的,它在被创建就已经固定好了数组的大小,不会随着队列的增加而增加,“有界缓存区”

linkedBlockingQueue与ArrayBlockingQueue的区别

Java有关锁的面试题_第4张图片

什么是死锁?如何分析解决死锁?

死锁:两个或两个线程在执行过程中,由于竞争资源或者由于彼此等待而造成的线程阻塞,线程不在下进行
满足死锁的四个条件:
互斥使用:即当资源被一个线程使用时,别的线程不能使用,
不可抢占:资源消费者不能从资源生产者(占有者)夺取资源
占有且等待:即当资源请求其他资源的同时,保持对原有资源的占有
循环等待:即A线程等待B线程占有的资源,而B线程等待A线程占有的资源形成一个环路:
示例代码:

package com.dead;

public class DeadLockTest {
     
    public static void main(String[] args) {
     
        Object a = new Object();
        Object b = new Object();
        new Thread(()->{
     
            synchronized (a){
     
                System.out.println(Thread.currentThread().getName() + " => I get a word");
                try {
     
                    Thread.sleep(2000);
                } catch (Exception e) {
     
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " => I want to get b");
            synchronized (b){
     
                System.out.println( "I get b word");
            }
        }).start();
        new Thread(()->{
     
            synchronized (b){
     
                System.out.println(Thread.currentThread().getName() + " => I get b word");
                try {
     
                    Thread.sleep(2000);
                } catch (Exception e) {
     
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " => I want to get a");
            synchronized (a){
     
                System.out.println( "I get a word");
            }
        }).start();
    }

}

分析排查死锁的方法:

图形化分析工具: jvisualvm
命令行分析: jps jcmd
命令行分析 jps jstack

你可能感兴趣的:(多线程)