Java并发

多线程

1.介绍一下Java的内存模型(JMM)

JMM是专门解决多线程并发问题的一套规则,核心是处理可见性,原子性和有序性这三个问题

可见性:使用volatile关键字解决,当修改被volatile修饰的变量后,会立刻刷会主内存,同时让其他线程的缓存失效

原子性:使用synchronized 或Lock锁保证原子性以及atomic包

有序性:使用volatile或者synchronized,通过内存屏障阻止这种重排序

2.Java多线程是什么

在一个 Java 程序中同时运行多个线程,这些线程共享程序的内存空间(如全局变量、方法区等),但有各自的栈和程序计数器,能同时执行不同的任务。

使用 Java 多线程需要注意以下几点:

首先是线程安全问题。多个线程同时操作共享数据时,可能出现错误。需要用synchronized关键字、Lock锁等方式,保证同一时间只有一个线程操作共享数据。.

其次是线程间通信。线程需要协作时,比如一个线程生产数据,另一个线程消费数据,要通过wait()、notify()等方法控制,避免出现一方没准备好,另一方就操作的情况,否则可能导致数据错误或线程无限等待。

然后是线程的创建和销毁成本。频繁创建和销毁线程会消耗系统资源,影响性能。可以用线程池管理线程,提前创建好一定数量的线程,重复使用,减少资源消耗。

3.线程创建有哪些方式

  • 继承Thread类
  • 实现Runnable接口,作为Thread类构造器的参数
  • 实现Callable接口,call方法有返回值,需要把它包装到FutureTask(可以接受Runnable和Callable)里,再作为Thread类构造器的参数
  • 使用线程池

4.如何停止线程

  • 异常法停止:线程调用interrupt()方法后,在线程的run方法中判断当前对象的interrupted()状态,如果是中断状态则抛出异常,达到中断线程的效果。
  • 在沉睡中停止:先将线程sleep,然后调用interrupt标记中断状态,interrupt会将阻塞状态的线程中断。会抛出中断异常,重新设置中断位,达到停止线程的效果
  • stop()暴力停止:线程调用stop()方法会被暴力停止,方法已弃用,该方法会有不好的后果:强制让线程停止有可能使一些请理性的工作得不到完成。
  • 使用return停止线程:调用interrupt标记为中断状态后,在run方法中判断当前线程状态,如果为中断状态则return,能达到停止线程的效果。
  • Future.cancel

5.java线程的状态

6.sleep和wait区别

sleep和wait都会进入超时等待状态,区别在于sleep可以在任意位置调用,超时自动恢复,与锁无关,会释放CPU。

而wait必须在同步代码块中才能调用,需要notify/notifyall或者超时恢复,调用之后释放锁,释放CPU

7.线程之间的通信方式

Object类的wait,notify方法,基于对象的监视器机制

Lock和Condition接口,await singal方法 基于ReentranLock

volatile

CountDownLatch 允许线程等待其他线程完成操作后再执行

juc包中的BlockingQueue,信号量(Semaphore)

并发安全

1.java中有哪些常用的锁

内置锁(synchronized):可以用于方法或代码块。当一个线程进入synchronized代码块或方法时,它会获取关联对象的锁;当线程离开该代码块或方法时,锁会被释放。如果其他线程尝试获取同一个对象的锁,它们将被阻塞,直到锁被释放。

ReentrantLock:如可中断的锁等待、定时锁等待、公平锁选项等。ReentrantLock使用lock()和unlock()方法来获取和释放锁。

读写锁(ReadWriteLock):允许多个读取者同时访问共享资源,但只允许一个写入者。读写锁通常用于读取远多于写入的情况,以提高并发性。

乐观锁和悲观锁:悲观锁(Pessimistic Locking)通常指在访问数据前就锁定资源,假设最坏的情况,即数据很可能被其他线程修改。synchronized和ReentrantLock都是悲观锁的例子。

乐观锁(Optimistic Locking)通常不锁定资源,而是在更新数据时检查数据是否已被其他线程修改。乐观锁常使用版本号或时间戳来实现。 CAS

自旋锁:自旋锁是一种锁机制,线程在等待锁时会持续循环检查锁是否可用,而不是放弃CPU并阻塞。通常可以使用CAS来实现。这在锁等待时间很短的情况下可以提高性能,但过度自旋会浪费CPU资源

2.CountDownLatch的原理

用于让一个或多个线程等待其他线程完成操作后再继续执行。

其核心是通过一个计数器(Counter)实现线程间的协调,常用于多线程任务的分阶段控制或主线程等待多个子线程就绪的场景,

核心原理:

