多线程面试八股大总结

目录

一、如何保证线程安全?

二、线程 vs 进程

1、线程的优点

2、进程和线程的区别

三、常见的锁策略

1、乐观锁 vs 悲观锁

2、读写锁

3、重量级锁 vs 轻量级锁

4、自旋锁(Spin Lock)

5、公平锁 vs 非公平锁

6、可重入锁 vs 不可重入锁

面试葵花宝典

1、你是怎么理解乐观锁和悲观锁的,具体怎么实现?

2、介绍下读写锁?

3、什么是自旋锁,为什么要使用自旋锁策略,缺点是什么?

4、synchronized 是可重入锁么?

5、CAS 

1)什么是CAS?

2)CAS 是怎么实现的

3)CAS有哪些应用?

4)CAS 的 ABA 问题

5)高频面试题

6)Synchronized 原理

7) 偏向锁 -->  轻量级锁 --> 重量级锁

7)锁消除 ?锁粗化?

8)Callable 接口

 9)JUC(java.util.concurrent)的常见类

10)原子类

11)线程池

12)信号量

 13)CountDownLatch

14)相关面试题

 15)线程安全的集合类

16)多线程环境使用 ArrayList 

 17)多线程环境使用队列

18)多线程环境使用哈希表

19)相关面试题

20)死锁 

21)如何避免死锁?

22)其他常见问题


一、如何保证线程安全?

1、使用没有共享资源的模型

2、使用共享资源只读不写的模型

1)不需要写共享资源

2)使用不可变对象

3、直面线程安全(重点)

        1)保证原子性

        2)保证顺序

        3)   保证内存可见性

二、线程 vs 进程

1、线程的优点

1)创建一个新的线程的代价要比创建一个新的进程小

2)与进程间切换相比,线程间的切换需要操作系统做的操作小得多

3)线程占用资源比进程少

4)能充分利用多处理器的可并行数量

5)在等待 I/O 操作结束的同时,程序可以执行其他任务

6)计算密集型应用,将计算分解到多个线程中实现

2、进程和线程的区别

1)进程是系统进行资源分配的最小单位,线程是程序执行的最小单位

2)进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈

3)由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行通信

4)线程的创建,切换及终止效率更高

三、常见的锁策略

多线程面试八股大总结_第1张图片

1、乐观锁 vs 悲观锁

悲观锁:

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候都上锁,这样别人想拿到这个数据就会阻塞直到拿到锁

乐观锁:

假设数据一般情况下不会发生冲突,所以在数据提交更新的时候,才会正式对数据是否产生并发进行检测,如果发现并冲突了,则让返回用户错误的信息,让用户决定处理

多线程面试八股大总结_第2张图片

synchronized 初始使用乐观锁策略,当发现锁竞争比较频繁的时候,就会自动切换悲观锁策略

多线程面试八股大总结_第3张图片 乐观锁的一个重要功能 就是要检测出数据是否发生冲突,我们可以引入一个”版本号“来解决

多线程面试八股大总结_第4张图片

多线程面试八股大总结_第5张图片

 多线程面试八股大总结_第6张图片

多线程面试八股大总结_第7张图片2、读写锁

多线程之间,数据的读取方式之间不会产生线程安全问题,但数据的写入方式互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗,所以读写锁因此而生。

读写锁(readers-writer lock) 在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。

一个线程对于数据的访问,主要存在两种操作:读数据和写数据

1)两个线程都只是读一个数据,此时并没有线程安全问题,直接并发的读取即可

2)两个线程都要写一个数据,此时会出现线程安全问题

3)一个线程读另外一个线程写,也有线程安全问题

读写锁就是把读操作和写操作区分对待。JAVA 标准库中提供了 ReentrantReadWriteLock 类,实现了读写锁

多线程面试八股大总结_第8张图片

其中,

多线程面试八股大总结_第9张图片 读写锁 特别适合于”频繁读,不频繁写“的场景中

比如一个教务系统

多线程面试八股大总结_第10张图片

synchronized 不是读写锁

3、重量级锁 vs 轻量级锁

锁的核心特性”原子性“,这样的机制追根溯源是 CPU 这样的硬件设备提供的

1)CPU 提供了”原子操作指令“

2)操作系统基于CPU的原子指令,实现了 mutex 互斥锁

3)JVM基于操作系统提供的互斥锁,实现了 synchronized 和 ReentrantLock 等关键字和类

 多线程面试八股大总结_第11张图片

重量级锁:加锁机制重度依赖 OS 提供的 mutex

 1)大量的内核态用户态切换

2)很容易引发线程的调度

轻量级锁:加锁机制尽可能不用 mutex,而是尽量在用户态代码完成,实在搞不定再用mutex

