当前位置:   article > 正文

09_多线程面试题_多线程访问资源出的问题

多线程访问资源出的问题

多线程面试题

在这里插入图片描述

1.什么是进程?什么是线程?

进程是程序的⼀次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行⼀个程序即是⼀个进程从创建,运行到消亡的过程。

在 Java 中,当我们启动 main 函数时其实就是启动了⼀个 JVM 的进程,而main 函数所在的线程就是这个进程中的⼀个线程,也称主线程。

线程与进程相似,但线程是⼀个比进程更小的执行单位。⼀个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地⽅法栈,所以系统在产生⼀个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
Java 程序天生就是多线程程序,我们可以通过 JMX 来看看⼀个普通的 Java 程序有哪些线程,代码如下

⼀个 Java 程序的运⾏是 main 线程和多个其他线程同时运行。

public class MultiThread {
 public static void main(String[] args) {
 // 获取 Java 线程管理 MXBean
 ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
 // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
 // 遍历线程信息,仅打印线程 ID 和线程名称信息
 for (ThreadInfo threadInfo : threadInfos) {
 System.out.println("[" + threadInfo.getThreadId() + "] " +
threadInfo.getThreadName());
 }
 }
}
//[5] Attach Listener //添加事件
//[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
//[3] Finalizer //调⽤对象 finalize ⽅法的线程
//[2] Reference Handler //清除 reference 线程
//[1] main //main 线程,程序⼊⼝
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

2.线程与进程的关系,区别及优缺点?

从 JVM 角度说进程和线程之间的关系

⼀个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈
在这里插入图片描述

总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不⼀定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?

程序计数器主要有下面两个作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下⼀条指令的地址。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

虚拟机栈和本地方法栈

  • 虚拟机栈: 每个 Java 方法在执行的同时会创建⼀个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着⼀个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
  • 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合⼆为⼀。

所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

了解堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的⼀块内存,主要用于存放新创建的对象(几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

并发与并行的区别

并发:两个及两个以上的作业在同⼀时间段内执行。
并行:两个及两个以上的作业在同⼀ 时刻执行。
最关键的点是:是否是 同时 执行。

同步和异步的区别

同步 : 发出⼀个调用之后,在没有得到结果之前, 该调用就不可以返回,⼀直等待。
异步 :调用在发出之后,不用等待返回结果,该调用直接返回。

3.什么是多线程编程?它的优势是什么?

总体来说:
计算机底层: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
互联网发展趋势: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
计算机底层来说:
单核时代: 在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了⼀个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有⼀个线程,此线程被IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有⼀个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,⼀个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
多核时代: 多核时代多线程主要是为了提高进程利用多核 CPU 的能里。举个例子:假如我们要
计算⼀个复杂的任务,我们只用⼀个线程的话,不论系统有几个 CPU 核心,都只会有⼀个CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。

多线程编程也存在一些挑战,如线程同步、资源竞争和死锁等问题,需要合理设计和管理线程的执行。

说说线程的⽣命周期和状态?

4.Java中实现多线程的方式有哪些?

  • 实现多线程的方式有以下几种:

  • 继承Thread类:创建一个继承自Thread类的子类,重写run()方法来定义线程的执行逻辑。

  • 实现Runnable接口:创建一个实现了Runnable接口的类,并实现其中的run()方法,然后将该类的实例传递给Thread类的构造函数创建线程对象。

  • 实现Callable接口:创建一个实现了Callable接口的类,并实现其中的call()方法,可以通过ExecutorService提交Callable对象来创建并执行线程,并返回执行结果。

  • 使用线程池:通过Executor框架创建线程池,可以管理和复用线程,提高线程的执行效率和资源利用率。

使用多线程可能带来什么问题?

并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。

说说线程的生命周期和状态?

Java 线程在运行的生命周期中的指定时刻只可能处于下面6种不同状态的其中⼀个状态:

  • NEW: 初始状态,线程被创建出来但没有被调用start() 。
  • RUNNABLE: 运行状态,线程被调用了 start() 等待运行的状态。
  • BLOCKED :阻塞状态,需要等待锁释放。
  • WAITING:等待状态,表示该线程需要等待其他线程做出⼀些特定动作(通知或中断)。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样⼀直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕。

线程在生命周期中并不是固定处于某⼀个状态而是随着代码的执行在不同状态之间切换。
Java 线程状态变迁图
在这里插入图片描述
由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调⽤ start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于RUNNING(运行) 状态

在操作系统层⾯,线程有 READY 和 RUNNING 状态;⽽在 JVM 层⾯,只能看到 RUNNABLE状态,所以 Java 系统⼀般将这两个状态统称为 RUNNABLE(运⾏中) 状态 。
为什么 JVM 没有区分这两种状态呢?
现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是⽤所谓的“时间分⽚(time quantum or time slice)”⽅式进⾏抢占式(preemptive)轮转调度(round-robin式)。这个时间分⽚通常是很⼩的,⼀个线程⼀次最多只能在 CPU 上运⾏⽐如 10-20ms 的时间(此时处于 running 状态),也即⼤概只有 0.01 秒这⼀量级,时间⽚⽤后就要被切换下来放⼊调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。

在这里插入图片描述

  • 当线程执行 wait() 方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。
  • TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。
  • 当线程进入synchronized 方法/块或者调用 wait 后(被 notify )重新进入 synchronized 方法/块,但是锁被其它线程占有,这个时候线程就会进入 **BLOCKED(阻塞)**状态。
  • 线程在执行完了 run() 方法之后将会进入到 TERMINATED(终止) 状态。

5.如何创建线程?有哪些方式可以实现?

  • 创建线程的方式包括以下几种:

  • 继承Thread类:创建一个继承自Thread类的子类,重写run()方法,并通过调用start()方法来启动线程。

  • 实现Runnable接口:创建一个实现了Runnable接口的类,并实现其中的run()方法,然后创建Thread对象,将实现了Runnable接口的对象作为参数传递给Thread的构造函数,最后通过调用start()方法来启动线程。

  • 实现Callable接口:创建一个实现了Callable接口的类,并实现其中的call()方法,通过创建ExecutorService对象,使用submit()方法提交Callable对象,并返回一个Future对象,可以通过Future对象获取线程执行的结果。

  • 使用线程池:通过Executor框架创建线程池,可以通过ThreadPoolExecutor或Executors工具类来创建线程池对象,然后将实现Runnable或Callable接口的任务提交给线程池执行。

线程安全指的是在多线程环境下,对共享资源的访问不会导致不确定的结果或产生并发错误。多线程访问共享资源时可能会出现竞态条件(Race Condition)、数据不一致等问题,因此需要采取措施来保证线程安全。

sleep() 方法和 wait() 方法对比

共同点 :两者都可以暂停线程的执行。
区别 :

  • sleep() 方法没有释放锁,而 wait()方法释放了锁 。
  • wait() 通常被用于线程间交互/通信, sleep() 通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同⼀个对象上的 notify() 或者notifyAll() 方法。 sleep() 方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • sleep() 是 Thread 类的静态本地方法, wait() 则是 Object 类的本地方法。

为什么 wait() 方法不定义在 Thread 中?

wait() 是让获得对象锁的线程实现等待,会动释放当前线程占有的对象锁。每个对象( Object )都拥有对象锁,既然要释放当前线程占有的对象锁并让其进⼊ WAITING 状态,自然是要操作对应的对象( Object )而非当前的线程( Thread )。
类似的问题:为什么 sleep() 方法定义在 Thread 中?
因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁

可以直接调用 Thread 类的 run 方法吗?

new ⼀个 Thread ,线程进入了新建状态。调用 start() 方法,会启动⼀个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行run() 方法的内容,这是真正的多线程共作。 但是,直接执行 run() 方法,会把 run() 方法当成⼀个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调⽤ start() 方法方可启动线程并使线程进入就绪状态,直接执行run() 方法的话不会以多线程的方式执行

volatile 关键字

如何保证变量的可见性?
在 Java 中, volatile 关键字可以保证变量的可见性,如果我们将变量声明为 volatile ,这就指示JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
在这里插入图片描述
在这里插入图片描述
volatile 关键字其实并非是 Java 特有的,在 C 也有,它最原始的意义就是禁用 CPU 缓存。如果我们将⼀个变量使用 volatile 修饰,这就指示编译器,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
volatile 关键字能保证数据的可见性,但不能保证数据的原子性。 synchronized 关键字两者都能保证。
如何禁止指令重排序?
在 Java 中, volatile 关键字除了可以保证变量的可⻅性,还有⼀个重要的作用就是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插⼊特定的内存屏障的方式来禁止指令重排序。
在 Java 中, Unsafe 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:

public native void loadFence();
public native void storeFence();
public native void fullFence();
  • 1
  • 2
  • 3

理论上来说,你通过这个三个方法也可以实现和 volatile 禁止重排序⼀样的效果,只是会麻烦⼀些。 下面我以⼀个常见的面试题为例讲解⼀下
volatile 关键字禁止指令重排序的效果。 “单例模式了解吗?手写⼀下!给我解释⼀下双重检验锁方式实现单例模式的原理呗!”

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

public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
 }
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进⼊加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
 }
 }
 }
