0


多线程(进阶)(高频八股文型面试题)———javaweb

1 如果synchronized是修饰普通方法的,就针对当前this(当前对象的引用)进行加锁

如果两个线程同时调用这个方法的时候,不一定会触发锁竞争的操作,看是否出发锁竞争就看当前锁住的对象是不是同一个了(如果是不同的对象调用increase,就不会触发锁);

2 如果synchronized是修饰静态方法的,当前锁住的是当前类的类对象,由于类对象是单例的,

所以两个线程并发调用该静态方法就一定会竞争锁;

Java中任意一个对象都可以作为锁对象,这里面包含对象头,对象头包含了一些各种对象的公共属性;

所谓加锁操作,就是把当前指定的锁对象的锁标记设成true,所谓的解锁操作,就是把指定的所对象的锁标记设成false;

如果两个线程针对同一个锁对象对象加锁,此时一个线程会先获取到锁,另一个线程就会阻塞等待;如果两个线程针对不同的锁对象加锁,此时两个线程都会获取到各自的锁;

3 此外synchronized会解决内存可见性的问题,都会把数据真的从内存里面读,这就相当于让程序跑的慢一点,但是算得准,但是volatile(一个线程读一个线程写的场景)只能保证内存可见性,不可以保证原子性;

4 synchronized会有可重入:就是允许一个线程针对一把锁,咔咔加锁两次;

sychronized public void increase()
{
    sychronized(this){//在这里阻塞了,无法释放第一把锁
   count++;
                     }
}
进入increase方法后加了一次锁,进入代码块以后又加了一次锁,按理说会出现问题;
因为第一次加锁后,此时对象头的所标记已经是true,线程就要被阻塞等待,等待这个锁标记改成false,才可以继续竞争这把锁,他会内部记录当前的哪个线程持有这把锁

5 标准库中的集合类(面试题)

对于Arraylist,Linkedlist,HashMap,Hashset都是线程不安全的,不可以在多线程情况下并发修改同一个对象;

但是还有一些线程安全的

vector(动态数组,里面使用了synchronized)(几乎给每个方法都加了锁),这么干并不好,大多数是在单线程下使用,会对单线程环境下的程序的操作效率进行影响;

还有一个hashTable也是把很多方法加了synchronized,不建议使用;

Stack,Concurrenthashmap,stringbuffer都很不好,不建议去使用;

string也是线程安全的,虽然没加锁,是不可变对象,不可能存在两个线程同时修改string的值;

要想修改必须new

6 但是****Synchronized虽然保证了线程安全,但是在性能上并不是最优的,Synchronized会让没有得到锁资源的线程进入

BLOCKED

状态,而后在争夺到锁资源后恢复为

RUNNABLE

状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。

7 常见的锁策略(面试高频考点)

加锁是一个开销比较大的事情,我们希望在特定场景下,针对一些场景进行取舍,好让锁更高效一些

面试题一 你是怎么理解乐观锁和悲观锁的?具体是怎么实现的呢?

乐观锁:假设每次锁冲突的概率比较低,所以在每次的数据提交更新的时候,才会对数据产生并发冲突而去检测

悲观锁:总是假设最坏的情况,锁冲突的概率比较高,每次拿数据的时候都会认为别人会修改,所以在每次拿数据的时候都会上锁,此时就会愿意付出更多的成本来处理冲突;

悲观锁的实现就是先加锁,获取到锁就操作数据,获取不到所就阻塞等待;

前面介绍的Synchronized 其实是以悲观锁为主,其实Synchronized初始使用悲观锁,担当发现锁竞争比较激烈的时候,就会自动切换到悲观锁策略;

使用乐观锁:我们就要引入版本号这个策略

假设当前余额剩了100,我们也引入一个版本号verson初始值是1,并且我们规定,提交版本必须大于当前记录才能执行更新余额。画图理解:

1 线程一此时将其读出(balance=100,version=1),此时线程二也读入此信息(verson=1,balance=100);

