什么是线程和进程?线程与进程之间的关系?并发与并行的区别?为什么要用多线程?
1.进程是计算机操作系统运行的基本单位,进程也就是程序执行一次的过程(在我们的Java中,当我们启动一个main函数的时候就相当于启动了一个JVM 进程,而main函数所在的线程也就是我们说的主线程)
2.线程就是更小的执行单位了 一个进程可以有多个线程,同时多个线程之间共享进程的堆和方法区;同时每个线程拥有自己独立的本地方法栈,虚拟机栈以及程序计数器;
3.并发(同一时间段内,做多个任务)与并行(同一时刻,做多个任务)
4.为什么需要多线程?提高程序执行效率和响应速度!
为什么程序计数器,本地方法栈和虚拟机栈是私有的?堆和方法区的作用是什么?
程序计数器:线程切换后能过恢复到正确的位置
1.在多线程的情况下,记录当前线程所运行的位置,从而当线程被切换回来的时候才能够知道该线程上次运行到了哪里
2.字节码解释器通过改变程序计数器来依次读取指令(顺序执行,循环,选择等),从而实现代码的流程控制
虚拟机栈和本地方法栈:保证线程中的局部变量不被别的线程访问到
1.虚拟机栈的作用是每个Java方法在执行的时候需要创建一个栈帧来存储局部变量,操作数栈,常量池的引用;同时每一个方法的调用和完成都代表着一个栈帧在Java虚拟机栈中入栈和出栈的过程
2.本地方法栈和虚拟机栈的作用差不多;本地方法栈主要是为虚拟机使用到的Native方法服务;
堆和方法区:
1.堆是进程中最大的一块内存,主要是用于存储新创建的对象(所有的对象都是在堆分配内存)
2.方法区主要是用于存放已被加载的类信息,常量,静态变量以及编译器编译后的代码
上下文切换与线程死锁
1.什么是上下文切换:当前任务在执行完CPU的时间切片切换到其他任务之前会先保存自己的状态,以便下次切换到这个任务的时候可以再加载到这个任务的状态(任务从保存到再加载的过程)
2.什么是线程死锁?如果避免线程死锁?
线程死锁指的是 线程A持有B资源 线程B持有A资源 而他们同时都想申请对方的资源,但是他们都不放弃自己手里的资源,所以就会相互等待从而陷入“死锁”状态
线程死锁的四个条件:1.互斥条件(一个资源只能一个线程把控)2.请求与保持条件(一直请求别人的,自己有的却不放手,真tm狗)3.不剥夺条件(自己线程的资源在没使用完之前不能被其他线程强行剥夺)4.循环等待条件(几个线程之间形成一种头尾相接的循环等待资源关系)
解决方法:1.一次性申请所有的资源 2.申请不到别人的资源时,主动释放自己的 4.按序申请资源
synchronized关键字的使用
1.修饰实例方法 (给当前对象实例加锁):
2.修饰静态方法(锁定当前类的Class对象)
3.修饰代码块(锁定指定对象)
双重校验锁实现对象单例(线程安全的情况下)
volatile关键字的作用:
volatile关键字可以保证多线程环境下对变量instance的可见性,同时在JDK1.5之前,由于指令重排序的原因,上述代码可能会出现问题,因此需要将变量instance声明为volatile类型才能确保其正确性;
JMM(Java内存模型):
1.在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量
2.在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中
3.而不是直接在主存中进⾏读写。这就可能造成⼀个线程在主存中修改了⼀个变量的值,⽽另外⼀个线程还继续使⽤它在寄存器中的变量值的拷贝,造成数据的不⼀致
4.volatile 保证变量的可见性,某个线程修改了变量的值,其他线程立即能够看到变量的最新值
5.synchronized 关键字保证线程之间的有序性和排他性,同一时间只有一个线程能够执行代码块;
6.final 关键字可以保证不可变性,即一旦变量被赋值,就不能再修改它的值。
JUC并发编程原子类与AQS!
Atomic原子类:
1.基本类型:整形原子类(AtomicInteger) 长整型原子类(AtomicInteger) 布尔型原子类(AtomicBoolean)
2.数组类型;整形数组原子类(AtomicIntegerArray)长整形数组原子类(AtomicLongArray)
3.引用类型:
4.对象的属性修改类型:
AQS:并发编程工具->主要是用于实现同步器
1.AQS原理:
核心思想是基于一个共享的抽象队列来实现线程的同步和互斥。在这个队列中,每个节点代表一个正在等待某个锁或者信号的线程,当持有锁或者信号的线程释放锁或者发出信号时,就会从队列中唤醒下一个需要锁或者信号的线程;
1. 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作
2.AQS 使⽤ CAS 对该同步状态进⾏原⼦操作实现对其值的修改,状态信息通过 protected 类型的 getState,setState,compareAndSetState 进⾏操作
2.AQS资源共享方式:
独占模式:只有⼀个线程能执行,如 ReentrantLock 。又可分为公平锁和非公平锁;
公平锁:按照请求锁的先后顺序依次获得锁
非公平锁:不保证按照请求锁的先后顺序依次获得锁
共享模式:多个线程可同时执行,如CountDownLatch(后续代码示例分析)
3.AQS底层与AQS组件:
AQS 使用了模板方法模式,⾃定义同步器时需要重写下面几个 AQS 提供的模板方法:
-
tryAcquire(int) 该方法尝试获取同步状态,并返回一个boolean值表示是否成功获取。在这个方法中,我们需要实现具体的获取同步状态的逻辑,并根据获取结果返回true或false。
-
tryRelease(int) 该方法尝试释放同步状态,并返回一个boolean值表示是否成功释放。在这个方法中,我们需要实现具体的释放同步状态的逻辑,并根据释放结果返回true或false。
-
tryAcquireShared(int) 该方法尝试获取共享同步状态,并返回一个int值表示当前同步状态的数量。在这个方法中,我们需要实现具体的获取共享同步状态的逻辑,并根据获取结果返回当前同步状态的数量。
-
tryReleaseShared(int) 该方法尝试释放共享同步状态,并返回一个boolean值表示是否成功释放。在这个方法中,我们需要实现具体的释放共享同步状态的逻辑,并根据释放结果返回true或false
AQS组件:
4.CountDownLatch:
作用:允许 count 个线程阻塞在⼀个地方,直至所有线程的任务都执行完毕
场景应用:我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。
1.定义了⼀个线程池和 threadCount 为 6 的 CountDownLatch 对象
2.使⽤线程池处理读取任务,每⼀个线程处理完之后就将 count-1
3.调用 CountDownLatch 对象的 await() 方法,直到所有文件读取完之后,执行后面的逻辑
使用 CompletableFuture
类来优化处理文件的并发性能
CAS操作:
CAS操作是一个原子性的操作,它包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。CAS指令执行时,当且仅当内存位置V的值与预期原值A相等时,才将内存位置V的值更新为新值B,否则不做任何操作;
17.进程间如何通信,可以具体讲讲它们的区别吗(进程间的通信方式不必须烂熟于心?)
管道(Pipe):管道是一种单向通信机制,它可以在父子进程之间进行通信。它可以通过调用系统函数pipe()来创建一个半双工的管道,在父子进程中分别使用管道的写端和读端进行通信。
命名管道(Named Pipe):命名管道也是一种单向通信机制,它可以在不同进程之间进行通信。与管道不同的是,命名管道具有独立的文件名和文件描述符,可以通过mkfifo()系统调用来创建。
共享内存(Shared Memory):共享内存是一种高效的通信机制,它可以让多个进程直接访问同一块物理内存空间,避免了数据拷贝的开销。共享内存的实现需要借助于操作系统提供的API,如shmget()、shmat()等。
信号量(Semaphore):信号量是一种用于进程同步和互斥的机制,它可以保证多个进程对共享资源的访问顺序和数量。信号量的实现需要借助于操作系统提供的API,如semget()、semop()等。
消息队列(Message Queue):消息队列是一种通过消息传递的通信机制,它可以在不同进程之间进行通信。消息队列的实现需要借助于操作系统提供的API,如msgget()、msgsnd()、msgrcv()等
19.synchronized和lock有什么区别(sb!一时间短路只想起了与volatile的区别)
使用范围:synchronized可以用于修饰方法、代码块,而Lock只能用于代码块。
锁的获取方式:synchronized会自动释放锁,而Lock必须显式地调用unlock()方法才能释放锁。
粒度:在Java 5之前,synchronized锁对象的粒度是针对整个方法或代码块的,而Lock可以实现更灵活的锁定,例如可以针对某个数据结构中的一部分进行加锁。
可重入性:synchronized是可重入的锁,即同一个线程可以多次获取同一个锁,而不会被阻塞。Lock也是可重入的锁,但需要注意避免死锁问题。
性能:在Java 5之前,synchronized的性能往往比Lock差,因为它要涉及到操作系统级别的互斥信号量。但是在Java 5之后,synchronized的性能得到了大幅度优化,在大部分情况下已经与Lock相当甚至更好了。
等待可中断:Lock提供了tryLock()方法,可以尝试获取锁,并在指定时间内等待锁释放。同时还提供了lockInterruptibly()方法,可以在等待过程中被中断
45.什么是CAS?Unsafe类再操作系统层面是如何实现CAS的?CAS会一直自旋嘛?
1.CAS(Compare and Swap)是一种乐观锁技术,用于实现多线程环境下的无锁同步。CAS 操作包含三个操作数:内存位置 V、期望值 A 和新值 B。如果内存位置 V 的值等于期望值 A,则将内存位置 V 的值更新为新值 B;否则不做任何操作。
2.在 Java 中,CAS 可以通过 java.util.concurrent.atomic 包提供的 Atomic 类或者 Unsafe 类来进行操作。Unsafe 类是一个类似于 C 语言指针的工具类,它提供了一组低级别的、不安全的操作方法,包括 CAS 操作。在操作系统层面,CAS 是由 CPU 提供支持的原子操作指令,比如 x86 架构上的 CMPXCHG 指令。
3.当调用 Unsafe.compareAndSwapInt() 方法时,会先从内存地址中读取当前值,然后与期望的旧值比较。如果相等,则把新值写入到这个内存地址中,并返回 true 表示修改成功。否则返回 false,表示修改失败。由于CAS是一种忙等待的方式,所以如果同时有很多线程尝试修改同一个内存位置,就会导致大量的自旋操作浪费 CPU 资源。
4.为了避免自旋带来的性能问题,JDK8 引入了适应性自旋技术。该技术会根据当前 CAS 操作的历史执行时间和当前线程调度情况来动态地调整自旋次数,从而减少自旋带来的性能损失。此外,在 JDK8 中还引入了基于 CPU Cache 和锁膨胀机制等优化技术,可以显著提高 CAS 的性能和并发度
51.AtomicInteger之类的原子类型用到过没?CAS存在什么缺陷?
CAS 算法是一种乐观锁技术,它通过比较内存中的值与期望值是否相等来判断是否需要更新。如果相等,则将内存中的值更新为新值;否则,不执行任何操作。由于 CAS 操作是在一个原子操作中完成的,因此可以保证操作的原子性和线程安全性。
然而,CAS 也存在一些缺陷:
ABA 问题:CAS 只能检测到值是否有变化,但无法检测值的变化过程。例如,线程 A 将值从 1 修改为 2,然后再修改回 1,此时线程 B 修改该值时仍旧会认为其没有被修改过,导致出现 ABA 问题。
自旋时间过长:CAS 操作在并发量较大的情况下可能会导致自旋时间过长,严重影响系统性能。
只能保证单个变量的原子性:CAS 只能针对单个变量实现原子操作,不能针对多个变量实现复合操作
多线程了解吗,多线程的创建和销毁说一下?
多线程的创建通常取决于编程语言和相应的多线程库。在Java中,可以使用以下两种方式创建线程:
-
继承Thread类:通过创建一个继承自
Thread
类的子类,重写run
方法,然后创建该子类的实例来创建线程。 -
实现Runnable接口:创建一个实现
Runnable
接口的类,实现run
方法,然后通过将该类的实例传递给Thread
类的构造函数来创建线程。 -
Callable
和Runnable
都可以用于创建线程,但它们之间存在一些关键区别
-
返回值类型:
Runnable
接口的run
方法没有返回值,因此它无法返回线程执行的结果。Callable
接口的call
方法可以返回一个值,即线程的执行结果,它允许线程执行后返回一个值。
-
异常处理:
run
方法不能抛出已检查异常,因此无法通过run
方法来抛出受检查的异常。call
方法可以抛出异常,包括受检查异常,允许更灵活的异常处理
多线程的销毁通常是在线程的run
方法执行完毕后,线程自动销毁。但有时需要手动中止线程的执行,可以使用一些标志位或方法来停止线程的运行
线程池的创建和种类?
-
创建线程池:
在 Java 中,通常可以使用
ExecutorService
接口来创建线程池。常见的方式包括:-
newFixedThreadPool(int nThreads)
: 创建一个固定大小的线程池,其中线程数量固定为nThreads
。 -
newCachedThreadPool()
: 创建一个可缓存的线程池,线程数量根据需要自动增加或减少。 -
newSingleThreadExecutor()
: 创建一个单线程的线程池。 -
newScheduledThreadPool(int corePoolSize)
: 创建一个定时执行任务的线程池,可以定期执行任务或者延时执行任务。 -
自定义线程池:可以使用
ThreadPoolExecutor
类来自定义线程池,以满足特定需求。
-
-
核心参数介绍:
-
corePoolSize
: 核心线程数,线程池中一直存活的线程数量。在执行任务时,首先尝试使用核心线程来处理任务。如果核心线程数不足,新线程将被创建。 -
maximumPoolSize
: 最大线程数,线程池中允许的最大线程数量。当任务队列满了,并且核心线程都在工作,将会创建新线程,但数量不超过最大线程数。 -
keepAliveTime
: 当线程池中的线程数量超过核心线程数时,多余的空闲线程会在经过一段时间后被终止并回收。这个时间由keepAliveTime
指定。 -
workQueue
: 任务队列,用于保存等待执行的任务。常见的队列类型包括LinkedBlockingQueue
、ArrayBlockingQueue
、PriorityBlockingQueue
等。 -
ThreadFactory
: 用于创建新线程的工厂,通常用于自定义线程的名称和特性
-
线程池的分类及应用场景:
-
FixedThreadPool(固定大小线程池):
- 核心线程数和最大线程数相等,不会增加或减少线程数量。
- 适用于需要限制线程数量的场景,如服务器后台处理。
-
CachedThreadPool(缓存线程池):
- 不固定线程数量,可根据需求自动增加或减少线程。
- 适用于短期异步任务,如大量耗时较短的计算。
-
SingleThreadExecutor(单线程线程池):
- 只有一个核心线程,确保任务按照顺序执行。
- 适用于需要顺序执行任务的场景,如日志处理。
-
ScheduledThreadPool(定时线程池):
- 用于定时执行任务,支持延时执行和定期执行任务。
- 适用于需要定时任务的场景,如定时任务调度。
-
WorkStealingPool(工作窃取线程池):
- 是Java 8 引入的一种线程池,具备自动任务分配和负载均衡的特性。
- 适用于需要大量计算密集型的并行任务。
-
ForkJoinPool(分治线程池):
- 用于分治任务,将任务分解成子任务并将子任务的结果合并。
- 适用于复杂的递归计算,如归并排序、并行计算等。
-
CustomThreadPool(自定义线程池):
- 可根据特定需求自定义线程池,设置核心线程数、最大线程数、任务队列、拒绝策略等参数。
- 适用于特定场景,需根据具体需求来定制线程池
ThreadLocal相关问题?
ThreadLocal
是 Java 中的一个类,用于解决多线程环境下的数据隔离问题。每个线程都可以拥有自己的 ThreadLocal
变量,这个变量对其他线程是不可见的。它通常用于保存线程本地的一些变量,比如用户会话、数据库连接、事务上下文等。以下是与 ThreadLocal
相关的问题:
ThreadLocal
是 Java 中用来实现线程局部变量的机制。每个线程可以在 ThreadLocal
中存储自己的独立变量,这些变量在该线程内部是隔离的,互不干扰。ThreadLocal
的内部原理和为何会导致内存泄漏是需要了解的。
ThreadLocal
的内部原理:
ThreadLocal
使用一个特殊的数据结构(通常是ThreadLocalMap
),该结构以线程为 key,以存储的值为 value。每个线程可以从该结构中获取和修改自己的变量,而不影响其他线程。- 当你调用
ThreadLocal
的set
方法时,实际上是在当前线程的ThreadLocalMap
中存储了一个键值对。 - 当你调用
get
方法时,ThreadLocal
会在当前线程的ThreadLocalMap
中查找对应的值,并返回给你
-
ThreadLocal 是什么?
ThreadLocal
是 Java 中的一个类,它提供了线程本地变量的机制。通过ThreadLocal
,每个线程可以独立地访问自己的变量,而不会干扰其他线程的数据。 -
ThreadLocal 的使用场景是什么?
ThreadLocal
主要用于多线程环境下的数据隔离。它常见的应用场景包括保存用户会话信息、数据库连接、事务上下文等。 -
ThreadLocal 的工作原理是什么?
ThreadLocal
使用一个以当前线程为键的散列表来存储数据,每个线程都可以根据当前线程获取自己的数据。这样,每个线程都可以独立地访问自己的数据,不会干扰其他线程。 -
ThreadLocal 的使用方法是什么?
使用ThreadLocal
需要创建一个ThreadLocal
对象,然后通过set
方法为当前线程设置数据,通过get
方法获取当前线程的数据。通常需要注意在使用后调用remove
方法,防止内存泄漏。 -
ThreadLocal 是否有风险?
使用ThreadLocal
时要小心内存泄漏问题,因为线程本地变量的数据生命周期通常与线程的生命周期相关。如果不注意清理,会导致数据在内存中得不到释放。另外,过度使用ThreadLocal
也可能导致代码难以维护和理解