return uniqueInstance;
 }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执行了 1和 3,此时 T2 调用getUniqueInstance () 后发现 uniqueInstance 不为空,因此返回 uniqueInstance ,但此时 uniqueInstance 还未被初始化。

volatile 可以保证原⼦性么?
volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。

/**
* 微信搜 JavaGuide 回复"⾯试突击"即可免费领取个⼈原创的 Java ⾯试⼿册
*
* @author Guide哥
* @date 2022/08/03 13:40
**/
public class VolatoleAtomicityDemo {
public volatile static int inc = 0;
public void increase() {
inc++;
 }
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
VolatoleAtomicityDemo volatoleAtomicityDemo = new
VolatoleAtomicityDemo();
for (int i = 0; i < 5; i++) {
threadPool.execute(() -> {
for (int j = 0; j < 500; j++) {
volatoleAtomicityDemo.increase();
 }
 });
 }
 // 等待1.5秒,保证上⾯程序执⾏完成
Thread.sleep(1500);
System.out.println(inc);
threadPool.shutdown();
 }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

正常情况下,运行上面的代码理应输出 2500 。但你真正运行了上面的代码之后,你会发现每次输出结果都小于 2500 。
为什么会出现这种情况呢?不是说好了, volatile 可以保证变量的可见性嘛!
也就是说,如果 volatile 能保证 inc++ 操作的原子性的话。每个线程中对 inc 变量自增完之后,其他线程可以立即看到修改后的值。5 个线程分别进行了 500 次操作,那么最终 inc 的值应该是5*500=2500。
很多人会误认为自增操作 inc++ 是原⼦性的,实际上,inc++ 其实是⼀个复合操作,包括三步:

  1. 读取 inc 的值。
  2. 对 inc 加 1。
  3. 将 inc 的值写回内存。

volatile 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现:

  1. 线程 1 对 inc 进行读取操作之后,还未对其进行修改。线程 2 又读取了 inc 的值并对其进行修改(+1),再将 inc 的值写回内存。
  2. 线程 2 操作完毕后,线程 1 对 inc 的值进行修改(+1),再将 inc 的值写回内存。

这也就导致两个线程分别对 inc 进行了⼀次自增操作后, inc 实际上只增加了 1。
其实,如果想要保证上面的代码运行正确也非常简单,利用 synchronized 、 Lock 或者 AtomicInteger 都可以。
使用 synchronized 改进:

public synchronized void increase() {
inc++;
}
  • 1
  • 2
  • 3

使⽤ AtomicInteger 改进:

public AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
  • 1
  • 2
  • 3
  • 4

使⽤ ReentrantLock 改进:

Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
try {
inc++;
 } finally {
lock.unlock();
 }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

synchronized 关键字

说⼀说自己对于 synchronized 关键字的了解
synchronized 翻译成中文是同步的的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有⼀个线程执行。
在 Java 早期版本中, synchronized 属于 重量级锁,效率低下。 因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒⼀个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对长的时间,时间成本相对较高。
不过,在 Java 6 之后,官方对从 JVM 层⾯对 synchronized 较大优化,所以现在的synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字

如何使用 synchronized 关键字?

synchronized 关键字最主要的三种使用方式:

  1. 修饰实例方法(锁当前对象实例)
//给当前对象实例加锁,进⼊同步代码前要获得 当前对象实例的锁 
synchronized void method() {
//业务代码
}
  • 1
  • 2
  • 3
  • 4
  1. 修饰静态方法
/*给当前类加锁,会作⽤于类的所有对象实例 ,进⼊同步代码前要获得 当前 class 的锁。
这是因为静态成员不属于任何⼀个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。*/
synchronized static void method() {
//业务代码
}
  • 1
  • 2
  • 3
  • 4
  • 5

静态 synchronized ⽅法和⾮静态 synchronized ⽅法之间的调⽤互斥么?不互斥!如果⼀个线程 A调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程 B 需要调⽤这个实例对象所属类的静态synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁

  1. 修饰代码块
