【并发】java并发重要知识点(面试常考!!深度理解!)

简述银行家算法

        当进程提出对资源申请的请求时,会先预判此次分配之后是否会导致系统进入不安全状态。如果经过预判分析之后,发现会进入不安全的状态则不进行此次分配,然后将进程阻塞等待。

        具体实现就是根据一个资源的分配表(包含进程的最大需求、已经分配、最多还需分配)来找出一条安全序列。通过资源数量减去已分配的数量,得出剩余资源的数量,并查看剩余的资源大于哪个进程的还需要分配的资源数量,然后将资源分配给它,然后等它执行完之后就会将所有用到的资源都归还,然后再根据现有的资源数进行对进程的资源分配,以此类推最终找到将所有进程都分配完资源的一条安全序列。(如果找不到一条安全序列的话,则证明当前状态还存在着潜在的死锁风险,需要采取想要的策略进行调整)

简述死锁产生的条件

1、互斥条件:一个资源只能被一个进程所占用,一个进程获取资源之后其他进程必须等待。

2、请求与保持条件:进程在持有部分资源的同时,又对其他资源进行了请求。如果进程(需要)持有的资源被别的进程占用,而该进程又被阻塞,等待其他资源就会导致死锁

3、不可剥夺:资源只能在进程使用完自行释放,不能被强制性的剥夺。即使其他进程请求该资源,持有该资源的进程也不能被迫释放

4、环路等待:存在一个进程资源的循环链,每个进程都在等待下一个进行所持有的资源,这样形成的一个闭环会导致死锁

解决死锁的几种方式

1、预防死锁:破坏死锁产生的四个必要条件,防止死锁的发生。例如可以限制进程对资源的申请数量和时间。

2、避免死锁:资源分配之前,使用算法来判断分配资源是否有导致死锁的可能,并根据算法的结果进行决策

3、检测和恢复死锁:周期性检测系统中是否存在死锁,并采取相应的措施来恢复死锁。比如,使用资源分配图或资源分配表来检测死锁,并且进行资源回收或进程终止来解除死锁状态

4、忽略死锁:选择忽视死锁问题,认为死锁发生的概率较低不进行显式的死锁处理措施。这种方法适用于具有较小规模和简单结构的系统,但并不推荐在需要高可靠性和资源利用率的系统中使用。

简述synchronized锁的膨胀

        首先是偏向锁,第一次进入到同步块的时候会先尝试获取偏向锁,偏向锁认为在大多数情况下,都是由同一个线程进行上锁的,所以为了避免频繁的上锁和解锁,所以引入了偏向锁使相同线程连续进行锁的获取的时候不用再重复获取然后释放,当其他线程需要获取锁的时候就进行了一个将偏向锁写到该线程身上的一个过程。需要注意的是,偏向锁每进行一次线程的切换,就会将计数加一,当到达阈值的时候,就会将锁升级为轻量级锁。

        轻量级锁(轻量级锁使用CAS操作来尝试获取锁),在使用轻量级锁的过程中,不会进行线程的上下文的切换,当一个线程获取到锁,另一个线程也需要获取锁的时候,此时另一个线程会自旋等待(线程忙等待)。需要注意的是,线程自旋等待也有一个次数时间限制,超过时间限制轻量级锁就将升级为重量级锁。

        重量级锁,在使用过程中拿到锁的进程会进行相应的操作,所有没有拿到锁的进程全都只能阻塞在队列上,直到获取到锁的进程释放掉锁,此时会尝试竞争锁然后完成线程所需要做的操作。

什么是cas

        CAS是并发编程中常用的原子操作,实现了多线程下的无锁同步。它能够保证对共享变量的原子性操作,解决并发访问的竞争问题。

执行过程如下:

1、首先读取内存位置的当前值,并与期望值做比较

2、如果与期望值一致,则证明内存位置的值还没有被其他的线程修改过,可以将新值写入内存位置

3、如果与期望值不一致,说明已经被其他线程修改过当前内存位置的值,此次CAS操作失败,不执行写入操作。

        CAS 是一种乐观锁的实现方式,它避免了使用传统的互斥锁所带来的上下文切换和线程阻塞开销,更适合在并发量较高的情况下使用。然而,CAS 也存在一些问题,例如ABA问题(即一个值从 A 变成 B,然后又变成 A,但是 CAS 操作无法识别这种变化),需要额外的机制来解决。

cas 存在什么问题?如何解决?

存在的问题主要是ABA问题,什么是ABA问题?

假设有两个线程 A 和 B 同时对一个共享变量进行操作。初始情况下,共享变量的值为 A。然后,线程 A 将共享变量的值从 A 修改为 B,接着再将其修改回 A。此时,线程 B 开始执行,并进行 CAS 操作,期望共享变量的值为 A,实际上也是 A,因此 CAS 操作成功。