初始化计数器:创建 CountDownLatch 时指定一个初始计数值(如 N)。

等待线程阻塞:调用 await() 的线程会被阻塞,直到计数器变为 0。

任务完成通知:其他线程完成任务后调用 countDown(),使计数器减 1。

唤醒等待线程:当计数器减到 0 时,所有等待的线程会被唤醒。

3.synchronized工作原理

synchronized是Java提供内置锁,也被称为监视器锁。

它的主要原理是基于对象头和监视器实现,对象头中有一个Mark Word(标记字段),里面存储了锁的状态信息,包含了锁标识位和偏向锁标识位。

每个对象关联一个Monitor,用于实现互斥和同步。当synchronized修饰代码块的时候,在字节码文件中,进入同步代码块前,执行monitorenter指令时会尝试获取monitor,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。离开同步代码块后,执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。synchronized修饰方法的时候,通过标志位判断是否为同步方法。(原子性)

有两个队列entryList和waitSet,entryList存放就绪态的线程,waitSet存放调用wait后的线程,可以被notify唤醒

从内存语义上来讲,加锁(获取 Monitor) :JVM 会将主内存中的共享变量最新值更新到当前线程的本地内存。解锁(释放 Monitor):JVM 会将当前线程的本地内存中的共享变量刷新到主内存。(可见性)

有序性通过可见性和原子性实现(临界区全部完成"或"临界区还没开始)

4.Reentranlock工作原理

ReentrantLock 的底层实现主要依赖于 AbstractQueuedSynchronizer(AQS)这个抽象类。

AQS 是一个提供了基本同步机制的框架,其中包括了队列、状态值等。

ReentrantLock 在 AQS 的基础上通过内部类 Sync 来实现具体的锁操作。不同的 Sync 子类实现了公平锁和非公平锁的不同逻辑。

  • 可中断性: ReentrantLock 实现了可中断性,这意味着线程在等待锁的过程中,可以被其他线程中断而提前结束等待。
  • 设置超时时间: ReentrantLock 支持在尝试获取锁时设置超时时间,即等待一定时间后如果还未获得锁,则放弃锁的获取。
  • 公平锁和非公平锁: 在直接创建 ReentrantLock 对象时,默认情况下是非公平锁。公平锁是按照线程等待的顺序来获取锁,而非公平锁则允许多个线程在同一时刻竞争锁,不考虑它们申请锁的顺序。
  • 多个条件变量: ReentrantLock 支持多个条件变量,每个条件变量可以与一个 ReentrantLock 关联。
  • 可重入性: ReentrantLock 支持可重入性,即同一个线程可以多次获得同一把锁,而不会造成死锁。通过计数器的形式,一个线程获取锁之后,计数器+1,再次获取锁,再+1;释放锁,-1,直到为0,锁才完全释放。

5.synchronized和reentrantlock区别

  • 用法不同:synchronized 可用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用在代码块上。
  • 获取锁和释放锁方式不同:synchronized 会自动加锁和释放锁,当进入 synchronized 修饰的代码块之后会自动加锁,当离开 synchronized 的代码段之后会自动释放锁。而 ReentrantLock 需要手动加锁和释放锁
  • 锁类型不同:synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁。
  • 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
  • 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。
  • 都是可重入锁

6.synchronized锁升级过程

无锁:没有开启偏向锁的时候的状态,有一个偏向延迟,需要在JVM启动之后的多少秒之后才能开启

偏向锁:当一个线程访问同步代码块并获取锁时,该锁会进入偏向模式,锁标志的状态将被设置为偏向(01),并且锁的拥有者被设置为当前线程(偏向锁线程 id = 当前线程 id)。当该线程执行完同步代码块后,线程并不会主动释放偏向锁。当线程再次进入同步代码块时,会首先判断此时持有锁的线程与它是否为同一线程,如果是则正常往下执行,由于此前是没有释放锁的,所以这次就不会有任何的获取锁操作。

轻量级锁:当一个线程持有偏向锁时,另外一个线程来竞争锁,这时偏向锁就会升级为轻量级锁。轻量级锁的竞争方式一种比较轻量级的竞争方式,当某个线程没有获取到锁,它并不是立刻被挂起,而是采取自旋的方式来竞争锁资源

重量级锁:轻量级锁自旋是要有限度的,不能一直在那里空转,所以如果锁竞争环境比较严重,当自旋次数达到某个阈值(默认 10 次,可自动调整 )后,就是停止自旋,此时锁膨胀为重量级锁。当其膨胀为重量级锁后,其他线程就不再是等待了,而是阻塞等待。重量级锁依赖对象内部的监视器(monitor)实现