/*synchronized(object) 表示进⼊同步代码库前要获得 给定对象的锁。
synchronized(类.class) 表示进⼊同步代码前要获得 给定 Class 的锁*/
synchronized(this) {
//业务代码
}
  • 1
  • 2
  • 3
  • 4
  • 5

总结:

  • synchronized 关键字加到 static 静态方法和synchronized(class) 代码块上都是是给 Class 类上
    锁;
  • synchronized 关键字加到实例⽅法上是给对象实例上锁;
  • 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能。

构造方法可以使用 synchronized 关键字修饰么?

不可以,构造方法本身就属于线程安全的,不存在同步的构造方法⼀说

讲⼀下 synchronized 关键字的底层原理

synchronized 关键字底层原理属于 JVM 层面
synchronized 同步语句块的情况

public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
 }
 }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

通过 JDK ⾃带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:⾸先切换到类的对应⽬录执⾏ javac SynchronizedDemo.java 命令⽣成编译后的 .class ⽂件,然后执⾏ javap -c -s -v -lSynchronizedDemo.class 。

6.什么是线程安全?如何保证线程安全?

线程安全指的是在多线程环境下,对共享资源的访问不会导致不确定的结果或产生并发错误。多线程访问共享资源时可能会出现竞态条件(Race Condition)、数据不一致等问题,因此需要采取措施来保证线程安全。

以下是一些保证线程安全的方法:

  • 使用同步(Synchronization):使用synchronized关键字或使用ReentrantLock等锁机制来实现对共享资源的互斥访问,一次只允许一个线程进行访问。
  • 使用volatile关键字:通过volatile关键字修饰共享变量,保证线程之间的可见性,即一个线程对共享变量的修改对其他线程是可见的。
  • 使用线程安全的数据结构:例如Vector、ConcurrentHashMap等,这些数据结构在内部实现上考虑了线程安全的问题。
  • 使用原子类(Atomic classes):Java提供了一些原子类,如AtomicInteger、AtomicLong等,它们提供了原子性操作,可以确保对共享变量的操作是不可分割的,从而避免了竞态条件。
  • 使用线程安全的设计模式:例如Immutable(不可变)对象、ThreadLocal等,它们的设计能够在多线程环境下保证线程安全。

需要根据具体的场景和需求选择适当的方法来保证线程安全,以确保多线程程序的正确性和可靠性。

7.什么是互斥锁?Java如何使用互斥锁来实现线程同步?

  1. 互斥锁(Mutex Lock)是一种同步机制,用于实现线程之间的互斥访问,以避免数据竞争和不一致性。在Java中,互斥锁通常使用关键字synchronized来实现。

要使用互斥锁来实现线程同步,可以按照以下步骤:

  1. 定义共享资源:确定需要被多个线程共享的资源。

  2. 使用synchronized关键字:在访问共享资源的代码段之前,使用synchronized关键字来标记该代码段,以确保同一时间只有一个线程可以访问。

    synchronized (sharedResource) {
        // 访问共享资源的代码
    }
    
    • 1
    • 2
    • 3

    或者,也可以使用synchronized修饰方法来实现对整个方法的同步:

    public synchronized void sharedMethod() {
        // 方法体
    }
    
    • 1
    • 2
    • 3

    在使用synchronized关键字标记的代码段执行期间,其他线程将被阻塞,直到当前线程释放锁定。

  3. 访问共享资源:在获取锁定之后,线程可以安全地访问共享资源或执行需要同步的代码段。

  4. 死锁(Deadlock)是指两个或多个线程被永久地阻塞,因为每个线程都在等待其他线程所持有的资源,从而无法继续执行。

8.什么是死锁?Java如何预防和避免死锁?

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

public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
System.out.println(Thread.currentThread() + "waiting get
resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get
resource2");
 }
 }
 }, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
 } catch (InterruptedException e) {
e.printStackTrace();
 }
System.out.println(Thread.currentThread() + "waiting get
resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get
resource1");
 }
 }
 }, "线程 2").start();
 }
}
//Thread[线程 1,5,main]get resource1
//Thread[线程 2,5,main]get resource2
//Thread[线程 1,5,main]waiting get resource2
//Thread[线程 2,5,main]waiting get resource1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过 Thread.sleep(1000); 让线程 A 休眠 1s 为的是让线程 B 得到执⾏然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对⽅的资源,然后这两个线程就会陷⼊互相等待的状态,这也就产⽣了死锁

在Java中,可以采取以下几种方式来避免死锁:

  • 避免循环等待:尽量避免线程之间循环依赖资源的情况,例如按照固定的顺序获取资源,以避免形成循环等待的条件。
  • 加锁顺序:在使用多个锁的情况下,尽量确保线程按照相同的顺序获取锁,从而避免不同线程之间的交叉等待。
  • 使用超时机制:在获取锁的过程中,可以使用带有超时参数的tryLock()方法来尝试获取锁,并设定一个超时时间,在超时后放弃获取锁,避免长时间等待。
  • 死锁检测与恢复:可以使用工具或算法检测死锁的存在,并采取相应的措施进行恢复,例如中断其中一个线程或回滚操作。
如何预防死锁?

破坏死锁的产生的必要条件即可:

  1. 破坏请求与保持条件 :一次性申请所有的资源。
  2. 破坏不剥夺条件 :占用部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  3. 破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件
如何避免死锁?

避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态

安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3…Pn)来为每个线程分配所需资源,直到满⾜每个线程对资源的最⼤需求,使每个线程都可顺利完成。称 <P1、P2、P3…Pn> 序列为安全序列

new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
 } catch (InterruptedException e) {
e.printStackTrace();
 }
System.out.println(Thread.currentThread() + "waiting get
resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get
resource2");
 }
 }
 }, "线程 2").start();
 //Thread[线程 1,5,main]get resource1
//Thread[线程 1,5,main]waiting get resource2
//Thread[线程 1,5,main]get resource2
//Thread[线程 2,5,main]get resource1
//Thread[线程 2,5,main]waiting get resource2
//Thread[线程 2,5,main]get resource2
//Process finished with exit code 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

线程 1 ⾸先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占⽤,线程 2 获取到就可以执⾏了。这样就破坏了破坏循环等待条件,因此避免了死锁

9.什么是线程池?Java如何创建线程池?

线程池(ThreadPool)是一种用于管理和重用线程的机制。它由一组预先创建的线程组成,用于执行提交给池的任务,从而减少了线程创建和销毁的开销,并提高了线程的利用率。