1)少量的内核态用户态切换

2)不太容易引发线程调度

多线程面试八股大总结_第12张图片

4、自旋锁(Spin Lock)

 按之前的方式,线程在抢锁失败后进入阻塞状态,放弃CPU,需要很久才能被再次调度

但实际上,大部分情况下,虽然当前抢锁失败,但过不了多久,锁就会被释放。没必要放弃这个CPU,这个时候就可以使用自旋锁来处理这样的问题

自旋锁伪代码:

while (抢锁(lock)== 失败) {}

如果获取锁失败,立即再尝试获取锁,无限循环,直到获取锁为止,第一次获取锁失败,第二次的尝试会在极短的时间内到来

一旦锁被其他线程释放,就能第一时间获取到锁,

多线程面试八股大总结_第13张图片

自旋锁是一种典型的 轻量级锁 的实现方式

优点: 没有放弃CPU,不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁

缺点:如果锁被其他线程持有的时间比较久,那么就会持续的消耗CPU资源(而挂起等待的时候是不消耗CPU的)

synchronized 中的轻量级锁策略大概率是通过自旋锁的方式实现的

5、公平锁 vs 非公平锁

多线程面试八股大总结_第14张图片 

多线程面试八股大总结_第15张图片

1) 操作系统内部的线程调度可以视为随机的,如果不做任何额外的限制,锁就是非公平锁,如果要想实现公平锁,就需要依赖额外的数据结构,来记录线程们的先后顺序

2)公平锁和非公平锁没有好坏之分,关键看适用场景

Synchronized 是非公平锁

6、可重入锁 vs 不可重入锁

可重入锁的字面意思是”可以重新进入的锁“,即允许同一个线程多次获取同一把锁。

比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫递归锁)

Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的

而LInux提供的mutex是不可重入锁

多线程面试八股大总结_第16张图片

面试葵花宝典

1、你是怎么理解乐观锁和悲观锁的,具体怎么实现?

多线程面试八股大总结_第17张图片

2、介绍下读写锁?

多线程面试八股大总结_第18张图片

3、什么是自旋锁,为什么要使用自旋锁策略,缺点是什么?

多线程面试八股大总结_第19张图片

4、synchronized 是可重入锁么?

多线程面试八股大总结_第20张图片

5、CAS 

1)什么是CAS?

CAS:全称Compare and swap,字面意思:”比较并交换:,一个CAS涉及到以下操作:

多线程面试八股大总结_第21张图片

 CAS伪代码

下面写的代码不是原子的,真实的 CAS 是一个原子的硬件指令完成的,这个伪代码只是辅助理解CAS 的工作流程

多线程面试八股大总结_第22张图片

两种典型的不是“原子性”的代码

1)check and set (if 判定然后设定值)

2)read and update(i++)

当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号

CAS 可以视为一种乐观锁(或者理解成 CAS 是乐观锁的一种实现方式)

2)CAS 是怎么实现的

 针对不同的操作系统,JVM用到了不同的CAS实现原理,简单来讲:

1)java 的 CAS 利用的是 unsafe 这个类提供的 CAS 操作

2)unsafe 的CAS 依赖了的是 JVM 针对不同的操作系统实现的 Atomic

简而言之,硬件给予支持,软件层面才能做到

3)CAS有哪些应用?

1)实现原子类

多线程面试八股大总结_第23张图片

这里边的 getAndIncrement 相当于 i++ 操作 

伪代码实现:

多线程面试八股大总结_第24张图片

 多线程面试八股大总结_第25张图片

多线程面试八股大总结_第26张图片

多线程面试八股大总结_第27张图片 多线程面试八股大总结_第28张图片

2)实现自旋锁

基于 CAS 实现更灵活的锁,获取到更多的控制权

多线程面试八股大总结_第29张图片

4)CAS 的 ABA 问题

1)什么是 ABA 问题?

ABA 问题:

假设存在两个线程 t1 和 t2 ,有一个共享变量 num,初始值为 A

接下来,线程 t1 想使用 CAS 把值改为 Z,那么就需要

1、先读取 num 的值,记录到 oldNum 变量中

2、使用 CAS 判定当前 num 的值是否为 A,如果为 A,就修改为 Z

多线程面试八股大总结_第30张图片

 2)ABA 问题引来的 BUG

大部分情况下,t2 线程这样的一个反复横跳改动,对于 t1 是否修改 num,是没有影响的,但是不排除一些特殊情况

多线程面试八股大总结_第31张图片

多线程面试八股大总结_第32张图片

3)解决 ABA 问题的方案

给要修改的值,引入版本号,在 CAS  比较数据当前值和旧值之前,也要比较版本号是否符合预期。

