Java多线程与并发面经汇总

并发编程的优缺点

优点:

并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度

缺点:

并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如**:内存泄漏、上下文切换、线程安全、死锁**等问题。

并发编程三要素是什么?在Java中怎么保证多线程的运行安全?

并发编程的三要素:

  • 原子性:是指一个或多个操作要么全部执行成功要么全部执行失败。
  • 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。
  • 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

出现线程安全问题的原因:

  • 线程切换带来的原子性问题
  • 缓存导致的可见性问题
  • 编译优化带来的有序性问题

解决方法:

  • JDK Atomic开头的原子类、synchronized关键字、Lock接口可以解决原子性问题
  • synchronized关键字、volatile关键字、Lock接口可以解决可见性问题
  • Happens-Before 规则可以解决有序性问题

并行与并发的区别

并发:同⼀时间段,一个CPU执行多个任务。

并行:单位时间内,多个CPU同时处理多个任务。

串行:多个任务由单个线程按顺序执行。

进程与线程的区别

进程是程序的一次执行过程,或正在运行的程序。系统运行一个程序,即为一个进程的创建、运行以及消亡的过程。

线程是程序的一条执行路径。线程又称轻量级进程。

  • 进程是系统运行程序的基本单元,而线程是比进程更小的执行单位;
  • 进程之间相互独立,不能共享资源,一个进程在执行过程中会产生多个线程,多个线程之间共享进程的方法区;每个线程都有自己的程序计数器、虚拟机栈、本地方法栈

sleep 和 wait 有什么区别

  • wait方法与synchronized一起使用,并且会释放对象锁,sleep方法不会释放对象锁;
  • wait是Object类的方法;sleep是Thread类的方法;
  • 调用wait方法后,线程会变为 WAITING 等待状态;调用sleep方法后,线程会变为 TIMED_WAITING 超时等待状态;
  • 进入wait状态的线程需要使用 notify 或 notifyAll唤醒;而sleep状态的线程过了等待时间就会自动被唤醒;

为什么sleep不会释放锁,wait会释放锁

为什么 wait、notify、notifyAll必须在同步方法或者同步块中被调用

当一个线程需要调用wait方法时,它必须拥有该对象的锁,接着它就会释放这个对象锁并进入阻塞状态,直到其他线程调用这个对象上的notify方法来释放这个对象锁,让其他在等待的线程能够获取到对象锁。由于这些方法都需要线程持有对象的锁,所以只能通过同步实现。

sleep与yield的区别

  • 执行sleep方法后进入阻塞状态,执行yield方法后进入就绪状态;
  • sleep方法声明抛出 InterruptException异常,yield方法没有声明异常;
  • yield方法会给相同或更高优先级的线程运行的机会;sleep方法不考虑线程的优先级;

interrupted与isInterrupted方法的区别

  • interrupt:用于中断线程。调用该方法后线程的状态将变成中断状态。
  • interrupted:静态方法,查看当前中断信号并清除中断信号。若一个线程被中断,第一次调用返回true,之后调用会返回false;
  • isInterrupt:查看当前中断信息是true还是false;不清除中断信号

注:线程中断只是给线程设置一个中断状态,不会停止线程。一旦线程的被设置为中断状态,就会抛出中断异常。

notify与notifyAll方法的区别

调用了对象的wait方法,线程就会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

notifyAll会唤醒所有线程由等待池转移到锁池,然后参与锁的竞争,竞争成功则继续执行,否则留在锁池等待锁被释放后再次参与竞争。

notify只唤醒一个线程,由JVM决定唤醒哪一个线程。

线程的生命周期

  • NEW :尚未启动的线程状态
  • RUNNABLE :在Java虚拟机中执行的线程状态
  • BLOCKED :被阻塞等待监视器锁定的线程状态
  • WAITING :正在等待另一个线程执行特定动作的线程状态
  • TIMED_WAITING :正在等待另一个线程执行动作达到指定等待时间的线程状态
  • TERMINATED :已退出的线程状态

start方法与run方法的区别

start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。

start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。

run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。

为什么调用start方法会执行run方法,为什么不能直接调用run方法