在这个例子中,线程 B 无法察觉到共享变量的值其实经历了从 A 到 B 再到 A 的过程,它只看到共享变量的值与期望值相等,所以 CAS 操作成功。但实际上,共享变量的状态已经发生了改变,这可能导致一些意外的结果。例如,线程 B 可能会根据共享变量的值执行一些逻辑判断,如果此时基于 B 值的逻辑判断产生了一些不正确的结果,就会出现问题。

解决 ABA 问题,常用的方法有以下几种:

  1. 版本号或标记:在进行 CAS 操作时,除了判断值是否相等,还可以引入一个版本号或标记。每次修改值时,不仅更新值本身,还更新版本号或标记。这样,即使值从 A 变为 B,再变回 A,但是版本号或标记已经发生变化,CAS 操作可以识别出这种情况,并避免意外修改。

  2. 哈希值检查:类似于版本号或标记,在进行 CAS 操作时,除了比较值本身,还可以比较哈希值。每次修改值时,不仅更新值本身,还更新哈希值。通过比较哈希值,可以更快地检测到值是否发生变化。

  3. 双重 CAS:使用两次 CAS 操作来确认值的变化。首先进行一次 CAS 操作,将期望值更新为新的值。然后再次进行 CAS 操作,检查期望值与实际值是否一致。如果一致,说明没有发生 ABA 问题;如果不一致,则表示值已经发生变化,需要重新尝试 CAS 操作。

  4. 使用带有回退的原子操作:一些编程语言和库提供了带有回退(backoff)机制的原子操作,例如 Java 中的 AtomicStampedReference。这种原子操作不仅可以比较和交换值,还可以比较和交换引用以及版本号。它会在比较失败时暂时放弃修改,并进行一些等待或休眠后再次尝试,减少竞争并提高成功率。

jmm 简单理解

JMM(Java内存模型)是Java虚拟机规范中定义的一种抽象的计算机内存模型,用于屏蔽不同硬件和操作系统的内存访问差异。它定义了变量的可见性、原子性和有序性等行为,确保在多线程环境下的正确性。

JMM 解决了多线程并发执行时可能出现的一些问题,例如指令重排序、内存可见性和原子性问题。它确保多个线程之间对共享变量的操作具有可见性和顺序一致性,避免了数据竞争和不确定的结果。

JMM 使用一些规则和原则来实现这些保证:

  1. 原子性:JMM 通过使用锁、同步块和原子变量等机制,保证对共享变量的读取和写入操作是原子的。即使是在多线程环境下,一个线程的操作不会被其他线程干扰或分割。

  2. 可见性:JMM 使用 volatile 关键字来保证共享变量对多个线程的可见性。当一个线程修改了一个 volatile 变量的值,这个值会立即被其他线程看到,而不会出现缓存不一致的情况。

  3. 顺序一致性:JMM 定义了 happens-before 原则,确保在多线程环境下的操作顺序满足一致性要求。即,如果操作 A happens-before 操作 B,那么操作 A 的结果对操作 B 可见,并且操作 A 和操作 B 之间不会发生重排序。

volatile

什么是volatile?

volatile是Java中的一个关键字,它用于修饰变量。当一个变量被声明为volatile时,它具有以下特性:

  1. 可见性(Visibility):当一个线程修改了volatile变量的值后,其他线程可以立即看到最新的值,而不会使用过期的缓存值。换句话说,volatile保证了变量对所有线程的可见性。

  2. 顺序性(Ordering):在多线程环境下,指令重排是常见的优化手段,可能会改变代码的执行顺序。然而,当一个变量被声明为volatile时,编译器和处理器会对指令重排进行限制,保证volatile变量的读写操作按照程序的顺序执行

volatile常用于以下几种情况:

  1. 作为状态标志:当一个变量用于表示某个状态,多个线程需要根据该状态进行判断和执行不同的逻辑时,可以将该变量声明为volatile。这样可以确保各个线程都能及时感知到状态的变化

  2. 双重检查锁定(Double-Checked Locking):在单例模式等场景下,为了避免多个线程同时创建对象,通常会使用双重检查锁定来实现线程安全的延迟初始化。在此情况下,将共享的实例变量声明为volatile可以确保各个线程看到的初始化状态是一致的

    处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致