在Java中,可以使用java.util.concurrent包下的ExecutorService接口来创建线程池。以下是创建线程池的一般步骤:

  1. 创建线程池对象:使用Executors类提供的静态方法之一创建一个ExecutorService实例,例如:

    ExecutorService executorService = Executors.newFixedThreadPool(10);
    
    • 1

    上述代码创建了一个固定大小为10的线程池。

  2. 提交任务:使用execute()方法或submit()方法向线程池提交任务,例如:

    executorService.execute(new MyTask());
    
    • 1

    其中,MyTask是一个实现Runnable接口的任务。

  3. 关闭线程池:在不再需要线程池时,调用shutdown()方法来关闭线程池,例如:

    executorService.shutdown();
    
    • 1

    这将停止线程池接受新的任务,并在所有任务执行完成后关闭线程池。

  4. 线程优先级(Thread Priority)是用于指定线程相对执行顺序的一种机制。每个线程都有一个优先级,用于决定在竞争CPU资源时获得执行的几率。

10.什么是线程优先级?Java如何设置线程优先级?

在Java中,线程的优先级通过整数表示,范围从1到10,其中1为最低优先级,10为最高优先级。可以使用setPriority()方法来设置线程的优先级,例如:

Thread thread = new Thread();
thread.setPriority(Thread.MAX_PRIORITY); // 设置最高优先级
  • 1
  • 2

请注意,线程优先级只是给出建议,实际的线程调度行为受到底层操作系统和线程调度器的影响。

  1. 线程调度(Thread Scheduling)是操作系统或线程调度器决定哪个线程在给定时间点执行的过程。Java中的线程调度算法取决于底层操作系统和Java虚拟机的实现。

在Java中,可以使用Thread类的静态方法yield()来暗示线程调度器当前线程可以放弃CPU资源,让其他线程执行。此外,还可以使用sleep()方法来让线程休眠一段时间,从而影响线程的调度行为。

线程调度算法的具体实现取决于操作系统和虚拟机的实现细节,包括时间片轮转、优先级调度、公平调度等策略。这些策略通常是操作系统和虚拟机的核心组件,用于合理分配CPU资源并控制线程的执行顺序。

11.什么是线程调度?Java中的线程调度算法是什么?

线程调度(Thread Scheduling)是操作系统或线程调度器决定哪个线程在给定时间点执行的过程。Java中的线程调度算法取决于底层操作系统和Java虚拟机的实现。

在Java中,可以使用Thread类的静态方法yield()来暗示线程调度器当前线程可以放弃CPU资源,让其他线程执行。此外,还可以使用sleep()方法来让线程休眠一段时间,从而影响线程的调度行为。

线程调度算法的具体实现取决于操作系统和虚拟机的实现细节,包括时间片轮转、优先级调度、公平调度等策略。这些策略通常是操作系统和虚拟机的核心组件,用于合理分配CPU资源并控制线程的执行顺序。

12.什么是线程间通信?Java有哪些方式可以实现线程间通信?

  1. 线程间通信(Inter-thread Communication)是指多个线程之间进行信息交换和数据共享的机制。在线程并发编程中,线程之间可能需要协调和共享数据,这就需要一种机制来实现线程间的通信。

在Java中,有多种方式可以实现线程间通信,包括:

  • 共享变量:多个线程共享同一个变量,通过对该变量的读写操作来进行信息交换和数据共享。但是需要注意线程安全性,需要使用同步机制来保护共享变量的一致性。
  • 等待/通知机制:通过wait()notify()方法实现线程的等待和唤醒,允许线程在特定条件下等待并释放资源,等待其他线程满足条件后进行唤醒。
  • 锁机制:使用锁对象(如互斥锁、读写锁)来实现线程间的同步和通信。线程可以通过获取锁的方式来等待其他线程释放锁,并通过释放锁的方式来唤醒等待的线程。
  • 信号量(Semaphore):通过信号量来控制多个线程的访问权限,通过申请和释放信号量来实现线程之间的同步和通信。
  • 阻塞队列(Blocking Queue):使用阻塞队列作为线程间的共享数据结构,一个线程可以将数据放入队列中,另一个线程可以从队列中取出数据,从而实现线程间的数据交换和通信。

13.什么是线程的等待和唤醒机制?

  1. 线程的等待和唤醒机制是一种用于线程间协作的机制,允许线程在满足特定条件之前等待,然后在条件满足时被唤醒继续执行。

在Java中,线程的等待和唤醒机制通常是通过以下方法来实现:

  • wait()方法:使当前线程进入等待状态,并释放持有的锁。调用wait()方法的线程将等待其他线程调用相同对象的notify()notifyAll()方法来唤醒它。
  • notify()方法:唤醒在相同对象上调用wait()方法进入等待状态的线程中的一个线程。如果有多个线程在等待,哪个线程被唤醒是不确定的。
  • notifyAll()方法:唤醒在相同对象上调用wait()方法进入等待状态的所有线程。

这种等待和唤醒机制通常与共享变量、锁机制和同步代码块一起使用,以实现多个线程之间的协调和通信。

14.什么是守护线程(Daemon Thread)?Java如何创建守护线程?

守护线程(Daemon Thread)是一种在后台运行的线程,它的存在不会阻止程序的终止。当所有非守护线程结束时,守护线程会自动终止,无论它是否执行完任务。

在Java中,可以通过设置线程的setDaemon(true)方法将线程设置为守护线程。这个方法需要在线程启动之前调用,否则会抛出IllegalThreadStateException异常。

下面是创建守护线程的示例代码:

Thread daemonThread = new Thread(new MyRunnable());
daemonThread.setDaemon(true);
daemonThread.start();
  • 1
  • 2
  • 3

在上述示例中,MyRunnable是一个实现了Runnable接口的任务。

需要注意的是,守护线程不能访问一些必要的资源,例如打开的文件或数据库连接等,因为它们可能在非守护线程执行完毕时被强制关闭。

15.什么是线程局部变量?Java如何使用线程局部变量?

线程局部变量(Thread Local Variable)是一种特殊类型的变量,它为每个线程都创建了一个独立的副本,每个线程都可以独立地操作和访问自己的副本,互不干扰。

在Java中,可以使用ThreadLocal类来创建线程局部变量。以下是使用线程局部变量的一般步骤:

  1. 创建线程局部变量:通过实例化ThreadLocal类来创建线程局部变量,例如:

    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
    • 1

    上述代码创建了一个存储String类型的线程局部变量。

  2. 设置和获取变量的值:使用set()方法设置线程局部变量的值,使用get()方法获取线程局部变量的值,例如:

    threadLocal.set("value"); // 设置线程局部变量的值
    String value = threadLocal.get(); // 获取线程局部变量的值
    
    • 1
    • 2

    每个线程操作自己的线程局部变量副本,互不干扰。

  3. 清除变量的值:在使用完线程局部变量后,可以调用remove()方法将线程局部变量的值清除,释放对应的资源,例如:

    threadLocal.remove();
    
    • 1

    清除线程局部变量的值是一种良好的实践,以避免潜在的内存泄漏问题。

