当前位置: 首页 > article >正文

Java 并发编程知识点

1. 什么是线程和进程?区别和联系?

进程:程序的一次执行过程。一个 Java 程序的运行一般是 main 线程和多个其他线程同时运行。

线程:比进程更小的执行单位。同类的多个线程共享进程的堆和方法区,但每个线程有自己的程序计数器、虚拟机栈、本地方法栈。

程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。 为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

  • 程序计数器:记录java代码下一条指令的地址,字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制;在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
  • 虚拟机栈:每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
  • 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

2. 堆和方法区是什么?

堆和方法区是所有线程共享的资源。

是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存)。

方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

3. 如何创建线程?

使用new Thread().start()创建线程。

在 Java 代码中使用多线程的方法:继承Thread类、实现Runnable接口、实现Callable接口、使用线程池、使用CompletableFuture类等等。

4. 线程的生命周期和状态有什么?

Java线程在生命周期中有6种状态,随着代码的执行在不同状态中切换。

  1. NEW:初始状态。线程被创建,但没有调用.start()。
  2. RUNNABLE:运行状态。线程被调用了start()开始运行。
  3. WAITING:等待状态。线程需要等待其他线程的特定动作,如通知或中断。
  4. TIME_WAITING:超时等待。线程需要等待一定的时间才能回到RUNNABLE。
  5. BLOCKED:阻塞状态。线程需要等待其他线程释放锁之后才能运行。
  6. TERMINATED:终止状态。线程运行结束

thread.join()表示调用此方法的线程被阻塞,仅当thread完成以后,方法才能继续运行。

5. 什么是线程上下文切换?

线程切换时:线程从CPU状态中退出,需要保存当前线程的上下文(比如程序计数器、虚拟机栈、本地方法栈等),加载即将占用CPU的线程的上下文。

当前线程从CPU状态退出的几种情况:

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

6. Thread.sleep()和Object.wait()的异同点?

同:都可以暂停线程的执行。都是native方法

不同:

  1. sleep()不释放锁,wait()释放锁。
  2. wait只能在同步代码块或同步控制块中使用,而sleep可以在任何位置使用。
  3. sleep()常被用于暂停执行,wait()常被用于线程通信。
  4. 唤醒方法不同。sleep()和wait(long timeout)完成后,线程自动苏醒;而wait()调用后,需要别的线程调用同一个对象上的notify()或notifyAll()方法来唤醒正在等待的线程。
  5. sleep()时Thread类的静态本地方法,wait()是Object类的本地方法。

wait()为什么是定义在Object中而不是Thread中?

每个对象都有对象锁,wait()是希望让获得对象锁的线程等待,会释放当前线程占有的对象锁,是需要操作对应的Object而不是Thread。

sleep()为什么定义在Thread中?

sleep()是希望当前线程暂停执行,不涉及对象。

7. 直接调用Thread类的run方法会怎么样?

线程从创建到运行,是在new之后,执行thread.start()方法之后进入RUNNABLE状态。start()方法进行线程的准备工作,之后自动执行run()方法,这是多线程运行模式。但是如果手动调用thread.run()而不执行start()的话只会把run()方法当作普通方法,而不会以多线程方式执行。

8. 并发与并行、同步和异步?

并发:多个作业在同一时间段执行。并行:多个作业在同一时刻执行。

同步:发出调用后,没有得到结果前,此调用不能返回。异步:发出调用后不用等待返回结果,调用可以直接返回。

9. 为什么需要使用多线程?

减少开销:线程可以比作轻量级的进程,线程是程序执行的最小单位,线程切换和调度成本远低于进程,多核CPU时代多个线程可以同时运行,减少线程上下文开销。

互联网发展的要求:如今的系统很多需要百万级、千万级的并发,多线程并发编程时开发高并发系统的基础,利用多线程机制可以提升系统并发能力和性能。

从计算机底层来讲:

单核时代:多线程可以提高进程利用CPU和IO系统的效率。如果Java进程只有一个线程,当线程被IO阻塞时整个进程都被阻塞,CPU和IO设置只能有一个在运行,系统整体效率低。

多核时代:多线程可以提高进程利用多核CPU的能力。多个线程可以被映射到底层多个CPU核心上执行,提高效率。

10. 单核CPU支持Java多线程吗?

支持。操作系统通过时间片轮转的方式,把CPU时间分配给不同线程,虽然单核CPU一次只能执行一个线程,但是通过多个线程间快速切换,可以让用户感觉多个任务同时进行。

11. 单核CPU上运行多个线程效率一定高吗?