调用start方法可以启动线程并使线程进入就绪状态,而run方法只是线程的⼀个普通方法,还是在主线程中执行。

线程死锁

死锁:线程A持有线程B想要的资源,线程B持有线程A想要的资源,他们同时想要申请对方的资源,造成这两个线程相互等待而进入死锁状态。

形成死锁必要条件:

  • 互斥条件:在一段时间内某资源仅为一进程所占用。
  • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  • 环路等待条件:在发生死锁时,必然存在一个进程资源的环形链。

如何避免线程死锁:

破坏互斥条件

这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

破坏请求与保持条件

一次性申请所有的资源。

破坏不剥夺条件

占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

破坏循环等待条件

靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

死锁与活锁的区别

活锁:任务没有被阻塞,但是由于某些条件不满足,导致一直重复尝试、失败、尝试、失败。

处于死锁的表现为一直等待,处于活锁的表现为不断的改变状态,活锁有可能自行解开,死锁则不可能;

死锁与饥饿的区别

饥饿:一个或多个线程因为其他原因一直无法获取需要的资源,导致一直无法执行的状态。

Java 中导致饥饿的原因:

1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。

2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。

3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。

创建线程的方式

  • 继承 Thread 类;
  • 实现 Runnable 接口;
  • 实现 Callable 接口;
  • 使用 Executors 工具类创建线程池

如果你提交任务时,线程池队列已满,这时会发生什么

  • 若使用无界队列 LinkedBlockingQueue,那么可以继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 无限存放任务。
  • 若使用有界队列,比如ArrayBlockingQueue,任务首先会添加到有界队列中,若ArrayBlockingQueue已满,则会根据 maximumPoolSize 的值增加线程数量,若增加的线程数量还是无法满足,那么则会使用拒绝策略RejectedExecutionHanlder处理满了的任务,默认是AbortPolicy。

为什么代码会重排序

  • 在单线程下不能改变程序运行的结果;
  • 存在数据依赖关系的不允许重排序

as-if-serial规则和happens-before规则的区别

  • as-if-serial规则保证单线程内程序的执行结果不被改变;happens-before规则保证多线程程序的执行结果不被改变;
  • as-if-serial规则使单线程程序按顺序执行;happens-before规则使多线程程序按happens-before指定的顺序执行;
  • as-if-serial与happens-before规则的目的是为了在不改变程序执行结果的情况下,尽可能提高程序的执行并行度

Java中的锁

公平锁和非公平锁

公平锁:多个线程在等待同一个锁时,必须按申请锁的先后顺序来获得锁。

非公平锁:多个线程相互竞争(抢占式)来获得锁。

注:公平锁可以使用java中ReentrantLock类实现。例如new ReentrantLock(true)

自旋锁

说白了就是让一个线程执行一个 while(true) 的无限循环。如果一个线程自旋转超过限定次数(默认10次)还没有成功获取锁资源,就会挂起线程。

特点:若锁被占用的时间很短,自旋等待的效果就会非常好;若锁被占用的时间过长,自旋的线程就会浪费CPU资源,降低性能。

可重入锁

允许同一个线程多次获取同一把锁,java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。

注:在java中ReentrantLock、synchronized都是可重入锁。可重入锁最大的作用就是避免死锁

共享锁和排它锁

共享锁:若事务A对数据D加了共享锁,则其他事务只能对数据D加共享锁,不能加排它锁。获取共享锁的事务只能读数据,不能修改数据。

排它锁:若事务A对数据D加了排它锁,则其他事务不能再对数据D加任何类型的锁。获取排它锁的事务对数据可读可写。

读写锁

一个资源能被多个读线程访问,或被一个写线程访问但不能同时存在读线程。

互斥锁

一次最多只能有一个线程持有的锁。

注:synchronized、Lock就是互斥锁。

协程是什么

在单线程里实现多任务的高度,并在单线程里维持多个任务间的切换。

synchronized关键字

作用:用来实现线程同步,在多线程中,保证synchronized代码块不被多个线程同时执行。

