多线程常见面试题

1.说一下并发编程的三大要素

        原子性,可见性,有序性
 

        原子性:指的是有一个或者多个操作,这些操作要么不执行,但是只要执行了,就不能被其他线程所打断

        可见性:共享变量在多线程的情况下,有一个线程对其进行了修改,那么其他线程就要立马看到修改之后的结果

        有序性:指代码的执行顺序要跟写代码时候的顺序一样
 

        并发编程的三个要素就导致三个问题:由于cpu上下文切换导致的原子性问题,线程之间的工作内存跟主内存之间的关系导致了可见性问题,还有jvm的重排序优化导致的有序性问题

2.线程池的工作原理:

        用户提交任务,会查看核心线程是否达到最大值,如果没有达到最大值,那么就会创建核心线程来执行任务,如果达到最大值,就会去阻塞队列中看阻塞队列有没有满,如果阻塞队列没有满的话,就添加到阻塞队列中去,要是阻塞队列满了,就看线程数是否达到最大值,如果没有达到最大值,就创建普通线程来执行任务,要达到了最大值,就通过丢弃策略来丢弃无法执行的任务

3.线程的内存模型:

        多线程之间的共享变量是存在主内存的,而每一个线程都会有工作内存,这个工作内存是抽象出来的,不是真实存在的,每一个线程都会读取主内存中的共享数据以副本的形式保存到自己的工作内存中,然后对自己工作内存中的数据进行处理,但是这个处理的结果其他线程是看不到的,只有当前线程可以看得到,这也是线程可见性的一个原因

4.多线程的生命周期:

        首先新创建一个线程对象,然后调用这个对象的start方法,让它进入一个就绪状态,在就绪状态的线程如果被jvm虚拟机调度了,那么就执行run方法进入运行转态。
        在运行状态的线程可能会因为某种原因放弃了cpu 使用权,而进入了阻塞状态,而进入阻塞的情况分为三种:

  • 当前线程使用wait方法,jvm会把当前线程放入等待队列中
  • 当前线程想要获取对象锁的时候,这个对象锁被别的线程所占有,那么jvm会把当前线入到锁池中程放
  • 当前线程使用sleep或者join方法时,也会造成一个阻塞

当线程正常执行完毕,或者出现异常的时候,那么线程对象就会被销毁

5.实现多线程的三种方式:

1.继承Thread类,重写run方法
2.实现runnable接口,实现run方法
3.实现callable接口,实现call方法
runnable和callable的区别:

  • 首先他们都是一个接口,然后runnable接口,实现run方法,callable接口,实现call方法
  • 还有就是call方法是有返回值的,而且还能抛异常,而run方法是没有返回值的,而且也不能抛异常

继承Thread类跟实现runnable接口没有本质区别,主要是因为java类是单继承多实现的,比如a类已经继承了某个父类,然后我想让a类实现多线程,这个时候就只能通过实现接口的形式来实现多线程

6.悲观锁跟乐观锁

        悲观锁跟乐观锁是锁的一种抽象概念,悲观锁认为数据被并发访问修改的概率比较大,所以需要在修改数据之前先加锁。采取的是一种“先取到锁再访问”的保守策略,而乐观锁则是认为数据在一般情况下不会被修改的,所以读取的时候不加锁,但是在更新的时候会做一个判断,判断这个数据是否被别人修改过了,如果没有修改就更新数据,如果有被修改就进行重试,这个判断通常是根据版本号来进行的,但是这样子会存在一个aba问题。

        这个判断的方式是通过CAS来实现的,CAS的基本原理就是:有三个参数,旧的期望值A,要更新的值B,还有一个当前主内存的值V,A值是第一次读取主内存中的值,而V则是要把数值B更新到主内存前读取到的值,当A等于V的时候,才会进行一个更新,要不然就重试,这就是CAS的基本原理

        aba:因为乐观锁会进行版本号判断,假设版本号原本是a,这个a值被别的线程改成了b再改回了a,那么乐观锁对这个版本号进行判断的时候是没有办法知道这条数据是否有被修改过的,这就是aba的问题

7.wait,notify,sleep,join

        wait方法的作用让当前线程释放锁对象,进入阻塞状态,并使得当前线程进入这个对象锁的等待池中,直到notify或notifyAll方法来唤醒线程,被唤醒的线程会进入就绪状态,使用这三种方法的前提是当前线程必须获取到调用方法对象的对象锁,否则会报异常

        sleep是让当前线程休眠指定毫秒,进入超时等待状态,时间过后会重新进入运行状态。

        Join可以把指定的线程加入到当前线程,将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。join方法的底层用的是wait方法
        wait方法跟sleep方法的区别:wait方法是object类的,而sleep方法是Thread类中的方法,wait方法会使用当前线程释放锁对象,并使得当前线程进入这个对象锁的等待池中,等notify唤醒,sleep会让当前线程休眠指定毫秒,并进入阻塞状态,时间过后会重新进入运行状态。
        join方法的底层原理:join方法的底层原理_流连勿忘返的博客-CSDN博客