7.JVM对synchronized的优化?

锁膨胀:synchronized 从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它叫做锁膨胀也叫做锁升级。JDK 1.6 之前,synchronized 是重量级锁,也就是说 synchronized 在释放和获取锁时都会从用户态转换成内核态,而转换的效率是比较低的。但有了锁膨胀机制之后,synchronized 的状态就多了无锁、偏向锁以及轻量级锁了,这时候在进行并发操作时,大部分的场景都不需要用户态到内核态的转换了,这样就大幅的提升了 synchronized 的性能。

锁消除:指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。

锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。

自适应自旋锁:指通过自身循环,尝试获取锁的一种方式,优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的,所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。

8.AQS

AQS全称为AbstractQueuedSynchronizer,是Java中的一个抽象类。 AQS是一个用于构建锁、同步器、协作工具类的工具类(框架)。

AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。

最核心的三部分:

状态:state;在不同的实现类中有不同含义,在reentrantlock中,表示可重入计数。用volatile关键字修饰,修改state(CAS)的函数需要保证线程安全

控制线程抢锁和配合的FIFO队列(双向链表);当线程获取锁失败时,将其存放到队列中;等到锁释放时,挑选一个线程占有锁

期望协作工具类去实现的获取/释放等重要方法(重写):获取方法,释放方法

9.CAS

原理

CAS是一种乐观锁机制,它包含三个操作数:内存位置(V)、预期值(A)和新值(B)。CAS 操作的逻辑是,如果内存位置 V 的值等于预期值 A,则将其更新为新值 B,否则不做任何操作。整个过程是原子性的,通常由硬件指令支持。

缺点

ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。对比值和版本号去解决

循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。

只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。

10.ThreadLocal作用,原理

原理

Thread类中有一个ThreadLocalMap字段,key是ThreadLocal实例,value是存储的值。当调用ThreadLocal的set方法时,在当前线程的ThreadLocalMap中存储一个键值对,键是ThreadLocal对象自身,值是传入的值。

调用get方法时,ThreadLocal会检查当前线程的ThreadLocalMap中是否有与之关联的值。如果有,返回该值;如果没有,会调用initialValue()方法(null)来初始化该值,然后将其放入ThreadLocalMap中并返回。

作用

为每个线程提供了独立的变量副本,线程之间不会互相影响,同一个线程之间减少参数的传递。

问题

存在内存泄露:ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,不被回收,只有在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。

11.volatile关键字的作用

保证变量对所有线程的可见性。

当一个变量被声明为volatile时,它会保证对这个变量的写操作会立即刷新到主存中,而对这个变量的读操作会直接从主存中读取,从而确保了多线程环境下对该变量访问的可见性。这意味着一个线程修改了volatile变量的值,其他线程能够立刻看到这个修改,不会受到各自线程工作内存的影响。

禁止指令重排序优化。

volatile关键字在Java中主要通过内存屏障来禁止特定类型的指令重排序。

  • StoreStore屏障 :禁止volatile写与之前的普通写重排序。

  • StoreLoad屏障 :禁止volatile写与之后的读操作重排序。

  • LoadLoad屏障 :禁止volatile读与之后的普通读重排序。

  • LoadStore屏障 :禁止volatile读与之后的普通写重排序。

12.什么情况会产生死锁,如何解决?

死锁只有同时满足以下四个条件才会发生:

  • 互斥条件:互斥条件是指多个线程不能同时使用同一个资源。

  • 持有并等待条件:持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。

  • 不可剥夺条件:不可剥夺条件是指,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。

  • 环路等待条件:环路等待条件指的是,在死锁发生的时候,两个线程获取资源的顺序构成了环形链。

解决:使用资源有序分配法(获取资源的顺序各个线程是一致的)

线程池

1.线程池的工作原理

手动创建线程池

参数及设置