volatile和CAS组合起来使用是可以实现锁的效果的,这是为什么?两者缺其一会怎么样?

        volatile和CAS(Compare-And-Swap)结合使用可以实现锁的效果,主要是因为它们分别解决了两个关键问题:内存可见性原子性

  1. 内存可见性:volatile关键字能够确保对被修饰的变量的读写操作在多线程环境下是可见的。当一个线程修改了一个被volatile修饰的变量的值,其他线程能够立即看到这个变化。而普通的变量可能会存在本地缓存、寄存器等地方导致不同线程间的值不一致。在锁的实现中,内存可见性保证了锁状态的正确传递和共享变量的可靠更新。

  2. 原子性:CAS是一种无锁的原子操作,能够实现对某个值的比较和交换。它将当前内存中的值与期望的值进行比较,如果相等,则通过原子方式将新值写入内存;如果不相等,则表示有其他线程已经修改了值,操作失败。CAS的原子性保证了对共享变量的并发访问不会出现竞态条件等问题。

        在锁的实现中,一般会将锁状态作为共享变量来管理。使用volatile修饰锁状态变量可以确保锁状态的变化对其他线程可见,即保证了内存可见性。CAS操作则可以保证对锁状态的原子性更新,即只有一个线程能够成功修改锁状态。

        当一个线程尝试获取锁时,它会使用CAS操作尝试将锁状态从未被持有转为已被持有。如果CAS操作成功,表示获取到了锁;如果失败,表示有其他线程已经持有锁或者正在竞争获取锁,当前线程需要重试。这种基于volatile和CAS的锁实现方式避免了传统锁(如synchronized)中的线程阻塞和唤醒操作,提高了并发性能。

两者缺其一会如何

场景:假设有多个线程同时对一个共享变量进行累加操作,并最终返回累加结果。

  1. 只使用volatile关键字实现:

    • 使用volatile关键字修饰共享变量,确保对该变量的读写操作对其他线程可见。
    • 在每个线程中进行累加操作,每次读取变量的当前值,进行累加,并将结果写回变量。
    • 最后,读取变量的值作为累加结果返回。
  2. 只使用CAS操作实现:

    • 使用AtomicInteger等原子类来代替普通的变量,这些原子类内部使用了CAS操作来实现原子性的更新。
    • 在每个线程中使用CAS操作进行累加操作。首先读取变量的当前值,然后进行累加,并使用CAS操作尝试将新值写回变量。
    • 如果CAS操作成功,则累加成功并返回结果;否则,需要重复读取当前值、累加和CAS操作,直到CAS操作成功为止。
  3. 同时使用volatile和CAS操作实现:

    • 使用volatile关键字修饰共享变量,确保对该变量的读写操作对其他线程可见。
    • 使用AtomicInteger等原子类来代替普通的变量,内部使用CAS操作来实现原子性的更新。
    • 在每个线程中使用CAS操作进行累加操作。首先读取变量的当前值,然后进行累加,并使用CAS操作尝试将新值写回变量。
    • 如果CAS操作成功,则累加成功并返回结果;否则,需要重复读取当前值、累加和CAS操作,直到CAS操作成功为止。

区别比较:

  • 只使用volatile关键字:在多线程环境下,volatile能够保证对共享变量的可见性,但无法确保原子性。因此,在对共享变量进行复合操作时,可能会出现竞态条件和数据不一致的问题。(如果多个线程同时调用increment()方法进行累加操作,虽然每个线程都能正确读取count的最新值,但是在执行count++时,线程之间可能发生交错执行。这意味着两个线程可能同时读取相同的count值(比如都读取为10),然后各自加1得到11,并将结果写回count,导致最终的结果只增加了1,而不是期望的2。)
  • 只使用CAS操作:通过使用CAS操作来实现对共享变量的原子性更新,可以解决竞态条件和数据一致性的问题。但是,CAS操作可能存在ABA问题,即在尝试更新变量时,变量的值已经变化过两次,但是看起来只有一次。这可能导致CAS操作的结果不准确(在ABA问题中,线程A在某个时刻读取到的共享变量x的值为1,然后进行一系列操作后,将x的值改为了2,然后再改回了1。这一过程实际上是将变量的值从1到2再到1的一个循环操作。此时,线程B也读取到共享变量x的值为1,而它并不知道中间发生了一次值的改变。因此,线程B执行CAS操作时,仍然认为共享变量x的值是1,并且更新成功,尽管实际上其他线程对共享变量x的操作已经改变了其值。因此,CAS操作的结果在ABA问题中可能不准确)。
  • 同时使用volatile和CAS操作:这种组合可以兼顾可见性和原子性。volatile确保对共享变量的读写操作对其他线程立即可见,CAS操作保证了原子性的更新。这样就能够避免竞态条件、数据不一致以及ABA问题的发生。

单例模式 线程安全的两种代码 懒汉&饿汉·

懒汉式