不一定,取决于线程的类型和任务性质,而且都要适度。

对于CPU密集型线程,进行计算和逻辑处理需要占用大量的CPU资源,多个线程运行时会导致频繁的线程切换,增加系统开销。

对于IO密集型线程,主要进行输入输出操作,多个线程同时运行可以利用CPU在等待IO时的空闲时间,提高了效率。

12. 并发编程可能带来的问题?什么是线程安全和不安全?

并发编程目的是提高程序执行效率,但可能会导致内存泄漏、死锁、线程不安全。

线程安全:多线程环境下,同一份数据,不管多少线程同时访问,都能够保证数据安全和一致性。

线程不安全:多线程环境下,同一份数据,多个线程同时访问,数据不一致、错误、丢失等。

13. 什么是线程死锁?Java如何检测死锁?

死锁形成的4个条件:

  1. 互斥条件。一个资源任意时刻只能被一个线程占用。
  2. 请求与保持条件。一个线程请求新的资源阻塞时,对以获取的资源保持不放。
  3. 不可剥夺条件。线程已获取的资源在没有使用完之前不会被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件。多个线程之间形成首尾相接的循环等待资源关系。

Java如何检测死锁?

  • 使用jmap、jstack等查看JVM线程栈和堆内存的情况,死锁发生时jstack输出中通常包括"Found one Java-level deadlock:"字样,后面跟随死锁相关的线程信息。

shell # jps(Java Virtual Machine Process Status Tool)是Java自带的一个工具,它可以列出当前运行的所有Java进程及其相关信息。 jps -l # 找到当前java进程的pid jstack <pid>

  • 实际项目中可以搭配top、df、free等命令查看操作系统信息,出现死锁可能导致CPU、内存资源消耗过高。
  • 采用VisualVM、JConsole等工具排查。

 14. 如何预防和避免死锁?

预防死锁:破坏死锁产生的必要条件。

  • 破坏请求保持条件:一次性申请所有资源。
  • 破坏不可剥夺条件:如何线程申请资源申请不到,就释放持有的资源。
  • 破坏循环等待条件:申请资源时按照一定的线程顺序,释放资源时按照逆序。

避免死锁:资源分配时,借助算法(如银行家算法)对资源分配进行评估,使其进入安全状态。

银行家算法:
思想:当一个线程申请资源时,系统先试探性地分配,判断分配后系统是否处于安全状态,如果安全那么就分配,否则不分配让线程继续等待。
如何判断安全状态?假设线程P1申请资源R1,系统试探性分配后,判断剩余的资源中,能否满足队列中的其他某个线程执行完毕:
  1. 如果能,那么继续试探性的分配,直到所有的线程都能执行完毕,此时处于安全状态,给线程分配资源的顺序即为安全序列。
  2. 如果不能,则系统处于不安全状态。此时没有一个进程能够完成并释放资源,随着时间推移,系统会处于死锁状态。

15. 什么是乐观锁?问题?使用场景?

乐观锁:假设最好的情况,对共享资源的访问都不会产生问题,不用加锁不用等待,只需要在提交修改时验证(版本号机制、CAS算法)资源是否被其他线程修改了。例如AtomicInteger、LongAdder

可能产生的问题:高写入场景下,乐观锁可能导致 验证时 频繁的失败和重试,影响性能。(LongAdder以空间换时间的方式解决了大量失败重试的问题)

适用场景:优点是不存在锁竞争导致的现场阻塞,不会有死锁问题,性能更好,适用于多读场景。但是乐观锁主要针对对象是单个共享变量。

16. 什么是悲观锁?问题?使用场景?

悲观锁:假设最坏的情况,对共享资源的每次访问都可能发生问题,因此每次请求就上锁,一个资源一次只能被一个线程使用,其他线程阻塞。例如synchronized和ReentrantLock

可能带来的问题:

  • 高并发场景下,激烈的锁竞争造成线程阻塞,大量阻塞时操作系统上下文切换会影响系统性能。
  • 可能发生死锁问题,影响代码正常运行。

使用场景:多写场景,竞争激烈时,可以采用悲观锁,从而避免乐观锁频繁失败和重试影响性能。但如果乐观锁用LongAdder解决了频繁失败和重试问题的话,也可以用乐观锁。

17. 乐观锁如何实现的?什么是CAS算法?

版本号法:数据表中添加版本号,每次修改都让版本号加1,读取数据时读取版本号,写入时比较版本号和目前数据库中的数据版本号是否一致,一致则更新,不一致则说明其他线程已经修改了,重试操作。