Java 复制代码
// 手动配置线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(    
    corePoolSize, // 核心线程数 线程池长期维持的最小线程数    
    corePoolSize * 2, // 最大线程数 线程池能容纳的最多线程数    
    60L, // 空闲线程存活时间 超过核心线程数的空闲线程 多久后销毁    
    TimeUnit.SECONDS, // 存活时间单位    
    new ArrayBlockingQueue<>(100), // 任务阻塞队列 核心线程忙时 新任务存这里    
    Executors.defaultThreadFactory(), // 线程创建工厂 用于设置线程名 优先级等    
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略 队列满且线程数达最大时 如何处理新任务);
    // 提交任务的两种方式和之前一致 这里用execute提交Runnable(无返回值 不能捕获异常)
    threadPool.execute(() -> {    System.out.println(IO任务执行中 + Thread.currentThread().getName());    
    // 模拟IO操作 比如数据库查询 网络请求});
   	// 关闭线程池 推荐用shutdown 等待已提交任务完成后再关闭
    threadPool.shutdown();

核心线程数:CPU密集型:CPU核心数+1

​ IO密集型:CPU核数*2

拒绝策略:默认的(直接抛异常);提交任务的线程自己执行;丢弃;丢弃最旧的,在提交新任务

队列:阻塞队列,同步队列(不缓存任务)

工作原理

2.线程池的种类

ScheduledThreadPool:可以设置定期的执行任务,它支持定时或周期性执行任务,比如每隔 10 秒钟执行一次任务,我通过这个实现类设置定期执行任务的策略。

FixedThreadPool:它的核心线程数和最大线程数是一样的,所以可以把它看作是固定线程数的线程池,它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就是固定的,就算任务数超过线程数,线程池也不会再创建更多的线程来处理任务,而是会把超出线程处理能力的任务放到任务队列中进行等待。而且就算任务队列满了,到了本该继续增加线程数的时候,由于它的最大线程数和核心线程数是一样的,所以也无法再增加新的线程了。

CachedThreadPool:可以称作可缓存线程池,它的特点在于线程数是几乎可以无限增加的(实际最大可以达到 Integer.MAX_VALUE,为 2^31-1,这个数非常大,所以基本不可能达到),而当线程闲置时还可以对线程进行回收。也就是说该线程池的线程数量不是固定不变的,当然它也有一个用于存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。

SingleThreadExecutor:它会使用唯一的线程去执行任务,原理和 FixedThreadPool 是一样的,只不过这里线程只有一个,如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。这种线程池由于只有一个线程,所以非常适合用于所有任务都需要按被提交的顺序依次执行的场景,而前几种线程池不一定能够保障任务的执行顺序等于被提交的顺序,因为它们是多线程并行执行的。

SingleThreadScheduledExecutor:它实际和 ScheduledThreadPool 线程池非常相似,它只是 ScheduledThreadPool 的一个特例,内部只有一个线程。

这些可能会导致OOM

3.线程池的关闭方法

shutdown使用了以后会置状态为SHUTDOWN,正在执行的任务会继续执行下去,没有被执行的则中断。此时,则不能再往线程池中添加任何任务,否则将会抛出 RejectedExecutionException 异常

而 shutdownNow 为STOP,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。 它试图终止线程的方法是通过调用 Thread.interrupt() 方法来实现的,但是这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。

4.提交线程池的任务怎么被取消

可以,当向线程池提交任务时,会得到一个Future对象。

这个Future对象提供了几种方法来管理任务的执行,包括取消任务。

取消任务的主要方法是Future接口中的cancel(boolean mayInterruptIfRunning)方法。这个方法尝试取消执行的任务。

参数mayInterruptIfRunning指示是否允许中断正在执行的任务。如果设置为true,则表示如果任务已经开始执行,那么允许中断任务;如果设置为false,任务已经开始执行则不会被中断。

有正在执行的任务都执行完成了才能退出。

4.提交线程池的任务怎么被取消

可以,当向线程池提交任务时,会得到一个Future对象。

这个Future对象提供了几种方法来管理任务的执行,包括取消任务。

取消任务的主要方法是Future接口中的cancel(boolean mayInterruptIfRunning)方法。这个方法尝试取消执行的任务。

参数mayInterruptIfRunning指示是否允许中断正在执行的任务。如果设置为true,则表示如果任务已经开始执行,那么允许中断任务;如果设置为false,任务已经开始执行则不会被中断。

相关推荐
小当家.1052 小时前
JVM/八股详解(下部):垃圾收集、JVM 调优与类加载机制
java·jvm·面试
一念春风2 小时前
可视化视频编辑(WPF C#)
开发语言·c#·wpf
1***43802 小时前
MATLAB高效算法实战技术文章大纲工程领域的应用背景
开发语言·算法·matlab
天“码”行空2 小时前
java的设计模式-----------单例类
java·开发语言·设计模式
0***m8222 小时前
Java性能优化实战技术文章大纲性能优化的基本原则
java·开发语言·性能优化
艾莉丝努力练剑2 小时前
【QT】环境搭建收尾:认识Qt Creator
运维·开发语言·c++·人工智能·qt·qt creator·qt5
芒克芒克2 小时前
JVM性能监控
java·jvm
南棱笑笑生2 小时前
20260113给飞凌OK3588-C开发板适配Rockchip原厂的Android14系统时适配CAM3接口的OV5645
c语言·开发语言·rockchip