//懒汉式单例类.在第一次调用的时候实例化自己 
public class Singleton {
    private Singleton() {}
    private static volatile Singleton single=null;
    //
    public static Singleton getInstance() {
        if (singleton == null) {  
            synchronized (Singleton.class) {  
               if (singleton == null) {  
                  singleton = new Singleton(); 
               }  
            }  
        }  
        return singleton; 
    }
}

饿汉式

//饿汉式单例类.在类初始化时,已经自行实例化 
public class Singleton1 {
    private Singleton1() {}
    private static final Singleton1 single = new Singleton1();
    //静态工厂方法 
    public static Singleton1 getInstance() {
        return single;
    }
}

线程池的7个参数分别是什么作用

  1. corePoolSize(核心线程数):线程池中保持的最小线程数。如果任务数量少于 corePoolSize,即使其他线程处于空闲状态,也会创建新的线程来执行任务,直到达到 corePoolSize。

  2. maximumPoolSize(最大线程数):线程池中允许的最大线程数。当队列已满且当前线程数小于 maximumPoolSize 时,会创建新的线程来执行任务。超过最大线程数的任务将会通过饱和策略进行处理。

  3. keepAliveTime(线程的空闲时间):当线程池中的线程数量大于 corePoolSize 时,如果线程在空闲时间达到 keepAliveTime,则会被回收,直到线程池中的线程数量不大于 corePoolSize。

  4. unit(空闲时间的单位):用于指定 keepAliveTime 的时间单位,例如 TimeUnit.SECONDS。

  5. workQueue(任务队列):用于存储待执行的任务。可以选择不同类型的队列

  6. threadFactory(线程工厂):用于创建新线程的工厂。可以自定义线程工厂来为线程设置特定的名称、优先级、守护状态等属性。

  7. handler(饱和策略):当线程池和任务队列都已满时,新任务的处理策略。

请实现交替打印abc

package com.qcby.demoforspringboot.test;
​
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
​
public class PrintABC {
    public static void main(String[] args) throws InterruptedException {
        Lock lock = new ReentrantLock();
        Condition conditionA = lock.newCondition();
        Condition conditionB = lock.newCondition();
        Condition conditionC = lock.newCondition();
​
        Thread threadA = new Thread(() -> {
            lock.lock();
            try {
                for (int i = 0; i < 10; i++) {
                    System.out.println("A");
                    conditionB.signal();  // 唤醒等待的B线程
                    conditionA.await();   // 当前线程A等待
                }
                conditionB.signal();  // 唤醒最后一个B线程,使其能够正常结束
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });
​
        Thread threadB = new Thread(() -> {
            lock.lock();
            try {
                for (int i = 0; i < 10; i++) {
                    conditionB.await();   // 当前线程B等待
                    System.out.println("B");
                    conditionC.signal();  // 唤醒等待的C线程
                }
                conditionC.signal();  // 唤醒最后一个C线程,使其能够正常结束
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });
​
        Thread threadC = new Thread(() -> {
            lock.lock();
            try {
                for (int i = 0; i < 10; i++) {
                    conditionC.await();   // 当前线程C等待
                    System.out.println("C");
                    conditionA.signal();  // 唤醒等待的A线程
                }
                conditionA.signal();  // 唤醒最后一个A线程,使其能够正常结束
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });
​
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

AQS原理是什么

        AQS(AbstractQueuedSynchronizer抽象队列同步器)是Java中用于实现同步器的抽象基类。它提供了一种多线程访问共享资源的机制,并且可以通过继承和重写它的方法来构建各种类型的同步器,例如ReentrantLock、Semaphore、CountDownLatch等。

        AQS 的核心思想是使用一个FIFO的双向链表(也称为等待队列)来管理线程的获取和释放资源的顺序。在AQS内部,通过一个整型的state表示同步状态,当state大于0时表示资源可用,当state等于0时表示没有可用资源,当state小于0时表示有线程在等待资源。

AQS通过提供以下方法来实现同步操作:

  1. acquire方法:当一个线程需要获取资源时,首先会尝试通过CAS操作修改state的值,如果成功则表示成功获取到资源。如果失败,则线程会被加入到等待队列中,进入阻塞状态。

  2. release方法:当一个线程释放资源时,它会将state的值修改为表示资源可用的状态,并唤醒等待队列中的下一个线程。

  3. tryAcquire和tryRelease方法:通过重写这些方法,可以自定义获取和释放资源的逻辑。这些方法会在acquire和release方法内部调用。

        通过内部的双向链表,AQS实现了一个简单的锁和条件队列机制。在获取资源时,如果失败,则线程会进入等待队列中,等待被唤醒。当某个线程释放了资源时,它会唤醒等待队列中的下一个线程。这种基于等待队列和CAS的实现方式,保证了公平性和线程调度的准确性。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值