8.volatile

        volatile可以保证可见性,被volatile修饰的共享变量,只要它在某一个线程中的值被改变了,那么就会把改变后的值强制性的写回主内存,并使其他线程中的这个共享变量失效
当其他线程使用这个共享变量时候就会发现失效,那么就会再次往主内存中读取,就这保证了其他线程读取到的这个共享变量的值一直都是最新的,它还可以禁止重排序优化,但是不能保证原子性

9.说一下公平锁和非公平锁的区别

        公平锁指的是如果有多个线程来获取锁,那么会按照申请锁的顺序去获得锁,线程会进入队列中排队,公平锁可以让所有的线程都能得到资源,但是使用公平锁之后在高并发的情境下的性能不是很好,这是因为只有在队列中的第一个线程才能得到锁,其他的线程会阻塞

        非公平锁指的是多个线程去获取锁的时候,这些线程会直接去尝试获取锁,获取不到的时候会进入等待队列,如果能获取到锁,那么就直接获取到锁。使用非公平锁的时候有可能会让某个线程一直获取不到锁或者长时间获取不到锁,但是非公平锁的效率会比较高

10.可重入锁

        可重入锁指的是某个线程持有了对象锁,当这个线程再次请求对象锁的时候,可以直接获取到这个对象的锁,常见的可重入锁有:ReentrantLock和synchronized,synchronized是隐式可重入锁,ReentrantLock是显式可重入锁,主要的区别就是:ReentrantLock锁要自己手动获取以及手动释放,重入多少次就要释放多少次。

        

public class test {

    public synchronized void aVoid()
    {
        bVoid();
    }

    private synchronized void bVoid() {
        System.out.println("进入bVoid方法");
    }

    public static void main(String[] args) {
        test test = new test();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.aVoid();
            }
        }).start();
    }
}

        在代码中可以看到,创建线程执行了 aVoid 方法,当执行这个方法的时候,当前线程会持有test对象的锁,然后执行 bVoid 方法,bVoid这个方法同样是被synchronized修饰的,所以会在aVoid方法还没结束之前(aVoid结束后会释放锁)重新获取对象锁,要不然就会造成死锁(在执行bVoid方法的时候需要获取锁,如果锁不可重入,那就需要aVoid方法释放锁,才能获取到锁,但是aVoid方法释放锁的前提条件是执行完bVoid方法,所以这个时候会死锁)

11.锁类型

        偏向锁:偏向锁就是锁偏向于当前获取到锁的线程,偏向锁主要是优化在没有线程竞争的情况下,同一个线程多次获取到同一把锁的情况,当已经获取锁的线程再次获取锁的时候,就可以直接进入同步代码块,不用来回切换用户态跟内核态。

        偏向锁的核心原理是:如果不存在线程竞争的一个线程获得了锁,那么锁就进入偏向状态,会把锁对象的锁标志位被改为01,偏向标志位被改为1,然后线程的ID记录在锁对象的Mark Word中(使用CAS操作完成)。以后该线程获取锁时判断一下线程ID和标志位,如果相同就可以直接进入同步块,连CAS操作都不需要,这样就省去了大量有关申请锁的操作,从而也就提升了性能。

        轻量级锁:由偏向锁升级而来,当一个线程获取锁的时候,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,轻量级锁底层是通过自旋锁来实现的,并不会阻塞线程。


        自旋锁:如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等待锁的释放,在等待过程中做自旋,等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。


        重量级锁:如果自旋锁的自旋次数过多仍然没有获取到锁,则会升级成重量级锁,重量级锁会让除了获取到锁的那个线程处于运行状态,其他线程会进入阻塞状态。

        重量级锁原理:底层是使用 monitor 来实现的。每一个 Java 对象都有一个相关联的 monitor,当线程获取锁时,实际上是在访问锁对象的 monitor,如果 monitor 的状态是可用的并且未被持有,那么线程就可以成功获取锁。然后将 monitor 的状态设置为不可用并且把线程的id记录下来。当其它线程获取锁时,会发现锁对象的 monitor 的状态是不用的,那么这些线程会被阻塞并进入到等待队列中等待被唤醒,直到锁的线程释放锁为止。



        锁升级:在同一时间,只有一个线程来竞争锁,那这个时候就是偏向锁,如果有多个线程来竞争锁,那偏向锁会升级成为轻量级锁,如果进行锁竞争的线程CAS自旋操作超过一定次数还没获取到锁,那轻量级锁就会升级成为重量级锁

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值