CAS算法:比较和交换算法。CAS 操作是比较并交换,先比较内存中的值与预期值是否相等,相等则更新为新值。不相等说明有其他线程修改了变量值,需要放弃或重试。

18. java中是如何实现CAS算法的?有什么问题?

  • CAS 操作是比较并交换,先比较内存中的值与预期值是否相等,相等则更新为新值。CAS是一个无锁的原子算法。多个线程同时使用CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。
  • Java中CAS的实现依赖于Unsafe类。sun.misc包下的Unsafe类提供了compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong方法来实现的对Objectintlong类型的 CAS 操作。
  • Unsafe类中的CAS方法是native方法,是通过本地C、C++代码实现而不是java实现的,这些方法直接调用底层硬件指令实现原子操作,也就是说Java中CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI调用 Java Native Interface)。因此, CAS 的具体实现和操作系统以及 CPU 都有关系。
  • 可能的问题?
  • ABA问题:如果变量V的值最初为A,被改为B后再改回来变成A,CAS算法可能误认为变量没改变。解决思路是在变量前面追加上版本号或者时间戳。
  • 循环时间长开销大:CAS 算法在执行失败时,会一直循环尝试,直到成功为止。如果 CAS 操作一直失败,那么循环时间就会很长,从而导致开销增大。可能会导致线程饥饿:如果一个线程一直占用着 CAS 操作,那么其他线程就可能会一直等待,从而导致线程饥饿。
  • 只能保证一个共享变量的原子操作:CAS 算法只能保证一个共享变量的原子操作,如果需要对多个共享变量进行原子操作,那么就需要加锁或者把多个变量封装在一个AtomicReference类(保证引用对象之间的原子性)中。
Unsafe类位于 sun.misc包下,是一个提供低级别、不安全操作的类。由于其强大的功能和潜在的危险性,它通常用于 JVM 内部或一些需要极高性能和底层访问的库中,而不推荐普通开发者在应用程序中使用。

19. 什么是自旋锁?

什么是自旋锁机制?当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。再比如CSA算法因为并发冲突失败时,会不断循环尝试直到成功。

20. volatile关键字的作用和特点?

作用:

  • volatile关键字用于在并发编程中保证变量可见性,被volatile修饰的变量是共享的、不稳定的,每次都在主存中读取。
  • volatile关键字可以防止JVM指令重排序。
  • volatile 关键字不能保证对变量的操作是原子性的。
  • 原子性还得看Synchronized、Lock、原子变量AtomicInteger
  • 双重检验锁方式实现单例模式(线程安全)由于 JVM 具有指令重排的特性,uniqueInstance = new Singleton()执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }
    //只有getInstance是public的
    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                    //1. 为 uniqueInstance 分配内存空间
                    //2. 初始化 uniqueInstance
                    //3. 将 uniqueInstance 指向分配的内存地址
                }
            }
        }
        return uniqueInstance;
    }
}
  • 比如对public volatile static int inc = 0;的inc操作inc++分为①从读取inc的值,inc值加1,inc值写回内存。volatile无法保证这三个操作具有原子性。

改进办法:对操作函数使用synchronized加锁、使用AtomicInteger对象、使用ReentrantLock[riːˈentrənt lɒk]类加锁。

21. synchronized 和 volatile 有什么区别?

互补而非对立。

解决的问题:volatile主要用于解决变量在多个线程间的可见性,synchronized解决多个线程访问资源的同步性。

特性:synchronized可以保证可见性、有序性、原子性,volatile只能实现可见性和有序性。

性能:volatile是synchronized轻量级实现,比synchronized性能好。

修饰对象:volatile只能用于变量,synchronized还可修饰方法和代码块。

22. synchronized关键字

在 Java 中,synchronized 关键字用于实现同步机制,它可以保证在同一时刻只有一个线程可以访问被 synchronized 修饰的代码块或方法。

23. 如何使用 synchronized?

synchronized 关键字的使用方式主要有下面 3 种:

  1. 修饰实例方法 -> 给对象实例上锁
  2. 修饰静态方法 -> 给类上锁
  3. 修饰代码块
  4. synchronized(object) 表示进入同步代码库前要获得 给定对象的锁
  5. synchronized(类名.class) 表示进入同步代码前要获得 给定 Class 的锁
静态  synchronized 方法和非静态  synchronized 方法之间的调用互斥么?不互斥!如果一个线程 A 调用一个实例对象的非静态  synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态  synchronized 方法,是允许的,不会发生互斥现象,因为访问静态  synchronized 方法占用的锁是当前类的锁,而访问非静态  synchronized 方法占用的锁是当前实例对象锁。