多线程面试八股大总结_第33张图片 多线程面试八股大总结_第34张图片

5)高频面试题

1)讲解下你自己理解的 CAS 原理

CAS 全称 Compare and swap ,“比较并交换”,相当于通过一个原子的操作,同时完成“读取内存,比较是否相等,修改内存”这三个步骤,本质上需要 CPU 指令的支撑

2)ABA 问题怎么解决?

给需要修改的数据引入版本号,在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期,如果发现版本号一致就真正执行修改操作,并且让版本号自增。如果发现版本号不一致(数据一定被修改过),操作失败。

6)Synchronized 原理

1) 基本特点

 结合上面的锁策略,我们可以总结出,Synchronized 具有以下特性

1、开始是乐观锁,如果频繁的锁冲突,就转换为悲观锁

2、开始是轻量级锁,如果锁被持有的时间过长,就转换为重量级锁

3、实现轻量级锁的时候大概率用到的自旋锁策略

4、是一种不公平锁

5、是一种可重入锁

6、不是读写锁

2)加锁工作过程

JVM 将 synchronized 锁分为 无锁,偏向锁,轻量级锁,重量级锁状态。会根据情况,依次进行升级

多线程面试八股大总结_第35张图片

7) 偏向锁 -->  轻量级锁 --> 重量级锁

1、偏向锁

第一个尝试加锁的过程,优先进入偏向锁状态

多线程面试八股大总结_第36张图片

多线程面试八股大总结_第37张图片

2、轻量级锁

随着其他线程进入竞争,偏向锁状态被消除,进入轻量级锁状态(自适应的自旋锁)

此处的轻量级锁就是通过 CAS 来实现

多线程面试八股大总结_第38张图片

3、重量级锁

如果竞争进一步激烈,自旋不能快速获取到锁的状态,就会膨胀为重量级锁

此处的重量级锁就是指内核提供的 mutex

多线程面试八股大总结_第39张图片

7)锁消除 ?锁粗化?

1、锁消除

编辑器 + JVM 判断锁是否可消除,如果可以,就直接消除

多线程面试八股大总结_第40张图片

2、锁粗化 

 一段逻辑中如果多次出现加锁解锁,编译器 + JVM 会自动进行锁的粗化

多线程面试八股大总结_第41张图片

多线程面试八股大总结_第42张图片

由此可见,synchronized 的策略是比较复杂的,在背后做了很多事情,目的为了让程序员哪怕什么也不懂,也不至于写出特别慢的程序

8)Callable 接口

1、Callable 的用法

Callable 是一个 interface,相当于把线程封装了一个“返回值”,方便程序员借助多线程的方式计算结果

 多线程面试八股大总结_第43张图片

public class Result {
    static class Res {
        public int sum;
        public Object lock = new Object();
    }
    public static void main(String[] args) throws InterruptedException {
        Res  res = new Res();
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for (int i = 0; i < 1000; i ++) {
                    sum++;
                }
                synchronized (res.lock) {
                    res.sum = sum;
                    res.lock.notify();
                }
            }
        });
        t.start();
        synchronized (res.lock) {
            while (res.sum == 0) {
                res.lock.wait();
            }
            System.out.println(res.sum);
        }
    }
}

多线程面试八股大总结_第44张图片

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Result {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Callable callable = new Callable() {
            @Override
            public Integer call() throws Exception {
                int res = 0;
                for (int i = 0; i < 1000; i ++) {
                    res++;
                }
                return res;
            }
        };
        FutureTask futureTask = new FutureTask(callable);
        Thread t = new Thread(futureTask);
        t.start();
        int res = futureTask.get();
        System.out.println(res);
    }
}

 多线程面试八股大总结_第45张图片

相关面试题

多线程面试八股大总结_第46张图片 9)JUC(java.util.concurrent)的常见类

ReentrantLock

可重入互斥锁,和 synchronized 定位类似,都是用来实现互斥效果,保证线程安全

多线程面试八股大总结_第47张图片

ReentrantLock 和 synchronized 的区别

多线程面试八股大总结_第48张图片

多线程面试八股大总结_第49张图片

 如何选择使用那个锁?

多线程面试八股大总结_第50张图片

10)原子类

 原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个:

多线程面试八股大总结_第51张图片

11)线程池

虽然创建销毁线程比创建销毁进程更轻量,但是在频繁创建销毁线程的时候还是会比较低效

线程池就是为了解决频繁创建销毁线程的问题,如果某个线程不再使用了,并不是真正把线程释放,而是放到一个“池子”中,下次如果需要用到线程就直接从池子中取,不必通过系统来创建。

多线程面试八股大总结_第52张图片多线程面试八股大总结_第53张图片 多线程面试八股大总结_第54张图片