线程一工作内存(寄存器) 线程二工作内存(寄存器)

** balance=100 balance=100 version=1 version=1**

** 内存中(balance=100,verson=1)**

2 线程1的操作过程中并从账户余额中扣除50,线程2将账户余额扣除20;

线程一工作内存(寄存器) 线程二工作内存(寄存器)

** balance=50 balance=80 version=1 version=1**

** 内存中(balance=100,verson=1)**

**3 线程1完成修改操作,将数据版本号加1,联合账户被扣除余额,返回到内存中 **

线程一工作内存(寄存器) 线程二工作内存(寄存器)

** balance=50 balance=80 version=2 version=2**

** 内存中(balance=50,verson=2)**

4 因为此时线程二也完成了操作,但是数据一对比才发现,线程二的版本号是2,数据库提供的当前版本也是二,提交版本必须大于当前版本才可以执行更新的乐观锁策略,所以就认为这次操作失败;

对于这个机制来说,如果写入失败就需要重试(要是一直重试的话,效率其实就不高,但是我们是把锁放在冲突概率很低的程序中使用的),之前单纯的互斥的锁(会涉及到线程的阻塞和唤醒),是会涉及到用户态和内核态的交互的,但是这个乐观锁很少涉及到重试;

面试题二:介绍一下读写锁?

正常情况下,多线程之间的同时读取同一个变量不会涉及到线程安全问题,但数据的写入方之间以及读者之间都用到同一个锁,就会产生极大的性能消耗;

就是进行读和写操作分别进行加锁,读锁和读锁之间不会进行互斥,写锁和读锁之间存在互斥,写锁与写锁之间存在互斥,读写锁最适用于读的情况下比较多,写的情况下比较少

Synchronized不是读写锁,注意,只要涉及到互斥,就会产生线程的阻塞等待,再次唤醒就不知道隔了多久了,因此尽可能减少互斥的机会,这是提高效率的重要途径;

在java的标准库中,提供了一个类来创建读锁实例和写锁实例

ReentrantReadWriteLock.ReadLock 能够创建一个读锁实例** ReentrantReadWriteLock.WriteLock 可以创建一个写锁实例**

假设现在有10个线程,t0和t9是写线程,t1到t8是读线程,

** 如果此时 t1和t2两个读线程同时访问数据,两个读加锁并不会互斥,完全并发的执行,就好像从 来没有加过锁一样;**** 但是如果是t0和t1线程同时进行访问,此时读锁和写锁之间就会进行互斥,要么是读完在写,要么是写完再读;**

如果要是t0和t9同时访问,那么此时就会出现锁竞争的操作,必须等到一个线程写完了,下一个线程在写;

3 重量级锁和轻量级锁

重量级锁:加锁解锁开销很大,通常是在操作系统的内核中进行的(悲观锁做的工作往往更多,开销也很大,因此悲观锁很有可能是重量级锁)

轻量级锁:加锁解锁开销更少,通常是由用户态来做的(乐观锁做的工作要少一些,开销要少一些,所以乐观锁很有可能是轻量级锁)

乐观锁和悲观锁描述的是应用场景,看锁的冲突概率高不高;但是重量级锁和轻量级锁描述的是,加锁解锁的开销力度大不大;

加锁这里的能力是怎么来的呢?** 归根结底,是CPU的能力,CPU提供了一些特殊的指令,通过这些指令来完成互斥操作系统内核对这些指令完成了封装,并实现了阻塞等待;CPU提供了一些特殊指令,操作系统对这些指令封装了一层,提供了一个互斥量(mutex); **

如果当前的锁,是通过内核中的mutex来完成的,这样所的开销就比较大;

但是如果是通过用户态,通过一些其他的手段来完成的,这样的所得开销就比较小

synchronized即使轻量级锁,又是重量级锁;

面试题三 介绍自旋锁和挂起等待锁** **