16.什么是线程组(Thread Group)?它的作用是什么?

  1. 线程组(Thread Group)是Java中的一种线程组织机制,用于将线程分组管理。它允许将多个线程组织成一个树状结构,并提供了一些对线程组进行集中管理的方法。

线程组的作用主要包括:

  • 管理线程:线程组可以用于管理一组相关的线程,方便对这组线程进行集中管理和操作。
  • 设置线程属性:可以通过线程组来设置线程组中所有线程的优先级、守护状态等属性。
  • 处理线程未捕获异常:可以通过线程组的uncaughtException()方法来处理线程组中任何线程抛出的未捕获异常。
  • 线程组间的层级关系:线程组支持层级关系,可以创建子线程组,并在父线程组中统一管理。

在Java中,可以使用ThreadGroup类来创建线程组和管理线程组。以下是创建线程组的示例代码:

ThreadGroup threadGroup = new ThreadGroup("myThreadGroup");
Thread thread1 = new Thread(threadGroup, new MyRunnable());
Thread thread2 = new Thread(threadGroup, new MyRunnable());
  • 1
  • 2
  • 3

在上述示例中,使用ThreadGroup类的构造函数创建了一个名为"myThreadGroup"的线程组,并将两个线程分别添加到该线程组中。通过线程组,可以方便地对这两个线程进行集中管理和操作。

17.什么是线程的中断(Interrupt)机制?如何中断一个线程?

线程的中断(Interrupt)机制是一种用于通知线程停止执行的机制。当一个线程被中断时,它会收到一个中断信号,可以根据需要做出相应的响应。中断机制允许线程在执行期间检查中断状态并做出相应的处理。

在Java中,线程的中断通过调用线程的interrupt()方法来触发。以下是中断一个线程的示例代码:

Thread thread = new Thread(new MyRunnable());
thread.start();

// 在适当的时候中断线程
thread.interrupt();
  • 1
  • 2
  • 3
  • 4
  • 5

上述示例中,使用interrupt()方法中断了一个线程。被中断的线程可以通过检查isInterrupted()方法来获取中断状态,然后根据需要做出相应的处理。

请注意,中断只是向线程发送一个中断信号,并不会立即停止线程的执行。线程可以根据自己的逻辑和需要,在合适的时候响应中断信号并停止执行。

18.什么是线程的阻塞和非阻塞?

  1. 线程的阻塞和非阻塞是描述线程在执行过程中的状态。线程的阻塞指的是线程在某些条件下无法继续执行,暂时停止执行直到满足特定条件。线程的非阻塞指的是线程在执行过程中可以继续进行,不会被特定条件阻塞。

在Java中,线程的阻塞和非阻塞通常与线程的状态相关:

  • 阻塞状态(Blocked):线程在等待某些条件满足时进入阻塞状态,无法继续执行。常见的阻塞场景包括线程等待锁、等待输入/输出操作完成、等待某个条件满足等。
  • 非阻塞状态(Runnable):线程在可以继续执行的情况下处于非阻塞状态,可以执行自己的任务。

线程的状态可以通过调用线程的getState()方法来获取。

线程的阻塞和非阻塞是根据线程当前的执行状态来描述的,并与线程所处的上下文环境和特定条件相关。线程的状态会随着执行过程的不同而发生变化。

19.什么是线程的状态?Java中的线程状态有哪些?

线程的状态指的是线程在不同时间点的状态或条件。在Java中,线程的状态由Java Thread类中的枚举Thread.State表示。下面是Java中线程的几种状态:

  1. NEW(新建):当线程对象被创建时,它处于新建状态。在新建状态下,线程尚未启动。
  2. RUNNABLE(可运行):一旦调用了线程对象的start()方法,线程进入可运行状态。处于可运行状态的线程可能正在执行,也可能正在等待系统资源(如处理器时间片)。
  3. BLOCKED(阻塞):线程在等待获取锁时,如果锁被其他线程持有,该线程将进入阻塞状态。当锁可用时,线程将变为可运行状态。
  4. WAITING(等待):线程在调用了Object.wait()、Thread.join()或LockSupport.park()方法后,会进入等待状态。等待状态的线程需要等待其他线程的通知或特定条件的满足才能继续执行。
  5. TIMED_WAITING(计时等待):与等待状态类似,但具有等待超时时间。线程可以在指定的时间内等待其他线程的通知或特定条件的满足。
  6. TERMINATED(终止):线程执行完其任务或因异常而终止后,进入终止状态。

通过监视线程的状态,我们可以了解线程在执行过程中的行为和条件,以便进行适当的控制和管理。

20.什么是线程的上下文切换?它会带来什么开销?

线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。

  • 主动让出 CPU,比如调用了 sleep() , wait() 等。
  • 时间片用完,因为操作系统要防止⼀个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
  • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
  • 被终止或结束运行

这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用CPU 的时候恢复现场。并加载下⼀个将要占用 CPU 的线程上下文。这就是所谓的上下文切换
上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有⼀定损耗,如果频繁切换就会造成整体效率低下。

上下文切换会带来一些开销,包括以下方面:

  1. 寄存器保存和恢复:在切换线程时,需要保存当前线程的寄存器值,并在切换回来时恢复这些值。这涉及到寄存器的读写操作,可能需要将寄存器的内容写入内存或从内存中读取到寄存器中。
  2. 内存重定位:当线程切换时,内存中的数据可能已经被其他线程修改,因此需要将内存中的数据刷新或重定位到适当的位置。
  3. 虚拟内存切换:如果线程切换涉及到不同的虚拟内存空间,可能需要更新内存映射表和页表等数据结构,以便将新线程的虚拟地址映射到正确的物理内存地址。
  4. 缓存刷新:当线程切换时,当前线程的缓存中的数据可能无效,需要将缓存中的数据刷新或清除,以便新线程能够正确地访问内存。

上下文切换的开销是非常昂贵的,会有⼀定损耗,因为它涉及到多个复杂的操作和数据结构更新。因此,在设计和实现多线程应用程序时,应尽量减少上下文切换的次数,以提高系统的性能和效率。

21.什么是线程的活锁和饥饿现象?

**活锁(Livelock)**是指线程在执行过程中相互响应对方的动作,导致它们无法继续执行任务。在活锁中,线程不断改变自己的状态,但整体上并未取得任何进展,因此无法完成任务。活锁通常是由于线程的行为逻辑不正确或互相干扰导致的。例如,两个线程互相让步或互相请求共享资源,导致它们无法前进。