多线程面试八股大总结_第55张图片

多线程面试八股大总结_第56张图片 多线程面试八股大总结_第57张图片

12)信号量

信号量,可以用来表示“可用资源的个数”,本质上就是一个计数器

多线程面试八股大总结_第58张图片 多线程面试八股大总结_第59张图片

public class Worker {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(4);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("我要申请资源了!");
                    semaphore.acquire();
                    System.out.println("我申请到资源了");
                    Thread.sleep(1000);
                    System.out.println("我释放资源了!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 10; i ++) {
            Thread t = new Thread(runnable);
            t.start();
        }
    }

}

 13)CountDownLatch

同时等待 N 个任务执行结束

多线程面试八股大总结_第60张图片

多线程面试八股大总结_第61张图片

14)相关面试题

1、线程同步的方式

synochronized , ReentrantLock ,Semaphore 等都可以用于线程同步

2、为什么有了 synchronized 还需要 juc 下的 lock?

多线程面试八股大总结_第62张图片

3、AtomicInteger 的实现原理是什么?

 多线程面试八股大总结_第63张图片

4、信号量听说过?之前都用在那些场景下?

 多线程面试八股大总结_第64张图片

5、解释一下 ThreadPoolExecutor 构造方法的参数含义

参考:

(306条消息) 面试问我线程池?还好我早有应对_yan扬的博客-CSDN博客https://blog.csdn.net/qq_59539549/article/details/125014470?spm=1001.2014.3001.5501

 15)线程安全的集合类

原来的集合类,大部分都不是线程安全的

16)多线程环境使用 ArrayList 

1、自己使用同步机制 (synchronized 或者 ReentrantLock)

2、Collections.synchronizedList(new ArrayList);

 3、使用 CopyOnWriteArrayList

多线程面试八股大总结_第65张图片

 17)多线程环境使用队列

18)多线程环境使用哈希表

HashMap 本身不是线程安全的

多线程面试八股大总结_第66张图片

多线程面试八股大总结_第67张图片 多线程面试八股大总结_第68张图片

 多线程面试八股大总结_第69张图片

多线程面试八股大总结_第70张图片

19)相关面试题

1、ConcurrentHashMap 的读是否要加锁,为什么?

读操作没有加锁,目的是为了进一步降低锁冲突的概率,为了保证读到刚修改的数据,搭配了 volatile 关键字

2、介绍下 ConcurrentHashMap 的锁分段技术?

3、ConcurrentHashMap 在jdk1.8 做了那些优化?

多线程面试八股大总结_第71张图片 4、Hashtable 和 HashMap ,ConcurrentHashMap 之间的区别?

多线程面试八股大总结_第72张图片

20)死锁 

 1、死锁是什么?

死锁的情形:多个线程同时被阻塞,他们中的一个或多个全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止

多线程面试八股大总结_第73张图片

多线程面试八股大总结_第74张图片

多线程面试八股大总结_第75张图片 多线程面试八股大总结_第76张图片

多线程面试八股大总结_第77张图片 死锁是一种很严重地 BUG !导致一个程序卡死,无法正常工作

21)如何避免死锁?

死锁产生地四个必要条件:

1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用

2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放

3、请求和保持,即当资源请求者在请求其他资源的同时保持对原有资源地占有

4、循环等待:即存在一个等待队列:p1 占有 p2 的资源, p2 占有 p3 的资源,p3 占有 p1 的资源,这样就形成了一个等待环路。

 

 破坏循环等待

最常用的一种死锁阻止技术就是”锁排序“,假设有 N 个线程尝试 同时获取 M 把锁,就可以针对 M 把锁进行编号(1,2,3....M)

N 个线程尝试获取锁的时候,都按照固定的编号由小到大顺序来获取锁,这样就可以避免环路等待

多线程面试八股大总结_第78张图片

多线程面试八股大总结_第79张图片

22)其他常见问题

1、谈谈 volatile 关键字的用法?

 2、java多线程如何实现数据共享?

多线程面试八股大总结_第80张图片

3、java 创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?

多线程面试八股大总结_第81张图片

4、java 线程共有几种状态?状态之间如何切换?

多线程面试八股大总结_第82张图片

5、在多线程下,如果对一个数进行叠加,该怎么做?

6、Servlet 是否是线程安全的?

 7、Thread 和 Runable 的区别和联系?

多线程面试八股大总结_第83张图片 8、多次 start 一个线程会怎么样?

多线程面试八股大总结_第84张图片

9、有 synchronized 两个方法,两个线程分别同时用这个方法,请问会发生什么?

多线程面试八股大总结_第85张图片

10、进程和线程的区别?

你可能感兴趣的:(进程,线程,面试,多线程)