自旋锁:按照之前的方式,线程在抢锁失败后会进入阻塞等待,会放弃CPU需要等到好久才会被调度,这个自旋锁,在枪锁失败后,但过不了多久,所就会被释放,自旋锁,如果获取到锁失败就立即尝试在获取到锁,无限循环,知道获取到锁为止,第一次获取到锁失败,第二次的尝试会在极短的时间内到来

优点:一旦有被释放,就可以第一时间获取到锁;没有放弃CPU,不涉及线程等待和阻塞

缺点:如果所被其他线程占用的时间太长,就会持续地消耗CPU资源(但是挂起等待的时候不会消耗CPU资源)

synchronized中的轻量级锁大概率就是通过自旋锁的方式来实现的。

那么什么时候使用挂起等待锁,什么时候使用自旋锁呢? 1) 如果锁冲突的概率比较低,建议使用自旋锁(锁冲突概率比较高,那么自旋锁一直空转的时间就比较长了,他此时正在忙,忙等情况下是很吃CPU资源的,所以如果你线程冲突概率高的话,那就意味着其他线程就会在这里都尝试获取锁,那对计算机消耗是很大的,负担是很重的); 2) 如果持有锁的时间比较短,建议使用自旋锁; 3) 如果对CPU比较敏感,不希望吃太多的VPU资源,建议使用挂起等待锁

synrchronized中的自旋锁和挂起等待锁,都内置了,会自动适应

4 公平锁和不公平锁

针对公平锁来说:遵从先来后到的规则,谁排队在前面谁就先获取到锁,排在后面就给我干等

通过这个队列来干预操作系统原有的行为。

不公平锁;不遵循先来后到的规则,排在前面和排在后面的线程抢到锁的概率是一样的,对于操作系统的调度器来说默认就是不公平

我们要想实现公平锁,就要有额外的数据结构(例如有个队列,通过这个队列记录先来后到的过程 )大部分情况下,实现非公平锁就够了,但是如果涉及到期望线程的 调度的时间成本是可控的

这个时候就需要公平锁了

例如:例如发射卫星,通过十个线程并发执行十个任务,如果使用非公平锁,就可能出现极端的情况,9个线程霸占锁,第十个线程没获取到锁过,这是系统会崩溃,所以使用公平锁;

正是有这样的需求,才产生了实时操作系统的(线程调度花的时间是可控的)(我们平常使用的都不是实时操作系统)

5 可重入锁和不可重入锁(面试高频考点)

如果针对 同一把锁,卡卡加锁两次 ,如果是不可重入锁,就会陷入死锁问题 如果是可重入锁就不会出现死锁问题

例如来写一个代码:

 static synchronized void run1()
    {
       run2();
    }
    static synchronized void run2()
    {
        System.out.println(1);
    }
这个代码会存在很多问题,这两个方法都在针对同一个对象在加锁,run1获得锁加锁成功,接下来执行fun2,尝试获取到锁,但是run2获取到锁的前提是run1先释放锁呀!但是run2方法在run1方法内部一直阻塞等待,run1方法也无法获取到锁,线程此时就很尴尬
引入可重入概念的时候**,就要解决死锁问题,让当前的锁记录一下这个锁是当前哪个线程持有的,如果当前发现**当前有同一个线程尝试获取锁**,这个时候就让代码能够继续运行而不是出现阻塞等待,同时也在这个锁里面维护一个计数器** **,这个计数器就要记录当前这个线程针对这把锁加了几次锁,每次加锁就让计数器** ++,每次解锁**就让计数器减减,直到计数器是0了,此时才真的释放锁,才可以真的让其他线程获取到锁,synchronized就是一个可重入锁!!!**                                                                                            
标签: 面试

本文转载自: https://blog.csdn.net/weixin_61518137/article/details/123583589
版权归原作者 小比特大梦想 所有, 如有侵权,请联系我们删除。

“多线程(进阶)(高频八股文型面试题)———javaweb”的评论:

还没有评论