**饥饿现象(Starvation)**是指某个线程无法获取所需的资源以执行其任务,从而一直处于等待状态。饥饿通常是由于资源分配不公平或优先级设置不当导致的。当一个或多个线程无法获得所需的资源,而其他线程占用资源时间过长,就会出现饥饿现象。被饥饿的线程可能无法正常执行,甚至长时间无法获得资源,从而导致系统性能下降或任务无法完成。

活锁和饥饿都是多线程并发编程中的问题,它们会导致线程无法正常执行或无法取得进展,从而影响系统的稳定性和性能。

22.什么是线程的并发和并行?

**并发(Concurrency)**是指多个任务在同一个时间段内执行,它们之间通过时间片轮转或并发调度算法进行交替执行。在并发中,任务之间并不一定是同时进行的,而是根据调度算法在时间上交替执行。并发能够提高系统资源的利用率和吞吐量,但并不一定加快任务的完成时间。

**并行(Parallelism)**是指多个任务同时执行,每个任务在不同的处理器核心或计算单元上独立执行。在并行中,任务之间是真正同时进行的,它们可以通过多个独立的执行路径同时完成。并行可以加快任务的完成时间,特别是在多核处理器或分布式系统中。

简而言之,并发是指多个任务交替执行,而并行是指多个任务同时执行。并发是通过时间上的分片来实现任务的并发执行,而并行是通过物理上的并行处理单元来实现任务的同时执行。

23.什么是原子操作?Java中的原子操作有哪些?

**原子操作(Atomic Operation)**是指在并发环境下不可中断的单个操作。原子操作要么完全执行,要么不执行,不会出现中间状态或部分执行的情况。在多线程编程中,原子操作保证了数据的一致性和可靠性,避免了数据竞争和并发冲突的问题。

在Java中,Java.util.concurrent.atomic包提供了一些原子操作的类。以下是一些常见的Java原子操作类:

  1. AtomicInteger:用于对整型数据进行原子操作,如增减操作、读取和设置操作。
  2. AtomicLong:用于对长整型数据进行原子操作,功能类似于AtomicInteger。
  3. AtomicBoolean:用于对布尔类型数据进行原子操作,支持原子的读取和设置操作。
  4. AtomicReference:用于对引用类型数据进行原子操作,可以原子地更新引用对象的值。
  5. AtomicIntegerArray:用于对整型数组进行原子操作,提供了原子的读取和更新操作。
  6. AtomicLongArray:用于对长整型数组进行原子操作,功能类似于AtomicIntegerArray。
  7. AtomicReferenceArray:用于对引用类型数组进行原子操作,支持原子的数组元素读取和更新操作。

这些原子操作类提供了线程安全的原子操作方法,可以保证操作的原子性,减少并发编程中的竞态条件和数据冲突问题。

24.什么是线程安全的集合?Java中有哪些线程安全的集合类?

线程安全的集合是指多个线程可以同时访问和修改的集合,而不会引发数据不一致或产生竞态条件等并发问题。在并发编程中,线程安全的集合可以确保多个线程对集合的操作是正确的、可靠的和一致的。

Java中提供了许多线程安全的集合类,它们位于java.util.concurrent包下,以下是其中几个常用的线程安全集合类:

  1. ConcurrentHashMap:线程安全的哈希表实现,适用于多线程环境下的高并发读写操作。
  2. CopyOnWriteArrayList:线程安全的动态数组实现,通过复制整个数组来实现并发安全,适用于读操作频繁而写操作较少的场景。
  3. CopyOnWriteArraySet:线程安全的集合类,基于CopyOnWriteArrayList实现,适用于读操作频繁而写操作较少的场景。
  4. ConcurrentLinkedQueue:线程安全的队列实现,适用于高并发的生产者-消费者模式。
  5. ConcurrentSkipListMap:线程安全的有序映射表实现,基于跳表(Skip List)数据结构,适用于高并发读写操作的有序映射。
  6. ConcurrentSkipListSet:线程安全的有序集合类,基于ConcurrentSkipListMap实现,适用于高并发读写操作的有序集合。

25.什么是线程的可见性问题?Java如何解决可见性问题?

线程的可见性问题是指当多个线程同时访问共享变量时,一个线程对共享变量的修改可能对其他线程不可见,导致数据不一致或意外的行为。这是由于每个线程都有自己的工作内存,线程在执行过程中将共享变量从主内存中拷贝到自己的工作内存中进行操作,操作完成后再写回主内存,这个过程中可能存在延迟、重排序或缓存等问题,从而导致可见性问题。

Java提供了以下机制来解决线程的可见性问题:

  1. volatile关键字:使用volatile关键字修饰的变量可以确保对该变量的读写操作都是直接在主内存中进行的,而不是在线程的工作内存中进行。这样可以保证当一个线程修改了volatile变量的值时,其他线程能够立即看到最新的值。
  2. 同步机制(如synchronizedLock):使用同步机制可以确保在同一个锁范围内,对共享变量的读写操作都是原子性的,且在释放锁之前会将修改的值刷新回主内存,从而保证可见性。
  3. final关键字:对于final修饰的字段,一旦初始化完成后,其值对于其他线程是可见的。因此,可以使用final关键字来确保共享变量的可见性。
  4. java.util.concurrent工具类:Java提供了许多并发工具类,如CountDownLatchCyclicBarrierSemaphore等,这些工具类不仅提供了线程间的同步机制,同时也提供了可见性保证,确保在特定的同步点前后对共享变量的修改是可见的。

26.什么是线程的乐观锁和悲观锁?Java如何使用它们?

乐观锁(Optimistic Locking)和悲观锁(Pessimistic Locking)是并发编程中用于处理共享资源访问冲突的两种不同策略。

悲观锁是一种保守的策略,它假设会发生冲突,因此在访问共享资源之前会先获取锁,以防止其他线程对资源进行修改。悲观锁常用的实现方式是使用synchronized关键字或Lock接口的具体实现类来实现对共享资源的加锁。当一个线程获取到悲观锁后,其他线程需要等待该线程释放锁才能访问共享资源。悲观锁适用于并发冲突频繁的场景,但它可能会带来额外的开销和竞争。

乐观锁则是一种乐观的假设,它认为冲突的概率较低,因此不会立即加锁,而是在修改共享资源时先进行检查,如果发现其他线程已经修改了该资源,则进行回退、重试或放弃操作。乐观锁常用的实现方式是使用版本号(Versioning)或时间戳(Timestamp)等机制来判断资源是否被修改。在Java中,乐观锁常用的实现方式是通过Atomic类(如AtomicInteger、AtomicLong)和CAS(Compare and Swap)操作来实现。CAS操作是一种原子操作,它可以比较当前值与期望值,如果相等则进行更新,否则不进行更新,可以保证操作的原子性和可见性。