synchronized主要的三种使用方式:

  • 修饰实例方法:给当前对象实例加锁。进入同步代码前要获取当前对象实例的锁。
  • 修饰静态方法:给当前类加锁。如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态synchronized方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
  • 修饰代码块:指定加锁对象。进入同步代码块前要获取指定的对象锁。

注: 尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能

synchronized底层原理

synchronized使用时不需要显示加锁和解锁过程的,但是编译后会在同步代码块的前后添加上monitorenter 和 monitorexit指令。通过执行monitorenter 指定获取锁,执行完代码之后会通过 monitorexit 指令释放锁。java对象的内存布局分为:对象头、实例数据和对齐填充,而对象头又可以分为Markword和类型指针Klass。Markword用来存储对象自身的运行时数据,例如HashCode、分代年龄和锁标记位。

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }
}

通过javap -c -v SysnchronizedDemo反编译代码后,查看字节码结构,如下图所示:
img

monitorenter

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程:

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit

  1. 执行monitorexit的线程必须是objectref所对应的monitor的所有者。
  2. 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

为什么会为两个 monitorexit 指令

第二个monitorexit指令是保证在异常情况下,锁也可以得到释放,避免死锁情况

synchronized可重入原理

可重入锁:线程可以多次获取同一把锁。

底层原理:底层通过维护一个计数器,当线程获取该锁时,计数器加1;当线程释放该锁时,计数器减1;当计数器为0时,表示该锁没有被任何线程持有。

synchronized锁升级原理

锁有4种状态,根据优先级从低到高分别是无状态锁、偏向锁、轻量级锁、重量级锁锁可以升级但不能降级,但是偏向锁状态可以被重置为无锁状态

偏向锁

锁会偏向第一个获取它的线程,在接下来的执行过程中该锁没有其他的线程获取,则持有偏向锁的线程永远不需要再进行同步。

轻量级锁
重量级锁

锁升级过程:初次执行synchronized代码块时,锁对象会从无锁升级为偏向锁,通过CAS修改对象头中的锁标识,再次执行synchronized代码块时,会先判断对象头的 threadid 是否与当前线程 id 相同,若不相同,则升级偏向锁为轻量级锁。没有抢到锁的线程将自旋一定次数来获取锁,执行一定次数之后,若还没有正常获取到锁,就会把锁从轻量级升级为重量级锁。

img

用户态和内核态

讲一下用户态和内核态
  • 用户态(user mode):用户态运行的进程可以直接读取用户程序的数据。
  • 内核态(kernel mode):内核态运行的进程或程序可以访问计算机的任何资源,不受限制。
所有的系统调用都会进入内核态吗?

与内核态级别的资源有关的操作,都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。

用户态切换到内核态的方式
  • 系统调用:用户程序通过OS调用内核程序开放的接口来执行程序。
  • 异常:当CPU执行用户态的程序时,发生了异常。这时当前用户态的程序会切换到处理此异常的内核的程序中去。
  • 硬件设备的中断
用户态和内核态什么时候进行切换

程序都是运行在用户态 的,但有时程序确实需要做一些内核的事,例如从硬盘读取数据或从硬盘获取输入,而唯一可以做这些事的就是OS,所以程序就需要先操作系统请求以程序的名义来执行这些操作。

synchronized中依赖的monitor也需要依赖OS完成,因此需要用户态到内核态的切换

双重校验锁实现对象单例(线程安全)