24. synchronized能否修饰构造函数?

不能。构造方法本身是线程安全的,如果在构造方法中涉及到共享资源的操作,需要采取同步措施,比如可以用synchronized代码块。

25. synchronized关键字的特性是什么?

synchronized关键字可以保证并发编程的三大特性:原子性、可见性、有序性,而volatile关键字只能保证可见性和有序性,不能保证原子性,也称为是轻量级的synchronized

原子性:一个或多个操作全部执行成功或者全部执行失败。synchronized关键字可以保证只有一个线程拿到锁,访问共享资源。

可见性:当一个线程对共享变量进行修改后,其他线程可以立刻看到。

有序性:程序的执行顺序会按照代码的先后顺序执行。

26. synchronized关键字可以实现什么类型的锁?

悲观锁:synchronized关键字实现的是悲观锁,每次访问共享资源时都会上锁。

非公平锁:synchronized关键字实现的是非公平锁,即线程获取锁的顺序并不一定是按照线程阻塞的顺序。

可重入锁:synchronized关键字实现的是可重入锁,即已经获取锁的线程可以再次获取锁。

独占锁或者排他锁:synchronized关键字实现的是独占锁,即该锁只能被一个线程所持有,其他线程均被阻塞。

27. synchronized的底层原理是什么?

synchronized关键字的底层原理主要依赖监视器锁(Monitor),通过进入和退出Monitor对象。

在Java虚拟机HotSpot中,monitor是由 ObjectMonitor实现的,其源码是用C++语言编写的。 ObjectMonitor数据结构中,包括锁计数器 _count、等待线程数 _waiters、锁重入次数 _recursions、Monitor对象 _object、持有 ObjectMonitor对象的线程 _owner、wait状态的线程列表 _WaitSet、等待锁状态的线程列表 _EntryList

synchronized 修饰代码块

当用 synchronized 修饰代码块时,编译后的字节码会有 monitorentermonitorexit指令,分别对应的是获得锁和解锁。enter和exit两个指令保证了代码是否顺利都能释放锁。

synchronized 修饰方法

当用 synchronized 修饰方法时,会给方法加上ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM在进入方法时会进行锁竞争。

竞争Monitor的实现细节

  1. 当多个线程同时访问一段同步代码时,首先会进入 _EntryList 队列。
  2. (通过CAS的方法)尝试获取Monitor对象,锁的计数器为0表示可以获取到。获取到之后把_owner字段设置为当前线程,进入临界区,Monitor对象的锁计数器加1,_recursions加1 。
  3. 若持有Monitor的线程调用 wait()方法,将释放当前持有的Monitor,_owner变量恢复为null,_count减1,_recursions减1,同时该线程进入 _WaitSet 集合中等待被唤醒。在_WaitSet集合中的线程被唤醒后,再次放到_EntryList 队列中,重新竞争获取锁。
  4. 执行完同步代码块,释放锁,_count减1,_recursions减1,如果_recursions 减到 0,就说明线程需要释放锁了。释放Monitor并复位变量的值,以便其他线程进入获取锁。

可重入锁是根据_recursions 来判断的,重入一次就执行_recursions++,解锁一次就执行_recursions--,如果 _recursions 减到 0 ,就说明需要释放锁了

 


http://www.kler.cn/a/551236.html

相关文章:

  • Avalonia-wpf介绍
  • 汽车迷你Fakra连接器市场报告:未来几年年复合增长率CAGR为21.3%
  • 零基础学QT、C++(二)QT连接数据库
  • [Windows] Win7也能控制安卓手机屏幕(手机镜像投屏):scrcpy
  • 程序人生-Hello’s P2P
  • Spring Cloud之注册中心之Eureka
  • 20.【线性代数】——坐标系中,平行四边形面积=矩阵的行列式
  • Spring Cloud — 微服务容错保护 Hystrix入门
  • 解锁机器学习核心算法 | K-平均:揭开K-平均算法的神秘面纱
  • spring boot知识点2
  • UI自动化测试的优缺点?
  • 如何在 Mac 上下载安装仙剑游戏仙剑世界?可以通过IPA砸壳包安装非常简单
  • js考核第五题
  • iOS 中使用 FFmpeg 进行音视频处理
  • 机器学习_11 线性回归知识点总结
  • Python----数据结构(单链表:节点,是否为空,长度,遍历,添加,删除,查找)
  • mysql 存储空间增大解决方案
  • WordPress Ai插件:支持提示词生成文章和chat智能对话
  • 算法与数据结构(最小栈)
  • 13、《SpringBoot+MyBatis集成(1)——快速入门》