Java中的java.util.concurrent.atomic包提供了一系列原子类,如AtomicInteger、AtomicLong、AtomicReference等,它们利用乐观锁的思想来实现对共享资源的线程安全访问。这些原子类的方法使用了CAS操作,避免了显式加锁,从而提高了并发性能。

27.什么是线程的读写锁(ReentrantReadWriteLock)?如何使用它?

读写锁(ReentrantReadWriteLock)是Java中提供的一种高级锁机制,用于解决读写操作的并发性问题。读写锁允许多个线程同时读共享资源,但只允许一个线程进行写操作。这种锁设计适用于读多写少的场景,可以提高并发读操作的性能。

读写锁内部维护了两个锁:读锁和写锁。多个线程可以同时获取读锁,只要没有线程持有写锁。当某个线程持有写锁时,其他线程无法获取读锁或写锁,从而保证了对共享资源的独占性。

读写锁的使用方式如下:

import java.util.concurrent.locks.ReentrantReadWriteLock;

// 创建读写锁实例
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

// 读操作
rwLock.readLock().lock();
try {
    // 读共享资源的操作
} finally {
    rwLock.readLock().unlock();
}

// 写操作
rwLock.writeLock().lock();
try {
    // 写共享资源的操作
} finally {
    rwLock.writeLock().unlock();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在读操作中,通过readLock().lock()获取读锁,对共享资源进行读操作,然后使用readLock().unlock()释放读锁。

在写操作中,通过writeLock().lock()获取写锁,对共享资源进行写操作,然后使用writeLock().unlock()释放写锁。

28.什么是线程的条件变量(Condition)?如何使用条件变量?

线程的条件变量(Condition)是Java中用于线程间通信和线程调度的一种机制。它通常与锁(Lock)结合使用,用于实现线程的等待和唤醒操作。

条件变量可以让一个或多个线程在某个条件满足时等待,直到其他线程通过唤醒操作通知它们继续执行。条件变量提供了await()方法用于线程等待和释放锁,并提供了signal()signalAll()方法用于唤醒等待的线程。

使用条件变量的一般步骤如下:

  1. 创建一个锁(Lock)实例,并通过该锁创建一个条件变量(Condition)实例:

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    
    • 1
    • 2
  2. 在获取锁后,通过条件变量的await()方法进入等待状态,同时释放锁:

    lock.lock();
    try {
        while (条件不满足) {
            condition.await();
        }
    } finally {
        lock.unlock();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  3. 在某个线程中满足条件时,通过条件变量的signal()signalAll()方法唤醒等待的线程:

    lock.lock();
    try {
        // 修改条件
        condition.signal(); // 或 condition.signalAll();
    } finally {
        lock.unlock();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  4. 被唤醒的线程会重新尝试获取锁,并继续执行。

条件变量的使用允许线程在满足特定条件之前等待,从而有效地实现线程间的通信和协调。

29.什么是线程的原始锁(Lock)?如何使用原始锁?

线程的原始锁(Lock)是Java中提供的一种用于同步访问共享资源的机制。与传统的synchronized关键字相比,原始锁提供了更多的灵活性和功能。

原始锁提供了显式的加锁和释放锁操作,可以实现更细粒度的锁定控制。它允许更灵活的锁定和解锁方式,支持可重入性、公平性和超时控制等特性。

使用原始锁的一般步骤如下:

  1. 创建一个锁(Lock)实例:

    Lock lock = new ReentrantLock();
    
    • 1
  2. 在需要加锁的代码块前调用锁的lock()方法获取锁:

    lock.lock();
    try {
        // 访问共享资源的操作
    } finally {
        lock.unlock();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    注意在使用try-finally块时,确保在任何情况下都会释放锁,以避免死锁的发生。

原始锁的使用方式允许更细粒度的锁定和解锁控制,可以提供更高的灵活性和性能。此外,原始锁还提供了一些其他方法,如tryLock()用于尝试非阻塞地获取锁,newCondition()用于创建条件变量等,以满足不同的并发编程需求。

30.什么是线程的自旋锁(Spin Lock)?如何使用自旋锁?

线程的自旋锁(Spin Lock)是一种基于忙等待(Busy-Waiting)的锁机制,它在获取锁时不会立即进入阻塞状态,而是循环检测锁的状态,直到获取到锁为止。自旋锁适用于临界区较小且锁竞争短暂的情况。

使用自旋锁的一般步骤如下:

  1. 定义一个标志位作为锁的状态:

    private volatile boolean locked = false;
    
    • 1

    这里使用volatile关键字确保线程之间的可见性。

  2. 在获取锁时,通过循环检测锁的状态,直到获取到锁:

    while (!locked) {
        // 自旋等待,不执行任何操作或进行短暂的延时
    }
    
    • 1
    • 2
    • 3
  3. 在释放锁时,将标志位设置为未锁定状态:

    locked = false;
    
    • 1

自旋锁的特点是在竞争激烈的情况下,可能会造成较高的CPU占用,因为线程会忙碌地进行循环检测锁的状态。因此,自旋锁通常在以下情况下使用:

  • 临界区较小,锁竞争时间短暂,可以快速获取到锁的情况。
  • 并发竞争较轻,很少有其他线程争用锁的情况。
  • 在多处理器或多核系统中,自旋锁能够有效利用处理器的时间片,提高并发性能。

31.什么是线程的可重入锁(ReentrantLock)?如何使用可重入锁?

线程的可重入锁(ReentrantLock)是Java中提供的一种独占锁机制,它允许同一个线程多次获取同一个锁,即支持重入性。可重入锁是一种线程安全的锁,它提供了与synchronized关键字类似的功能,但更加灵活和可扩展。

可重入锁内部维护了一个持有锁的线程和一个计数器,当一个线程第一次获取锁时,计数器加1,当同一个线程再次获取锁时,计数器再次加1。只有当线程释放锁的次数与获取锁的次数相等时,其他线程才能获取该锁。

使用可重入锁的一般步骤如下:

  1. 创建一个可重入锁实例:

    ReentrantLock lock = new ReentrantLock();
    
    • 1
  2. 在需要加锁的代码块前调用锁的lock()方法获取锁:

    lock.lock();
    try {
        // 访问共享资源的操作
    } finally {
        lock.unlock();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    注意在使用try-finally块时,确保在任何情况下都会释放锁,以避免死锁的发生。

可重入锁允许同一个线程多次获取同一个锁,这使得线程可以重复进入自己已经拥有的锁所保护的临界区。这种机制提供了更大的灵活性和控制权,可以防止死锁,并允许线程在已经获取锁的情况下递归地调用同步方法。

可重入锁还提供了一些其他方法,如tryLock()用于尝试非阻塞地获取锁,newCondition()用于创建条件变量等,以满足不同的并发编程需求。

总之,可重入锁是一种强大而灵活的锁机制,它可以替代synchronized关键字,并提供更多的功能和控制选项。

32.什么是线程的阻塞队列(BlockingQueue)?如何使用阻塞队列?

线程的阻塞队列(BlockingQueue)是Java中提供的一种线程安全的队列实现,它在队列为空时会阻塞获取操作,并在队列已满时会阻塞插入操作,从而实现线程间的同步和协调。

阻塞队列提供了一组阻塞操作,包括阻塞的插入和获取元素的方法,使得线程可以安全地进行等待和唤醒操作,而不需要手动地实现等待/唤醒的逻辑。

使用阻塞队列的一般步骤如下:

  1. 创建一个阻塞队列实例:

    BlockingQueue<T> queue = new ArrayBlockingQueue<>(capacity);
    
    • 1

    这里的T表示队列中元素的类型,capacity表示队列的容量。

  2. 插入元素到队列中:

    queue.put(element); // 阻塞插入操作,如果队列已满则阻塞等待
    
    • 1
  3. 获取队列中的元素:

    T element = queue.take(); // 阻塞获取操作,如果队列为空则阻塞等待
    
    • 1

    注意,如果队列为空,则获取操作会被阻塞,直到队列中有元素可供获取。

阻塞队列还提供了一些其他的方法,如offer()用于尝试插入元素而不阻塞,poll()用于尝试获取元素而不阻塞,以及一些用于获取队列状态的方法,如size()isEmpty()等。

33.什么是线程的同步器(Synchronizer)?Java中有哪些同步器?

线程的同步器(Synchronizer)是一种用于控制多个线程之间协调和同步的机制。它可以用来实现线程间的互斥访问和临界区保护,以及线程间的等待和唤醒操作。

Java中提供了几种常用的同步器,包括:

  1. Mutex(互斥锁):最基本的同步器,通过synchronized关键字实现。它保证了临界区的互斥访问,同一时刻只有一个线程可以进入临界区。
  2. CountDownLatch(倒计时门闩):一种多线程协作的同步器,它允许一个或多个线程等待其他线程完成操作后再继续执行。它通过计数器来实现,当计数器的值减为0时,等待的线程会被唤醒。
  3. CyclicBarrier(循环屏障):一种多线程协作的同步器,它允许多个线程在某个屏障点上相互等待,直到所有线程都到达屏障点后再继续执行。与CountDownLatch不同,CyclicBarrier的计数器可以重复使用。
  4. Semaphore(信号量):一种用于控制同时访问特定资源的线程数量的同步器。它通过维护一定数量的许可证(permits)来实现,线程需要获取许可证才能继续执行,如果没有许可证可用,则需要等待其他线程释放许可证。
  5. Exchanger(交换器):一种支持两个线程间数据交换的同步器。它允许两个线程在一个点上交换数据,当两个线程都到达交换点时,它们可以交换数据并继续执行。

34.什么是线程的线程局部变量(ThreadLocal)?Java如何使用线程局部变量?

线程的线程局部变量(ThreadLocal)是Java中的一种特殊变量,它为每个线程提供了独立的变量副本,每个线程可以独立地操作自己的变量副本,互不干扰。线程局部变量在多线程环境下可以实现线程间数据的隔离,每个线程都可以独立地设置和获取自己的变量值。

Java中使用线程局部变量的一般步骤如下:

  1. 创建一个线程局部变量实例:

    ThreadLocal<T> threadLocal = new ThreadLocal<>();
    
    • 1

    这里的T表示线程局部变量的类型。

  2. 在某个线程中设置变量值:

    threadLocal.set(value);
    
    • 1

    这样就将value设置为当前线程的线程局部变量的值。

  3. 在其他地方获取线程局部变量的值:

    T value = threadLocal.get();
    
    • 1

    这样就可以获取当前线程的线程局部变量的值。

线程局部变量的特点是每个线程都有自己的独立副本,线程之间的变量操作互不干扰,因此可以实现线程间的数据隔离。线程局部变量常用于以下场景:

  • 线程池中的线程,每个线程需要独立地维护自己的状态或上下文信息。
  • Web应用程序中的请求处理,每个请求需要独立地携带自己的数据。

需要注意的是,使用线程局部变量时要小心内存泄漏问题,因为线程局部变量的生命周期与线程的生命周期一致,如果没有适时地清理线程局部变量的值,可能会导致内存泄漏。可以通过remove()方法或使用ThreadLocalinitialValue()方法进行清理和初始化操作。

35.什么是线程的线程池(Executor)?Java中有哪些线程池类?

线程的线程池(Executor)是一种用于管理和复用线程的机制,它可以提高多线程应用程序的性能和资源利用率。线程池通过预先创建一定数量的线程,并维护一个任务队列,来执行提交给它的任务。

Java中提供了一些线程池类,常用的线程池类包括:

  1. Executor:是线程池的顶层接口,定义了线程池的基本行为和方法。
  2. Executors:是一个工具类,提供了一些静态方法用于创建不同类型的线程池。
  3. ThreadPoolExecutor:是Java提供的最常用的线程池实现类。它提供了灵活的线程池配置选项,如核心线程数、最大线程数、线程空闲时间、任务队列等。
  4. ScheduledExecutorService:是一个支持定时任务调度的线程池接口,它扩展了ExecutorService接口,可以执行延迟任务和周期任务。
  5. ForkJoinPool:是Java提供的一种特殊类型的线程池,用于执行分治任务。它基于工作窃取算法,将任务分割成更小的子任务,并由多个工作线程并发执行。

这些线程池类提供了不同的功能和使用方式,可以根据具体的需求选择合适的线程池类。通常情况下,可以通过Executors类的静态方法来创建线程池,例如:

ExecutorService executor = Executors.newFixedThreadPool(10);
  • 1

这样就创建了一个固定大小为10的线程池。

使用线程池的好处包括:

  • 重用线程:避免了频繁地创建和销毁线程的开销。
  • 控制并发度:可以通过配置线程池的核心线程数和最大线程数来控制并发执行的线程数量。
  • 任务队列:线程池提供了一个任务队列,可以存储待执行的任务,避免任务丢失或过载。
  • 管理线程:线程池可以监控和管理线程的执行状态,如线程池的活跃线程数、完成任务数等。

使用线程池可以更好地管理多线程任务,提高程序性能和资源利用率,减少线程创建和销毁的开销,并提供更好的线程管理和控制机制。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号