public class SingleTon{
    private volatile static SingleTon instance;
    private SingleTon(){}
    public static SingleTon getInstance(){
    	//先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if(instance == null){
            synchronized(SingleTon.class){
                if(instance == null){
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }
}

instance 采用 volatile 关键字修饰也很有必要, instance = new SingleTon();这段代码其实是分为三步执行:

  • 为 instance 分配内存空间
  • 初始化 instance
  • 将 instance 指向分配的内存地址

因为JVM具有指令重排的特性,执行顺序可能会变成 1->3->2,导致实例还没有被初始化。所以使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

synchronized 和 Lock 的区别

  • synchronized不需要手动加锁和释放锁,当出现异常时,会自动释放锁,不会造成死锁;Lock需要手动加锁和释放锁;

  • synchronized是关键字,可以给类、方法、代码块加锁;Lock是接口,只能给代码块加锁;

synchronized 和 ReentrantLock 的区别

  • 都是可重入锁;
  • synchronized是关键字,可以修饰类,方法、代码块;ReentrantLock是类,只适用于代码块;
  • synchronized不需要手动加锁和释放锁,当出现异常时,会自动释放锁,不会造成死锁;Lock需要手动加锁和释放锁;

synchronized 和 volatile 的区别

  • volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。
  • volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
  • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

线程A进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入此对象的 synchronized 方法 B?

不能,因为线程A进入了A方法,说明线程A已经获取到了对象的锁,其他线程想进入B方法就只能在等待池中等待对象锁。

ReentrantLock原理分析

ConcurrentHashMap原理分析

乐观锁和悲观锁的理解及实现,有哪些实现方式

乐观锁:乐观的思想,认为每次获取数据时都不会有用户会修改,所以不上锁,但是在更新的时候会判断一下其他用户是否更新过数据。

悲观锁:悲观的思想,认为每次获取数据时都会有其他用户会修改,所以每次获取数据时都会加锁。

乐观锁的实现:

  • 通过版本号标识来确定读取的数据与提交数据的一致性。提交后修改版本号,数据不一致时可以采用丢弃和再次尝试的策略。
  • 当多个线程尝试用CAS来更新同一个变量时,只会有一个线程更新成功,其他线程都更新失败,失败的线程并不会被挂起,而是告知这次竞争中失败,并可以再次尝试。CAS包括三个操作数,V内存位置,A期望值,B新值,当内存位置V与期望值A相等时,CPU会将内存中的值更新为新值B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。

synchronizedMap与ConcurrentHashMap的区别

  • synchronizedMap一次锁住整个类来保证线程安全,所以每次只能有一个线程来访问。
  • ConcurrentHashMap使用分段锁来保证在多线程下的性能。一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。

AQS原理分析

核心思想:若被请求的共享资源空闲,则当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。若被请求的共享资源被占用,就将暂时获取不到锁的线程加入CLH队列中。

CLH队列

线程池

池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

线程池的状态

running:正常状态,接受新任务,处理等待队列中的任务;

shutdown:不接受新任务提交,但是会继续处理等待队列中的任务;

stop:不接受新任务提交,不在处理等待队列中的任务,中断正在执行的线程任务;

tidying:所以有任务都销毁了,workCount为0,并执行钩子方法 terminated();

terminated:钩子函数执行完后,线程池就会进入 terminated 状态。

线程池中submit和execute方法的区别

  • 接收参数:submit可以执行Runnable和Callable类型的任务;execute只能执行Runnable类型任务;
  • 返回值:submit可以返回一个Future对象;execute无返回值;

ThreadPoolExecutor构造函数参数分析

  • corePoolSize:核心线程数。线程定义了最小可以同时运行的线程数量。
  • maximuPoolSize:最大线程数。线程中允许存在的工作线程的最大数量。
  • workQueue:当新任务入队时会先判断当前运行的线程数量是否达到corePoolSize,若达到,任务就会被存在放在队列中。
  • keepAliveTime:线程池中的线程数量大于核心线程数时,若这些没有新任务提交,核心线程数的线程不会立即销毁,而是等待一段时间,直接等待的时间超过 keepAliveTime 都会被回收销毁。
  • unit:keepAliveTime 参数的时间单位。
  • threadFactory
  • hander:线程池任务队列超过最大线程数后的拒绝策略。

ThreadPoolExecutor饱和策略

定义:若当前同时运行的线程数量达到最大线程数并且队列中任务已满,ThreadPoolTaskExecutor定义了一些策略:

  • ThreadPoolExecutor.AbortPolicy:抛出异常 RejectedExecutionException 来拒绝新任务的处理。
  • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃。
  • ThreadPoolExecutor.DiscardOldesPolicy:丢弃最早未处理的任务请求。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。

线程池的原理

在这里插入图片描述

参考文章:

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

所幸你是例外

你的鼓励将是我创作的最大动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值