欢迎光临
我们一直在努力

Java多线程相关部分面试题整理

目录

1.进程和线程的区别是什么?

2.Java当中创建线程的方法有什么?

3.线程不安全的原因

3.1如何解决线程不安全问题

4.synchronized的代码块是可重入的,怎么实现一把可重入的锁?

5.sleep和wait的区别。

6.单例模式

6.2懒汉模式

6.3懒汉模式

7.线程池

7.1ThreadPoolExecutor

7.2Executors

8.锁策略

8.1悲观锁和乐观锁

8.2重量级锁和轻量级锁

8.2等待挂起锁和自选锁

8.3互斥锁和读写锁

8.4可重入锁和不可重入锁

8.5公平锁和非公平锁

9.synchronized的工作流程

10.CAS指令和ABA问题

11.synchronized和ReentranLock的区别(为什么有synchronized,还要使用juc中的lock)?

12.多线程下使用ArrayList

13.多线程下使用哈希表

14.死锁

14.1什么是死锁

14.2死锁形成的条件

14.3如何避免死锁


1.进程和线程的区别是什么?

  • 1.进程是资源分配的最小单位,线程是cpu调度的最小单位。
  • 2.进程包含线程,一个进程可以有多个线程,一个线程只属于一个进程。
  • 3.进程之间不能共享资源,因为线程是在进程的地址中工作的,所有线程之间可以共享进程的所有资源。
  • 4.线程的创建,销毁,调度效率比进程高,并且有自己独立的执行任务。
  • 2.Java当中创建线程的方法有什么?

  • 1.继承Thread类,重写当中的run方法。
  • 2.实现Runnable接口,重新当中的run方法,并把接口的类的实例传给Thread对象。
  • 3.实现Callable接口,,重新当中的call方法,,通过FutureTask的get获取线程的执行结果。
  • 4.使用lambda表达式(最方便)。
  • 5.使用线程池,可以之间使用Executor创建,或者使用选项更多的ThreadPoolExecutor创建。
  • 3.线程不安全的原因

  • cup对线程采用的是随机调度,这个是导致线程安全的之间原因。
  • 多个线程同时修改一个变量。
  • 修改操作不是原子的。
  • 内存可见性问题。CPU对一个变量进行修改(核心分为在内存中加载,在寄存器中读取,最后再写回内存当中),只是在寄存器中进行了修改,还没有来得及返回内存当中。
  • 指令重排序。编辑器和CPU为了优化性能,会对代码的执行顺序进行重排序,在单线程下没有影响,但在多线程下会报错。
  • 3.1如何解决线程不安全问题

    1.不共享资源,每一个线程独享一份资源,从根本上避免竞争。

    2.不修改资源,使用不可变的对象。

    3.保证原子性,使用synchronized关键字进行加锁。

    4.保证可见性和禁止重排序。

    4.synchronized的代码块是可重入的,怎么实现一把可重入的锁?

  • 1.记录一下是哪一个线程进行了加锁。
  • 2.对线程加锁的次数进行计数(可以创建一个变量,记录加锁时遇到左括号++,遇到右括号–)。
  • 5.sleep和wait的区别。

  • 1.wait必须在synchronized中使用,sleep不需要。
  • 2.wait是Object提供的,sleep是Thread提供的。
  • 3.wait需要noitfy进行释放锁,而sleep不需要释放。
  • 6.单例模式

    简单来说,单例模式,就是让一个类,变量等在整个进程中只被修改一次。

    6.2懒汉模式

    "饿"字体现的是一种迫不及待的状态,懒汉模式是在类加载的时候就创建出来,不管外部有没有人来获取实例,先把实例创建好,然后等待被使用。由于是时在类加载的时候创建的所以不涉及线程安全。

    如果一个单例模式体积比较到,在整个程序运行中根本没有用到,但是还没在类加载的时候被创建,此时就会白白浪费资源。

    6.3懒汉模式

    "懒"字体现的是一种能拖就拖的状态,懒汉模式就是类的加载时不创建,当有外部第一次真正要获取实例时才创建 ,如果没有,那永远不创建。

    懒汉模式最好采用,if + synchronized + if的结构,减少线程之间的竞争。

    为了预防内存可见性和指令重排序问题,实例的引用变量最好采用volatile修饰。

    7.线程池

    7.1ThreadPoolExecutor

    7.1ThreadPoolExecutor提供了7个参数。

    new ThreadPoolExector{
    corePoolSize,
    maximumPoolSize.
    keepAliveTime,
    unit,
    workQueue,
    threadFactory,
    handler
    }

  • corePoolSize:表示核心线程,线程池创建的时候就会被创建,只有当线程池全部被销毁的时候,才会被销毁。
  • maximumPoolSiae:表示最大线程数。最大线程数 = 核心线程 + 非核心线程。
  • keepAliveTime:线程允许的最大运行时间。
  • unit:时间单位,时枚举类型。
  • .workQueue:是一个元素为Runnable的阻塞队列,用来存放供给线程运行的任务。
  • threafFactory:线程工厂,可以用来弥补构造方法的缺陷,这个也是一种设计模式,和单例模式是并列关系。
  • hander(重点)拒绝策略,当线程无法处理submit时,决定如何报错。
    • AbortPolicy:默认策略,之间摆烂不干了,直接抛出RejectedExecutionException。
    • CallerRunsPoilcy:线程池没有更多的线程了,让调用submit的线程自行执行任务。
    • DiscardPolicy:抛弃最新的任务。
    • DiscardPoilcy:抛弃最老的任务。

    7.2Executors

    上边的太麻烦了,所有java标准库提供了另一组类,这点ThreadPoolExecutorsk进行进一步封装,简化线程池的使用。

    • newFixedThreadPool:指定线程池的数目,后续不可以改变
    • newCachedThreadPool:线程可以无限叠加。

    8.锁策略

    8.1悲观锁和乐观锁

    悲观锁就是在加锁时候,当有多个线程同时竞争一个锁时,会预测接下来发生的锁竞争会非常激烈,此时就会对这样激烈的情况做出相应的措施(先获取锁资源,在进行数据操作)。

    乐观锁就是在加锁的时候,当有少量线程同时竞争一个锁时,会预测接下来发生的锁竞争不激烈,因此就不需要对接下来的锁竞争做出其他措施(不直接加锁,而是等完成数据更新之后再判断数据是否冲突,如果冲突了,就返回错误信息,进一步确定如何处置)。

    8.2重量级锁和轻量级锁

    重量级锁时当多个线程竞争同一资源时,未获取锁的线程会被操作系统挂起(进入阻塞状态),直到锁被释放后由操作系统唤醒。

    轻量级锁是假设多线程竞争较少,通过CAS(Compare-And-Swap)等原子操作尝试直接获取锁,失败时再升级为重量级锁。

    8.2等待挂起锁和自选锁

       等待挂起锁是重量级锁的典型实现,也就是当线程进行加锁操作时,发现锁竞争非常激烈,此时就会进行阻塞等待,获取锁周期长(一直等,直到获取到锁),并且很难及时获取到锁,这个过程不消耗cpu。

    自选锁是轻量级锁的典型实现,一般不会进入阻塞等待,而是进入盲等状态,它获取锁的周期短(也就是短时间内多次进行加锁申请),并且可以及时获取到锁,但是这个过程会一直消耗cpu。

    8.3互斥锁和读写锁

    普通互斥锁是最简单的锁类型,核心作用就是保证统一时间里只有一个线程能够访问共享资源(Java中常用的就是synchronized)

    读写锁是为了解决“读多写少”的情况,核心原因是读锁之间不会产生线程安全问题,因此不需要加锁,但是写与写之间,读与写之间是线程不安全的,因此读写锁是读锁与读锁之间不互斥,只让读锁和写锁,写锁和写锁之间产生互斥。

    8.4可重入锁和不可重入锁

    可重入锁是允许同一个线程多次获取同一把锁,并在内部通过计数器进行记录重入次数每次加锁时,计数器加1,释放锁是计数器减1,直到计数器为零的时候,才会真正的释放锁。

    不可重入锁也就是不允许同一个线程重复获取锁,如果充分获取,此时会造成死锁或阻塞等待。

    8.5公平锁和非公平锁

    公平锁是线程获取锁的顺序严格按照锁申请的顺序执行,也就是那个线程等待的时间越长,那个线程优先获取锁。

    非公平锁是随机获取,允许插队,新申请的线程也可能直接获取到锁,不需要考虑等待队列中的顺序。

    synchronized是一个互斥锁,可重入锁,非公平锁。

    9.synchronized的工作流程

    总流程:无锁——偏向锁——轻量级锁——重量级锁

    偏向锁并不是真正的锁,而是在锁上边加了一个很轻的标志,如果没有其他线程来竞争这个锁,最终当线程执行到解锁代码的时候,只是将标记清除即可,如果有其他线程来竞争,此时这个标记就会变成真正的锁,也就从偏向锁——轻量级锁,其他线程只能阻塞等待。

    10.CAS指令和ABA问题

    CAS是cpu提供的一条指令,所有是线程安全的,cas进行的操作就是将寄存器中的值与内存中的值进行比较,如果相等,就把内存中的值和另一个寄存器中的值进行交换,这里也可以理解为赋值。

    ABA问题就是因为cas指令从内存中取值这这部分是我们自己的代码进行的,所以线程不安全,因此会出现问题,由一下例子说明:

    余额1000,三个并发操作同时发生: 线程一将内存值1000读入寄存器1,然后暂停,线程二(取500)读内存到寄存器2,比较一致,内存改为500,线程三(存500)读内存到寄存器3,内存改回1000,线程一恢复,寄存器1(1000) == 内存(1000)  再次扣500,内存变为500。 但实际余额应该是1000,这里进行了两次扣款 。

    要解决这种问题,其实就是解决这种值恰好重复的问题,因此我们可以在这里引入一个只增的版本号,只能通过cas指令进行加操作,这样就可以根据版本号是否匹配,判读是否因为aba问题而忽视的变化过程

    11.synchronized和ReentranLock的区别(为什么有synchronized,还要使用juc中的lock)?

    1.synchronized在进入代码块以后,是自动加锁并解锁的,ReentranLock是通过Lock和unLock手动加锁和解锁的,比较灵活。

    2.synchronized等待资源是死等,而ReentranLock提供了lockInterruptidly(),运行中断响应。

    3.synchronized为非公平锁,ReentranLock也默认为非公平锁,但是可以通过传入true表示使用公平锁。

    4.synchronized如果加锁失败会陷入阻塞等待,但是ReentranLock提供了trylock方法,能够在竞争失败后之间返回。

    5.synchronized使用的是Object的wait和notify,但一个锁只能绑定一个等待队列,ReentranLock可以通过Condition接口创建多个条件队列。

    12.多线程下使用ArrayList

    主要分为3种情况:

    1.自行加锁:自己使用synchronized或其他方式加锁。

    2.使用Collections.synchronizedList(new ArrayList)的方式创建一个ArrayList,在线程不安全部分使用synchronized。

    3.使用CopyOnWriteArrayList。复制一个和原数组一样的,在读的时候在原数组中读取,写的时候,在复制数组中操作,操作完成以后,再将原数组的引用改为对复制数组的引用,这样就在不加锁的情况下完成了线程安全是ArrayList。

    13.多线程下使用哈希表

    1.HashMap — 非线程安全 多线程下不加保护会出现数据竞争,因此需要自己加锁保护,key允许为空。

    2.. Hashtable — 全局同步锁 所有方法加了 synchronized,或者线程不安全的地方使用synchronized,其他地方和HashMap一样,key不允许为空。

    3.ConcurrantHashMap.多线程下最好使用。采用给每个桶加锁的方式,实现即保护线程安全,又尽可能降低锁冲突的方式。

    14.死锁

    14.1什么是死锁

    死锁是指在多线程系统中,两个或多个进程(或线程)因争夺资源而陷入无限等待的状态,导致所有相关进程都无法继续执行。每个进程都在等待其他进程释放资源,但自身又不释放已占用的资源,形成循环等待。

    14.2死锁形成的条件

    1.锁是互斥的,一个锁在同一时刻只能被一个线程持有。

    2.锁是不可抢占的,一个线程持有锁是,另一个线程无法获取到那个锁。

    3.请求和保持,线程请求锁资源后,除非获得锁资源,要不就一直等待。

    4.循环等待,a线程持有b的资源,b线程持有c的资源,c线程持有a的资源,形成循环。

    14.3如何避免死锁

    1.把多线程中相互嵌套的锁改成并列的锁(破除请求和保持)。

    2.对加锁的顺序做出约定,线程必须按照锁的优先级从高到低或从低到高的顺序获取锁。

    赞(0)
    未经允许不得转载:171主机测评 » Java多线程相关部分面试题整理
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址