Java基础、多线程、高并发

一、Java基础篇

-1、面向过程与面向对象的区别?

(1)面向过程编程侧重于解决问题的步骤和过程,以一系列的函数或者过程为基础,通过控制流程来达到解决问题的目的。而面向对象编程侧重于将数据和对数据的操作封装在一个对象中,通过对象之间的交互来解决问题。

(2)面向过程中数据和对数据的操作是分开的,函数对数据的操作是通过参数传递的。面向对象中数据和对数据的操作被组织成一个对象,对象内部包含了数据和处理数据的方法。

(3)面向过程的可重用性较低,因为函数是相对独立的,难以复用于其他项目。面向对象的可重用性相对较高,因为对象可以在不同的上下文中重复使用。

(4)面向过程主要以函数为单位,函数是对一系列操作的封装。而面向对象是以类和对象为单位,类定义了对象的属性和方法。

0、面向对象的三大特性?

(1)封装:隐藏部分对象的属性和实现细节,对数据的访问只能通过对外公开的接口。通过这种方式,对象内部数据提供了不同级别的保护,以防止程序中无关部分意外的改变,或错误的使用了对象的私有部分。

(2)继承:让某个类型的对象获得另一个类型对象的属性和方法。继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

1、多态的实现原理?

多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来。

静态绑定与动态绑定:

一种是在编译期确定,被称为静态分派,比如方法的重载;

一种是在运行时确定,被称为动态分派,比如方法的重写和接口的实现。

多态的实现:

多态的实现过程,就是方法调用动态分派的过程,如果子类覆盖了父类的方法,则在多态调用中,动态绑定过程会首先确定实际类型是子类,从而先搜索到子类中的方法。这个过程便是方法覆盖的本质。

Java实现多态有 3 个必要条件:继承、重写和向上转型。只有满足这 3 个条件,开发人员才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而执行不同的行为。

  • 继承:在多态中必须存在有继承关系的子类和父类。

  • 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。

  • 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。

2、静态变量和成员变量的区别?

(1)成员变量存放于堆内存中。静态变量存在于方法区中。

(2)成员变量与对象共存亡,随着对象创建而存在,随着对象被回收而释放。静态变量与类共存亡,随着类的加载而存在,随着类的消失而消失。

(3)成员变量所属于对象,所以也称为实例变量。静态变量所属于类,所以也称为类变量

(4)成员变量能被对象所调用。静态变量以被对象调用,也以被类名调用。

3、是否可以从一个静态方法内部发出对非静态方法的调用?

分为两种情况,发出调用时是否显示创建了对象实例。

(1)没有显示创建对象实例:不可以发起调用,非静态方法只能被对象所调用,静态方法可以通过对象调用,也可以通过类名调用,所以静态方法被调用时,可能还没有创建任何实例对象。因此通过静态方法内部发出对非静态方法的调用,此时可能无法知道非静态方法属于哪个对象。

(2)显示创建对象实例:可以发起调用,在静态方法中显示的创建对象实例,则可以正常的调用。

java静态变量、代码块、和静态方法的执行顺序是什么?

基本上代码块分为三种:Static静态代码块、构造代码块、普通代码块

代码块执行顺序静态代码块——> 构造代码块 ——> 构造函数——> 普通代码块

继承中代码块执行顺序:父类静态块——>子类静态块——>父类代码块——>父类构造器——>子类代码块——>子类构造器

4、==和equals的区别是什么?

==:运算符,用于比较基础类型变量和引用类型变量。对于基础类型变量,比较变量保存的值是否相同,类型不一定要相同。对于引用类型变量,比较的是两个对象的地址是否相同。

equals:Object类中定义的方法,通常用于比较两个对象的值是否相等。

equals在Object方法中其实等同于==,但是在实际的使用中,equals通常被重写用于比较两个对象的值是否相同。

1、区分重载和重写?

重载是参数列表不同,函数名相同,与返回值和参数类型无关。

  • 重载可以在同一个类中或父子类中进行

  • 重载的调用在编译时确定,根据参数类型和个数来决定调用哪个版本

两个方法只有返回值有变化,有没有什么问题(重载)

不可以。两个方法的参数列表完全相同,仅返回类型不同,这将导致编译器无法确定应该调用哪个方法,从而产生编译错误。

 

正确的做法是,通过重载方法的参数列表来区分不同的方法

 

两个方法只变了参数列表的泛型,有没有什么问题(重载)

不可以。泛型在编译时会进行类型擦除,会被擦除为Object,所以这两个方法其实都是同一个方法,所以这个时候我们应该避免重载。如果返回值为泛型,统一不能进行编译,所以我们应该避免用泛型参数进行重载。

重写是函数体不同,函数名相同,形参列表相同,返回值相同。

  • 重写发生在子类中重写父类的方法

  • 重写的调用在运行时确定,根据对象的实际类型来决定调用哪个版本

  • 重写方法不能比父类中的方法具有更严格的访问权限

5、深拷贝和浅拷贝区别是什么?

深拷贝:对于基础数据类型,直接复制数据值;对于引用数据类型,开辟新的内存空间,在新的内存空间里复制一个一模一样的对象,新老对象不共享内存,修改其中一个对象的值,不会影响另一个对象。

浅拷贝:对于基础数据类型,直接复制数据值;对于引用数据类型,只是复制了对象的引用地址,新旧对象指向同一个内存地址,修改其中一个对象的值,另一个对象的值随之改变。

深拷贝相比于浅拷贝速度较慢并且花销较大。

数据分为基本数据类型和引用数据类型。基本数据类型:数据直接存储在栈中。引用数据类型:存储在栈中的是对象的引用地址,真实的对象数据存放在堆内存里。

6、HashMap底层,为什么要使用红黑树?

在JDK1.7 和JDK1.8 中有所差别:

在JDK1.7 中,由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。

在jdk1.8版本后,底层是由“数组+链表+红黑树”组成。java对HashMap做了改进,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度。

为了加快检索速率。红黑树虽然本质上是一颗二叉树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。

7、在什么时候使用链表?什么时候使用红黑树?

对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在新增后超过8个(阈值8),如果此时数组长度大于等于64,则会触发链表节点转红黑树节点,而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。

对于移除,当同一个索引位置的节点在移除后达到6个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点。

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。

因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。

8、HashMap的默认初始容量?HashMap的容量有什么限制?

默认初始容量是16。HashMap的容量必须是2的N次方,HashMap会根据传入的容量计算一个大于等于该容量的最小的2的N次方,例如传9,容量为16。

9、HashMap的插入流程是怎么样的?

(1)首先,通过键的hashCode()方法计算出键的哈希值。哈希值是一个整数,用于确定该键值对在哈希表中的位置。

(2)确定存储位置。使用哈希值和哈希表的容量(数组的长度)来计算出该键值对在数组中的索引位置。通常会使用 (hash & (capacity - 1)) 的位运算来计算位置,其中 hash 是键的哈希值,capacity 是数组的长度。

(3)处理哈希冲突。如果发生了哈希冲突(即不同的键计算出了相同的位置),则会使用链表或红黑树来解决冲突。在JDK 8及之前的版本中,使用链表来存储具有相同哈希值的键值对,称为链地址法。从JDK 8开始,当链表的长度超过一定阈值(默认为8),链表会转化为红黑树,以提高查找效率。

(4)插入键值对。如果该位置没有元素,直接将键值对插入到该位置。如果该位置已经存在元素,会根据键的哈希值进行比较:如果哈希值相等且键相等(调用 equals() 方法返回 true),则更新对应的值。否则,将该键值对插入到链表或红黑树的末尾。

10、HashMap的扩容流程是怎么样的?

(1)当HashMap中的元素数量达到容量乘以加载因子时,就会触发扩容操作。加载因子默认为0.75,可以通过构造方法设置。

(2)当需要扩容时,HashMap 会创建一个新的数组,其大小为原数组的两倍。

(3)对于原数组中的每个非空位置(即存储了键值对的位置),会重新计算它们在新数组中的位置。重新计算的方法是通过对原位置的哈希值取模新数组长度。

(4)对于每个原数组的位置,如果它的链表长度小于等于阈值(默认为8),则直接将链表中的键值对按照顺序放入新数组的相应位置。如果链表长度超过阈值,会将该链表转化为红黑树,然后再将红黑树的节点按照顺序放入新数组的相应位置。

(5)当新数组中的所有元素都被放入新的位置后,新数组取代了原来的数组,成为新的存储结构。

(6)扩容操作完成后,原数组会被垃圾回收。

11、除了HashMap,还用过哪些Map,在使用时怎么选择?

img

12、HashMap和Hashtable的区别?

(1)HashMap允许key和value为null,Hashtable不允许。

(2)HashMap的默认初始容量为16,Hashtable 为11。

(3)HashMap的扩容为原来的2倍,Hashtable的扩容为原来的2倍加1。

(4)HashMap是非线程安全的,Hashtable是线程安全的。

(5)HashMap的hash值重新计算过,Hashtable直接使用hashCode。

(6)HashMap去掉了Hashtable中的contains方法。

(7)HashMap继承自AbstractMap类,Hashtable继承自Dictionary类。

13、hashcode是干什么用的?可以不重写hashcode方法吗?会出现什么问题?

hashcode是Java中的一个方法,它用于计算对象的哈希码。在 Java中,hashcode方法被用于哈希集合(如 HashSet)、哈希映射(如 HashMap)等数据结构中,以及在对象比较中用于提高性能。

如果不重写hashcode方法,而只重写了equals方法,可能会导致以下问题:

(1)在哈希集合中无法准确查找: 如果对象的equals方法判断为相等,但哈希码不同,那么在哈希集合中查找对象时可能无法找到对应的项。

(2)哈希映射中的问题: 在哈希映射中,相同的键应该具有相同的哈希码,否则可能导致无法正确地查找或删除映射项。

(3)不稳定的哈希集合和映射行为: 如果对象被修改后导致哈希码发生变化,它可能无法正确地从哈希集合或映射中移除或查找。

14、Java中的初始化?

(1)在类的内部,变量定义的先后顺序决定了初始化顺序。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造方法)被调用之前得到初始化。 (2)无论创建多少个对象,静态数据都只占用一份存储区域。static关键字不能应用于局部变量,因此它只能作用于域。如果一个域是静态的基本类型域,且也没有对它进行初始化,那么它就会获得基本类型的标准初值;如果它是一个对象引用,那么它的默认初始化值就是null。 (3)静态初始化只有在必要时刻才进行,例如:类里面的静态变量,只有当类被调用时才会初始化,并且静态变量不会再次被初始化,即静态变量只会初始化一次。 (4)初始化的顺序是先静态对象,然后是非静态对象。 (5)当有父类时,完整的初始化顺序为:父类静态变量(静态代码块)->子类静态变量(静态代码块)->父类非静态变量(非静态代码块)->父类构造器 ->子类非静态变量(非静态代码块)->子类构造器 。 (6)即使没有显示的使用static关键字,构造器实际上也是静态方法。

15、String、StringBuilder、StringBuffer的区别?

String:String的值被创建后不能修改,任何对String的修改都会引发新的String对象的生成。

StringBuffer:跟String类似,但是值可以被修改,使用synchronized来保证线程安全。

StringBuilder:StringBuffer的非线程安全版本,没有使用synchronized,具有更高的性能,推荐优先使用。

16、什么是反射?

反射是指在运行状态中,对于任意一个类都能够知道这个类的所有的属性和方法,并且对于任意一个对象,都能够调用它的任意一个方法,这种动态获取信息以及动态调用对象方法的功能称为反射机制。

17、重载和重写的区别?

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。

重载:一个类中有多个同名的方法,但是具有有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)。

重写:发生在子类与父类之间,子类对父类的方法进行重写,参数都不能改变,返回值类型可以不相同,但是必须是父类返回值的派生类。

18、为什么不能根据返回类型来区分重载?

方法的返回值只是作为方法运行之后的一个“状态”,但是并不是所有调用都关注返回值,所以不能将返回值作为重载的唯一区分条件。

19、抽象类和接口有什么区别?

抽象类只能单继承,接口可以多实现。

抽象类可以有构造方法,接口中不能有构造方法。

抽象类中可以有成员变量,接口中没有成员变量,只能有常量(默认就是 public static final)。

抽象类中可以包含非抽象的方法,在Java 7之前接口中的所有方法都是抽象的,在Java 8之后,接口支持非抽象方法:default 方法、静态方法等。Java 9支持私有方法、私有静态方法。

抽象类中的方法类型可以是任意修饰符,Java 8之前接口中的方法只能是public 类型,Java 9支持private类型。

设计思想的区别:

接口是自上而下的抽象过程,接口规范了某些行为,是对某一行为的抽象。我需要这个行为,我就去实现某个接口,但是具体这个行为怎么实现,完全由自己决定。

抽象类是自下而上的抽象过程,抽象类提供了通用实现,是对某一类事物的抽象。我们在写实现类的时候,发现某些实现类具有几乎相同的实现,因此我们将这些相同的实现抽取出来成为抽象类,然后如果有一些差异点,则可以提供抽象方法来支持自定义实现。

20、Error和Exception有什么区别?

Error和Exception都是Throwable的子类,用于表示程序出现了不正常的情况。区别在于:

(1)Error表示系统级的错误和程序不必处理的异常,是恢复不可能但很困难的情况下的一种严重问题,比如内存溢出,不可能指望程序能处理这样的情况。

(2)Exception表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题,也就是说,它表示如果程序运行正常,从不会发生的情况。

21、Java中的final关键字有哪些用法?

(1)修饰类:该类不能再派生出新的子类,不能作为父类被继承。因此,一个类不能同时被声明为abstract和final。

(2)修饰方法:该方法不能被子类重写。

(3)修饰变量:该变量必须在声明时给定初值,而在以后只能读取,不可修改。 如果变量是对象,则指的是引用不可修改,但是对象的属性还是可以修改的。

22、阐述final、finally、finalize的区别。

finally是对Java异常处理机制的最佳补充,通常配合try、catch使用,用于存放那些无论是否出现异常都一定会执行的代码。在实际使用中,通常用于释放锁、数据库连接等资源,把资源释放方法放到finally中,可以大大降低程序出错的几率。

finalize:Object中的方法,在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。在Java 9中该方法已经被标记为废弃,并添加了新的java.lang.ref.Cleaner,提供了更灵活和有效的方法来释放资源。

23、JDK1.8之后有哪些新特性?

(1)接口默认方法:Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用default关键字即可。

(2)Lambda表达式和函数式接口:Lambda表达式本质上是一段匿名内部类,也可以是一段可以传递的代码。Lambda允许把函数作为一个方法的参数,使用 Lambda 表达式使代码更加简洁,但是也不要滥用,否则会有可读性等问题。

(3)方法引用:可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

(4)日期时间API:Java 8 引入了新的日期时间API改进了日期时间的管理。

(5)Stream API:用函数式编程方式在集合类上进行复杂操作的工具,配合Lambda表达式可以方便的对集合进行处理。使用Stream API对集合数据进行操作,就类似于使用SQL执行的数据库查询。也可以使用Stream API来并行执行操作。

24、为什么Java中只有值传递?

值传递:在调用函数时,将实参拷贝一份赋值给函数的形参,对形参进行操作;

引用传递:在函数调用时,将实参传递给函数,直接对实参进行操作。

Java方法传参,都是对所传变量进行拷贝,对基本数据类型来讲,拷贝的是实际数值,对引用数据类型来讲拷贝的是引用地址。

Java中不存在函数对实参的操作,全部是对经过拷贝的形参的操作,也就是说Java中只存在值传递,不存在引用传递。

25、List、Set、Map三者的区别?

(1)List: List 接口存储一组不唯一(可以有多个元素引用相同的对象)、有序的对象。

(2)Set:不允许重复的集合,不会有多个元素引用相同的对象。

(3)Map:使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。

26、ArrayList和LinkedList的区别?

ArrayList底层基于动态数组实现,LinkedList底层基于链表实现。

对于按index索引数据(get/set方法):ArrayList通过index直接定位到数组对应位置的节点,而LinkedList需要从头结点或尾节点开始遍历,直到寻找到目标节点,因此在效率上ArrayList优于LinkedList。

对于随机插入和删除:ArrayList需要移动目标节点后面的节点(使用System.arraycopy方法移动节点),而LinkedList只需修改目标节点前后节点的 next或prev属性即可,因此在效率上LinkedList优于ArrayList。

对于顺序插入和删除:由于ArrayList不需要移动节点,因此在效率上比LinkedList 更好。

27、ArrayList和Vector的区别?

Vector和ArrayList几乎一致,唯一的区别是Vector在方法上使用了synchronized来保证线程安全,因此在性能上ArrayList具有更好的表现。

28、ConcurrentHashMap?

一种线程安全的高效Map集合,它是【线程安全】的哈希表。它是通过“分段锁”来实现多线程下的安全问题。

  • 底层数据结构

    • jdk1.7采用分段数组+链表实现

    • jdk1.8采用数组+链表/红黑树

  • 加锁的方式

    • jdk1.7采用segment分段锁,底层使用的是ReentrantLock

    • Jdk1.8采用CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点。

29、Volatile和Synchronized的区别?

volatile关键字详解:是Java语言提供的一种稍弱的同步机制,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译时与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

(1)作用的位置不同。 synchronized是修饰方法,代码块。volatile是修饰变量。

(2)作用不同。 synchronized,可以保证变量修改的可见性及原子性,可能会造成线程的阻塞;synchronized在锁释放的时候会将数据写入主内存,保证可见性。 volatile仅能实现变量修改的可见性,但无法保证原子性,不会造成线程的阻塞;volatile修饰变量后,每次读取都是去主内存进行读取,保证可见性。

30、设计模式?

(1)单例模式

某个类只能生成一个实例,该实例全局访问,例如Spring容器里一级缓存里的单例池。

优点: (1)唯一访问:如生成唯一序列化的场景、或者spring默认的bean类型。(1)提高性能:频繁实例化创建销毁或者耗时耗资源的场景,如连接池、线程池。

缺点: 不适合有状态且需变更的。

实现方式:

(1)饿汉式:线程安全速度快 。

(2)懒汉式:双重检测锁,第一次减少锁的开销、第二次防止重复、volatile防止重排序导致实例化未完成 。

(3)静态内部类:线程安全利用率高 。

(4)枚举:effictive JAVA推荐,反射也无法破坏。

(2)工厂模式

定义一个用于创建产品的接口,由子类决定生产何种产品。

优点:解耦,提供参数即可获取产品,通过配置文件可以不修改代码增加具体产品。

缺点:每增加一个产品就得新增一个产品类。

(3)抽象工厂模式

提供一个接口,用于创建相关或者依赖对象的家族,并由此进行约束。

优点:可以在类的内部对产品族进行约束。

缺点:假如产品族中需要增加一个新的产品,则几乎所有的工厂类都需要进行修改。

(4)原型模式

将一个对象作为原型,采用clone()方法来创建新的实例。

31、JUC知道哪些?

JUC是Java标准库中的一个包。

(1)ReentrantLock可重入锁。

(2)Executors,提供了一些常用的线程池工厂方法。

(3)ThreadPoolExecutor,线程池的实现类。

(4)ConcurrentHashMap,线程安全的哈希表实现。

(5)CountDownLatch,一个同步辅助类,用于等待一组线程完成后再执行。

32、Java的集合有哪些?

ArrayList、LinkedList、HashMap、TreeMap、HashTable、Vector、HashSet、LinkedHashSet、TreeSet、Queue、Stack等。

二、多线程

1、线程池中有几种状态,分别是如何变化的?

多线程:多个线程并发执行。

(1)RUNNING:会接收新任务并且会处理队列中的任务。

(2)SHUTDOWN:不会接受新任务但是会处理队列中的任务,任务处理完后会中断所有线程。

(3)STOP:不会接收新任务并且不会处理队列中的任务,会直接中断所有线程。

(4)TIDYING:所有线程都停止了之后,线程池的状态就会转为TIDYING,一旦达到此状态,就会调用线程池的terminated()。

(5)TERMINATED:terminated()执行完之后就会转变为TERMINATED。

这五种状态并不能任意转换,只会有以下几种转换情况:

(1)RUNNING—>SHUTDOWN:手动调用shutdown()触发,或者线程池对象GC时会调用finalize()从而调用shutdown()。

(2)RUNNING—>STOP:手动调用shutdownNow()触发。

(3)SHUTDOWN—>STOP:手动先调用shutdown()紧着调用shutdownNow()触发。

(4)SHUTDOWN—>TIDYING:线程池所有线程都停止后自动触发。

(5)STOP—>TIDYING:线程池所有线程都停止后自动触发。

(6)TIDYING—>TERMINATED:线程池自动调用terminated()后触发。

2、线程的生命周期,线程有哪些状态?

 

线程通常有五种状态:创建、就绪、运行、阻塞和死亡。

(1)新建状态(New):新创建了一个线程对象。

(2)就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

(3)运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

(4)阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

(5)死亡状态(Dead):线程执行完了或者因异常退出了run方法, 该线程结束生命周期。

阻塞的情况又分为三种:

(1)等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是Object类的方法。

(2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。

(3)其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法。

3、线程的状态流转?

(1)NEW:新建但是尚未启动的线程处于此状态,没有调用 start() 方法。

(2)RUNNABLE:包含就绪(READY)和运行中(RUNNING)两种状态。线程调用start()方法会进入就绪(READY)状态,等待获取CPU时间片。如果成功获取到CPU时间片,则会进入运行中(RUNNING)状态。

(3)BLOCKED:线程在进入同步方法/同步块(synchronized)时被阻塞,等待同步锁的线程处于此状态。

(4)WAITING:无限期等待另一个线程执行特定操作的线程处于此状态,需要被显示的唤醒,否则会一直等待下去。例如对于Object.wait(),需要等待另一个线程执行Object.notify()或Object.notifyAll();对于Thread.join(),则需要等待指定的线程终止。

(5)TIMED_WAITING:在指定的时间内等待另一个线程执行某项操作的线程处于此状态。跟WAITING类似,区别在于该状态有超时时间参数,在超时时间到了后会自动唤醒,避免了无期限的等待。

(6)TERMINATED:执行完毕已经退出的线程处于此状态。

线程在给定的时间点只能处于一种状态。这些状态是虚拟机状态,不反映任何操作系统线程状态。

4、如何优雅的停止一个线程?

可以使用stop()方法,但是stop()方法太粗暴了,一旦调用了stop(),就会直接停掉线程,这样就可能造成严重的问题,比如任务执行到哪一步了?该释放的锁释放了没有?都存在疑问。

stop()会释放线程占用的synchronized锁,而不会自动释放ReentrantLock锁。

可以使用中断来停止线程。

比如,可以通过控制变量在大于某一个数时停止,不然就算中断了也不会停止。此外,线程池中也是通过interrupt()来停止线程的。

5、线程池的核心线程数、最大线程数该如何设置?

对于CPU密集型任务,线程数最好就等于CPU核心数,但是为了应对线程执行过程发生缺页中断或其他异常导致线程阻塞的请求,可以额外在多设置一个线程,这样当某个线程暂时不需要CPU时,可以有替补线程来继续利用CPU。所以,对于CPU密集型任务,可以设置线程数为:CPU核心数+1

对于IO密集型任务,线程在执行IO型任务时,可能大部分时间都阻塞在IO中。假设现在有10个CPU,如果我们只设置了10个线程来执行IO型任务,那么很有可能这10个线程都阻塞在了IO上,这样这10个CPU就都没活干了,所以,对于IO型任务,通常会设置线程数为: 2*CPU核心数

但是这不一定是最佳的,比如,有10个CPU,线程数为20,那么就有可能这20个线程同时阻塞在了IO上,所以可以再增加线程,从而去压榨CPU的利用率。

通常,如果IO型任务执行的时间越长,那么同时阻塞在IO上的线程就可能越多,这样就可以设置更多的线程,但是,线程肯定不是越多越好,可以采用公式计算,公式为:线程数=CPU核心数*(1+线程等待时间/线程运行总时间)。

线程等待时间:指的就是线程没有使用CPU的时间,比如阻塞在IO。

线程运行总时间:指的是线程执行完某个任务的总时间。

可以利用jvisualvm抽样来估计这两个时间。

6、用过什么线程池,核心线程数量和最大线程数量一致会发生什么情况?

最常用的就是ThreadPoolExecutor类。

当核心线程数量和最大线程数量设置为一致时,线程池不会创建新的线程,除非现有的线程因超时或异常退出而被移除。这种情况下,线程池将一直维持固定数量的线程,不会根据任务负载的变化动态调整线程数。

(1)资源浪费: 如果任务提交速度很快,线程池中的线程可能会一直保持在最大线程数,导致资源浪费,降低系统性能。

(2)无法应对突发流量: 如果在短时间内出现大量任务提交,而线程池的大小一直保持不变,可能无法及时处理所有任务,导致响应时间增加。

7、简述线程池处理流程?

首先判断核心线程是否已满,如果没有满则创建核心线程执行,如果已经满了就判断任务队列是都已满,如果没有满则将任务放入到任务队列中,如果满了就判断最大线程数是否到达,如果没有到达则创建临时线程执行,如果已经达到则根据拒绝策略处理任务。最后,如果核心或临时线程执行完成任务后会检查任务队列中是否有需要执行的线程,如果有,则使用非核心线程执行任务。

8、为什么要用线程池?优缺点?解释下线程池参数?

如果我们在方法中直接new一个线程来处理,当这个方法被调用频繁时就会创建很多线程,会消耗系统资源,降低系统的稳定性。

(1)降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。

(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

(3)提高线程的可管理性。线程是稀缺资源,使用线程池可以统一分配,调优和监控。

线程池的缺点:

(1)可能引发死锁问题。

(2)任务分配可能不均匀:某些线程可能会处理更多的任务,而其他线程可能会空闲。

(3)资源消耗:线程池本身也需要占用一定的内存和CPU资源。

线程池参数介绍:

(1)核心线程数:当线程池运行的线程少于corePoolSize时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态。

(2)最大线程数:线程池允许开启的最大线程数。

(3)保持存活时间:如果线程池当前线程数超过 corePoolSize,则多余的线程空闲时间超过keepAliveTime时会被终止。

(4)任务队列:用于保留任务并移交给工作线程的阻塞队列。

(5)线程工厂:用来生产线程执行任务。可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然也可以选择自定义线程工厂,一般会根据业务来制定不同的线程工厂。

(6)任务拒绝策略:往线程池添加任务时,会在两种情况下触发拒绝策略:1)线程池运行状态不是 RUNNING;2)线程池已经达到最大线程数,并且阻塞队列已满时。

9、线程池有哪些拒绝策略?

(1)AbortPolicy:中止策略。默认的拒绝策略,直接抛出RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。

(2)DiscardPolicy:抛弃策略。什么都不做,直接抛弃被拒绝的任务。

(3)DiscardOldestPolicy:抛弃最老策略。抛弃阻塞队列中最老的任务,相当于就是队列中下一个将要被执行的任务,然后重新提交被拒绝的任务。如果阻塞队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用。

(4)CallerRunsPolicy:调用者运行策略。在调用者线程中执行该任务,该策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者,由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理完正在执行的任务。

10、线程池有哪些队列?

(1)ArrayBlockingQueue:基于数组结构的有界阻塞队列,按先进先出对元素进行排序。

(2)LinkedBlockingQueue:基于链表结构的有界/无界阻塞队列,按先进先出对元素进行排序,吞吐量通常高于 ArrayBlockingQueue。Executors.newFixedThreadPool使用了该队列。

(3)SynchronousQueue:不是一个真正的队列,而是一种在线程之间移交的机制。要将一个元素放入 SynchronousQueue中,必须有另一个线程正在等待接受这个元素。如果没有线程等待,并且线程池的当前大小小于最大值,那么线程池将创建一个线程,否则根据拒绝策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是被放在队列中,然后由工作线程从队列中提取任务。只有当线程池是无界的或者可以拒绝任务时,该队列才有实际价值。Executors.newCachedThreadPool使用了该队列。

(4)PriorityBlockingQueue:具有优先级的无界队列,按优先级对元素进行排序。元素的优先级是通过自然顺序或 Comparator 来定义的。

11、使用队列有什么需要注意的吗?

使用有界队列时,需要注意线程池满了后,被拒绝的任务如何处理。

使用无界队列时,需要注意如果任务的提交速度大于线程池的处理速度,可能会导致内存溢出。

12、线程池中阻塞队列的作用?为什么是先添加队列而不是先创建最大线程?

一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源。

在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。

13、线程只能在任务到达时才启动吗?

默认情况下,即使是核心线程也只能在新任务到达时才创建和启动。但是可以使用 prestartCoreThread(启动一个核心线程)或 prestartAllCoreThreads(启动全部核心线程)方法来提前启动核心线程。

14、核心线程怎么实现一直存活?

核心线程在获取任务时,通过阻塞队列的take()方法实现一直阻塞(存活)。

15、非核心线程如何实现在keepAliveTime后死亡?

利用阻塞队列的方法,在获取任务时通过阻塞队列的poll(time,unit) 方法实现延迟死亡。

16、非核心线程能成为核心线程吗?

线程池内部是不区分核心线程和非核心线程的。只是根据当前线程池的工作线程数来进行调整。

17、如何终止线程池?

(1)shutdown:“温柔”的关闭线程池。不接受新任务,但是在关闭前会将之前提交的任务处理完毕。

(2)shutdownNow:“粗暴”的关闭线程池,也就是直接关闭线程池,通过Thread#interrupt() 方法终止所有线程,不会等待之前提交的任务执行完毕。但是会返回队列中未处理的任务。

18、有哪些线程池?

(1)newFixedThreadPool:固定线程数的线程池。corePoolSize = maximumPoolSize,keepAliveTime为0,工作队列使用无界的LinkedBlockingQueue。适用于为了满足资源管理的需求,而需要限制当前线程数量的场景,适用于负载比较重的服务器。

(2)newSingleThreadExecutor:只有一个线程的线程池。corePoolSize = maximumPoolSize = 1,keepAliveTime为0, 工作队列使用无界的LinkedBlockingQueue。适用于需要保证顺序的执行各个任务的场景。

(3)newCachedThreadPool:按需创建新线程的线程池。核心线程数为0,最大线程数为 Integer.MAX_VALUE,keepAliveTime为60秒,工作队列使用同步移交 SynchronousQueue。该线程池可以无限扩展,当需求增加时,可以添加新的线程,而当需求降低时会自动回收空闲线程。适用于执行很多的短期异步任务,或者是负载较轻的服务器。

(4)newScheduledThreadPool:创建一个以延迟或定时的方式来执行任务的线程池,工作队列为 DelayedWorkQueue。适用于需要多个后台线程执行周期任务。

(5)newWorkStealingPool:用于创建一个可以窃取的线程池,底层使用 ForkJoinPool 实现。

19、线程池里有个ctl,是如何设计的?

ctl是一个打包两个概念字段的原子整数。

(1)workerCount:指示线程的有效数量;

(2)runState:指示线程池的运行状态,有RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED等状态。

int类型有32位,其中ctl的低29为用于表示workerCount,高3位用于表示runState。

20、ctl为什么这么设计?有什么好处吗?

ctl这么设计的好处是将runState和workerCount的操作封装成一个原子操作。

runState和workerCount是线程池正常运转中的2个最重要属性,线程池在某一时刻该做什么操作,取决于这2个属性的值。

因此无论是查询还是修改,都必须保证对这2个属性的操作是属于“同一时刻”的,也就是原子操作,否则就会出现错乱的情况。如果使用2个变量来分别存储,要保证原子性则需要额外进行加锁操作,这显然会带来额外的开销,而将这2个变量封装成1个 AtomicInteger 则不会带来额外的加锁开销,而且只需使用简单的位操作就能分别得到 runState 和 workerCount。

21、线程池中线程的复用原理?

在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start()来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的run方法,将run方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的run方法串联起来。

22、说说你对守护线程的理解?

守护线程:为所有非守护线程提供服务的线程,任何一个守护线程都是整个JVM中所有非守护线程的保姆。

守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;哪天其他线程结束了,没有要执行的了,程序就结束了,不会管守护线程,直接把它中断。

注意:由于守护线程的终止是自身无法控制的,因此千万不要把IO、File等重要操作逻辑分配给它;因为他不靠谱。

守护线程的作用是什么?

比如,GC垃圾回收线程:当程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

应用场景:(1)来为其它线程提供服务支持的情况。(2)或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要正确地关掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都是些关键的事务,比如,数据库录入或者更新,这些操作都是不能中断的。

用法:thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。不能把正在运行的常规线程设置为守护线程。

在守护线程中产生的新线程也是守护的。

守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作的中间发生中断。

Java自带的多线程框架,比如ExecutorService,会将守护线程转换为用户线程,所以如果要使用守护线程就不能用Java的线程池。

23、说说你对线程安全的理解?

指的不是线程安全,应该是内存安全,因为在JVM中堆是共享内存,可以被所有线程访问。

当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,就说这个对象是线程安全的。

堆是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。

在Java中,堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈相互独立。因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里面显式的分配和释放。

目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程,这是由操作系统保障的。

在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。

24、线程池中提交一个任务的流程是怎么样的?

(1)使用execute()方法提交一个Runnable对象;

(2)会先判断当前线程池中的线程数是否小于corePoolSize;

(3)如果小于,则创建新线程并执行Runnable对象;

(4)如果大于等于,则尝试将Runnable对象加入到workQueue中;

(5)如果workQueue没满,则将Runnable对象正常入队,等待执行;

(6)如果workQueue满了,则会入队失败,那么会去尝试继续增加线程;

(7)如果当前线程池中的线程数是否小于maximumPoolSize;

(8)如果小于,则创建新线程并执行任务;

(9)如果大于等于,则执行拒绝策略,拒绝此Runnable对象。

25、wait()和sleep()的区别?

sleep是Thread类的静态本地方法,wait是Object类的本地方法。

(1)对于同步锁的影响不同:如果当前线程持有同步锁,那么sleep是不会让线程释放同步锁的。wait()会释放同步锁,让其他线程进入synchronized代码块执行。

(2)使用范围不同:sleep()可以在任何地方使用。wait()只能在同步控制方法或者同步控制块里面使用,否则会抛IllegalMonitorStateException。

(3)恢复方式不同:两者会暂停当前线程,但是在恢复上不太一样。sleep() 在时间到了之后会重新恢复;wait() 则需要其他线程调用同一对象的 notify()/ nofityAll()才能重新恢复。

26、线程的sleep()方法和yield()方法有什么区别?

线程执行sleep()方法后进入超时等待状态,而执行 yield() 方法后进入就绪状态。

sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会。

yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行。

27、线程的join()方法是干啥的?

join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队列,直到线程A结束或中断线程。

28、Thread调用start()方法和调用run()方法的区别?

run():普通的方法调用,在主线程中执行,不会新建一个线程来执行。

start():新启动一个线程,这时此线程处于就绪状态,并没有运行,一旦得到CPU时间片,就开始执行run()方法。

29、如何理解Java并发中的可见性?

Java并发可见性指的是多线程并发访问共享变量时,对变量的更改能够被其他线程及时感知, 即在一个线程修改变量后,其他线程能够立即看到这个变量的修改结果。

在Java中,可以使用volatile关键字来保证变量的可见性,对于加了volatile的变量,线程在读取该变量时会直接从内存中读取,再修改该变量时会同时修改CPU高速缓存和内存中的值。

30、如何理解Java并发中的原子性?

Java并发原子性指的是在多线程并发的情况下,一段代码或操作要么完全执行成功,要么完全不执行,不出现执行一半被其他线程打断或干扰的情况。换句话说,就是对同一变量的多个操作能够像原子操作一样,保证多线程环境下的数据一致性,避免出现数据竞争和脏数据等问题。

由于CPU、内存、IO(磁盘、网络)之间的性能差距,为了能充分利用CPU,当线程执行IO操作时,线程会让出CPU,使得CPU去执行其他线程的指令,并且本身来说,为了达到线程并发执行的效果,CPU也会固定时间片来切换执行不同线程。

Java中需要通过各种锁机制来保证原子性。

31、如何理解Java并发中的有序性?

Java并发有序性指的是多个线程执行的指令和操作,按照开发者编写程序的顺序或者预定的顺序进行执行。多线程并发执行时,可能会发生指令的重排,导致程序的执行顺序与预期不一致,从而出现数据竞争和线程安全问题。

编译器有时为了进行编译优化,会进行指令重排序,比如:

new Person()

这行代码分三步:

(1)申请内存空间;

(2)在内存空间初始化Person对象相关的内容;

(3)返回内存空间地址。

但是编译有可能会优化为:

(1)申请内存空间;

(2)返回内存空间地址。

(3)在内存空间初始化Person对象相关的内容;

可以通过锁机制或者volatile来保证有序性。

32、多线程中有哪些锁?怎么用?

(1)synchronized锁,通过synchronized关键字可以对代码块或方法进行加锁,保证同一时刻只有一个线程可以访问被锁定的代码段。

(2)可重入锁,ReentrantLock是java.util.concurrent.locks包中提供的锁实现。它允许线程在同一时刻多次获得同一个锁,是一个可重入锁。

(3)读写锁ReentrantReadWriteLock,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。它包括一个读锁和一个写锁。

(4)StampedLock是Java 8引入的新锁机制,提供了乐观锁和悲观锁两种模式。它比ReentrantReadWriteLock 更加灵活,但也更加复杂。

33、乐观锁?

(1)基本思想:乐观锁假设在绝大多数情况下,共享资源不会发生冲突,因此允许多个线程同时访问和修改共享资源,但在更新时会先检查版本信息,以确保数据的一致性。

(2)实现方式:通常通过给共享资源添加一个版本号(或者时间戳)来实现,每次更新时,都会比较版本号是否一致,如果一致则执行更新操作,否则拒绝更新。

(3)应用场景:适用于读取频繁、写入少量的场景,以避免加锁导致的性能损耗。适用于冲突较少的情况,例如读取某个数据的同时,可能只有极小的概率会有其他线程修改该数据。

34、悲观锁?

(1)基本思想:悲观锁假设在大多数情况下,共享资源会发生冲突,因此在访问共享资源之前会先加锁,确保其他线程无法同时访问。

(2)实现方式:通常使用各种类型的锁来实现,如内置锁(synchronized)、ReentrantLock等。

(3)应用场景:适用于写入操作较频繁的情况,因为悲观锁能确保在对共享资源进行写操作时,其他线程无法同时访问。适用于冲突较多的情况,例如涉及到复杂的业务逻辑或需要长时间保持共享资源的独占。

35、如何选择?

(1)选择乐观锁时,需要确保在冲突发生时能够进行合适的处理,例如重新尝试、回滚等。

(2)选择悲观锁时,要注意避免锁的粒度过大,以减少锁竞争的可能性。

三、高并发

1、并发的三大特性?

(1)原子性:指在一个操作中CPU不可以在中途暂停,然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。AtomicInteger(保证i++) CAS 【synchronized】

(2)可见性(synchronized):当多个线程访问同一变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。(底层协议:总线Lock,和MESI保证可见性)【volatile、final、synchronized】

(3)有序性:虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。【volatile、synchronized】

2、ThreadLocal的底层原理和使用场景?

ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象中都存在一个ThreadLocalMap,key为ThreadLocal对象,value为需要缓存的值。

当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储到ThreadLocalMap对象中。

get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。

是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据。由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。

使用场景:

(1)在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

(2)线程间数据隔离。

(3)进行事务操作,用于存储线程事务信息。

(4)数据库连接,Session会话管理。

Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离。

3、ThreadLocal内存泄漏原因,如何避免?

ThreadLocal内存泄漏的根源是:因为当ThreadLocal对象使用完之后,应该把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏的问题。

ThreadLocal正确的使用方法:

(1)每次使用完ThreadLocal都调用它的remove()方法清除数据。

(2)将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

如果key为强引用,会出现什么问题?

当ThreadLocalMap的key为强引用,回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致内存泄露。

key使用弱引用,会怎么样呢?

当ThreadLocalMap的key为弱引用,回收ThreadLocal时,因为ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。

4、Java的四种引用类型?

强引用:一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。

弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。

软引用:用于描述一些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会回收软引用指向的对象。软引用通常用于实现内存敏感的缓存。

虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾收集器回收。

口诀:强引用不回收,弱引用JVMGC时回收,软引用内存不足时回收,虚引用随时会被回收。

5、Synchronized的底层原理?

synchronized的底层实现主要区分:代码块和方法。

synchronized修饰代码块时,编译后会生成 monitorenter和monitorexit指令,分别对应进入同步块和退出同步块。会有两个monitorexit,这是因为编译时JVM为代码块添加隐式的try-finally,在finally中进行了锁释放,这也是为什么synchronized不需要手动释放锁的原因。

synchronized修饰方法时,编译后会生成ACC_SYNCHRONIZED标记,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED访问标志是否被设置,如果设置了则会先尝试获得锁。

两种实现其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

6、Synchronized底层维护了几个链表存放被阻塞的线程?

synchronized底层对应的JVM模型为objectMonitor,使用了3个双向链表来存放被阻塞的线程:cxq(Contention queue)、EntryList、WaitSet。

当线程获取锁失败进入阻塞后,首先会被加入到cxq链表,cxq链表的节点会在某个时刻被进一步转移到EntryList链表。

当持有锁的线程释放锁后,EntryList链表头结点的线程会被唤醒,该线程称为successor(假定继承者),然后该线程会尝试抢占锁。

当调用wait()时,线程会被放入WaitSet,直到调用了notify()/notifyAll()后,线程才被重新放入cxq或EntryList,默认放入cxq链表头部。

7、为什么释放锁时被唤醒的线程被称为”假定继承者“?被唤醒的线程一定能获取到锁吗?

因为被唤醒的线程并不是就一定获取到锁了,该线程仍然需要去竞争锁,而且可能会失败,所以该线程并不是就一定会成为锁的”继承者“,而只是有机会成为,所以称它为假定的。

这是 synchronized 为什么是非公平锁的一个原因。

8、Synchronized 为什么是非公平锁?非公平体现在哪些地方?

(1)当持有锁的线程释放锁时,该线程会执行以下两个重要操作:

1)先将锁的持有者owner属性赋值为null;

2)然后唤醒等待链表中的一个线程。

在1和2之间,如果有其他线程刚好在尝试获取锁,则可以马上获取到锁。

(2)当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表就会先被唤醒。

9、notifyAll 是怎么实现全唤起的?

notify是获取WaitSet的头结点,执行唤起操作。

notifyAll的流程就是循环遍历WaitSet的所有节点,对每个节点执行notify操作。

10、Synchronized各种加锁场景的作用范围?

(1)作用于非静态方法,锁住的是对象实例(this),每一个对象实例有一个锁。

public synchronized void method(){}

(2)作用于静态方法,锁住的是类的Class对象,因为Class的相关数据存储在永久代元空间,元空间是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁住所有调用该方法的线程。

public static synchronized void method(){}

(3)作用于Lock.class,锁住的是Lock的Class对象,也是全局只有一个。

synchronized (Lock.class) {}

(4)作用于this,锁住的是对象实例,每一个对象实例有一个锁。

synchronized (this) {}

(5)作用于静态成员变量,锁住的是该静态成员变量对象,由于是静态变量,因此全局只有一个。

public static Object monitor = new Object();

synchronized (monitor) {}

11、Synchronized和Lock的区别?

(1)Lock是一个接口;synchronized是Java中的关键字。

(2)Lock在发生异常时,如果没有主动通过unLock()去释放锁,很可能会造成死锁现象,因此使用Lock时需要在finally块中释放锁;synchronized不需要手动获取锁和释放锁,在发生异常时,会自动释放锁,因此不会导致死锁现象发生。

(3)Lock的使用更加灵活,可以有响应中断、有超时时间等;而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,直到获取到锁。

13、为什么要引入偏向锁和轻量级锁?为什么重量级锁开销大?

重量级锁底层依赖于系统的同步函数来实现,在 linux 中使用 pthread_mutex_t(互斥锁)来实现。

这些底层的同步函数操作会涉及到:操作系统用户态和内核态的切换、进程的上下文切换,而这些操作都是比较耗时的,因此重量级锁操作的开销比较大。

而在很多情况下,可能获取锁时只有一个线程,或者是多个线程交替获取锁,在这种情况下,使用重量级锁就不划算了,因此引入了偏向锁和轻量级锁来降低没有并发竞争时的锁开销。

14、偏向锁有撤销、膨胀,性能损耗这么大为什么要用呢?

偏向锁的好处是在只有一个线程获取锁的情况下,只需要通过一次CAS操作修改markword ,之后每次进行简单的判断即可,避免了轻量级锁每次获取释放锁时的CAS操作。

如果确定同步代码块会被多个线程访问或者竞争较大,可以通过-XX:-UseBiasedLocking参数关闭偏向锁。

15、偏向锁、轻量级锁、重量级锁分别对应了什么使用场景?

(1)偏向锁:适用于只有一个线程获取锁。当第二个线程尝试获取锁时,即使此时第一个线程已经释放了锁,此时还是会升级为轻量级锁。但是有一种特例,如果出现偏向锁的重偏向,则此时第二个线程可以尝试获取偏向锁。

(2)轻量级锁:适用于多个线程交替获取锁。跟偏向锁的区别在于可以有多个线程来获取锁,但是必须没有竞争,如果有则会升级为重量级锁。

(3)重量级锁:适用于多个线程同时获取锁。

16、自旋发生在哪个阶段?

轻量级锁阶段并没有自旋操作,在轻量级锁阶段,只要发生竞争,就是直接膨胀成重量级锁。而在重量级锁阶段,如果获取锁失败,则会尝试自旋去获取锁。

17、为什么要设计自旋操作?

因为重量级锁的挂起开销太大。一般来说,同步代码块内的代码应该很快就执行结束,这时候竞争锁的线程自旋一段时间是很容易拿到锁的,这样就可以节省了重量级锁挂起的开销。

18、自适应自旋是如何体现自适应的?

自适应自旋锁有自旋次数限制,范围在:1000~5000。如果当次自旋获取锁成功,则会奖励自旋次数100次,如果当次自旋获取锁失败,则会惩罚扣掉次数200次。所以如果自旋一直成功,则JVM认为自旋的成功率很高,值得多自旋几次,因此增加了自旋的尝试次数。相反的,如果自旋一直失败,则JVM认为自旋只是在浪费时间,则尽量减少自旋。

19、Synchronized 锁能降级吗?

可以。具体的触发时机是在全局安全点中,执行清理任务的时候会触发尝试降级锁。当锁降级时,主要进行了以下操作:

(1)恢复锁对象的markword对象头;

(2)重置ObjectMonitor,然后将该ObjectMonitor放入全局空闲列表,等待后续使用。

20、CAS是什么?

CAS需要3个操作数:内存地址V,旧的预期值A,和即将要更新的目标值B。

CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。

CAS的使用流程为:1)首先从地址V读取值A;2)根据A计算目标值B;3)通过CAS以原子的方式将地址V中的值从A修改为B。

CAS的缺点,存在三大问题:

(1)循环时间长开销很大;(CAS通常是配合无线循环一起使用的,比如,getAndAddInt方法执行时,如果CAS失败,会一直进行尝试;如果CAS长时间一直不成功,可能会给CPU带来很大的开销。)

(2)只能保证一个变量的原子操作;(当对一个变量执行操作时,可以使用循环CAS的方式来保证原子操作,但是对多个变量操作时,CAS目前无法直接保证操作的原子性,但是可以通过以下两种方法解决:1、使用互斥锁来保证原子性,2、将多个变量封装成对象,通过AtomicReference来保证原子性。)

(3)ABA问题。在第1步中读取的值是A,并且在第3步修改成功了,并不能确定它的值在第1步和第3步之间有没有被其他线程改变。如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清除“ABA“问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

21、Synchronized和ReentrantLock的区别?

(1)sychronized是一个关键字,ReentrantLock是一个类。

(2)sychronized会自动的加锁与释放锁,ReentrantLock需要程序员手动加锁与释放锁。

(3)synchronized是Java中的关键字是JVM级别的锁,而ReentrantLock是一个Lock接口下的实现类,是API层面的锁。

(4)sychronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁。

(5)sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态。

(6)sychronized底层有一个锁升级的过程。

22、ReentrantLock中的公平锁和非公平锁的底层实现?

首先不管是公平锁还是非公平锁,它们的底层实现都会使用AQS来进行排队,区别在于:线程在使用lock()方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队;如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。

不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段。

另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。

ReentrantLock默认是非公平锁,性能更高一点。

可重入锁:同一个线程可以多次获取同一个锁,而不会造成死锁。

23、ReentrantLock中tryLock()和lock()方法的区别?

(1)tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false。

(2)lock()表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值。

24、CountDownLatch和Semaphore的区别和底层原理?

CountDownLatch表示计数器,可以给CountDownLatch设置一个数字,一个线程调用CountDownLatch的await()将会阻塞,其他线程可以调用CountDownLatch的countDown()方法来对CountDownLatch中的数字减一,当数字被减成0后,所有await的线程都将被唤醒。对应的底层原理就是,调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒。

Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过acquire()来获取许可,如果获取不到,线程会阻塞,可以通过release()方法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第一个线程开始依次唤醒,直到没有空闲许可。

25、CountDownLatch和CyclicBarrier的区别?

两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:

  1. CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。

  2. CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务。

  3. CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。

26、ReentrantLock锁的是对象吗?在分布式中可以用吗?

ReentrantLock是基于对象的,但并不是直接锁住对象本身,而是使用一个实例来管理锁的状态。具体地,它是通过代码中int类型的state标识来标识锁的状态。

在分布式环境中,ReentrantLock通常只用于单个Java虚拟机内的多线程同步,它不适用于分布式系统。在分布式环境中,因为不同的节点之间无法直接共享Java对象,所以ReentrantLock无法实现跨节点的锁定。

在分布式环境中,常见的分布式锁有:

(1)基于数据库的分布式锁:可以利用数据库的事务特性来实现分布式锁。通过在数据库中创建一个专门的锁表,不同的节点可以通过竞争数据库行级锁或者悲观锁来获取锁。

(2)基于Redis的分布式锁:通过在缓存中存储锁的状态信息,节点可以通过竞争缓存中的某个键来获取锁。常见的实现方式包括使用Redis的SETNX(set not exists)命令来创建锁。(key是锁名称,value是谁抢到了锁)

(3)基于Zookeeper的分布式锁:Zookeeper是一个分布式协调服务,可以用于实现分布式锁。节点可以通过在Zookeeper上创建临时有序节点来竞争锁。获取锁的节点是创建最小序号节点的节点,这种方式确保了锁的有序性。

(4)基于消息队列的分布式锁:通过在消息队列中发送锁请求和锁释放消息,可以实现分布式锁。节点可以订阅消息队列来获取锁的状态变化。

27、如何检测死锁?

死锁的四个必要条件:

(1)互斥条件:进程对所分配到的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

(2)请求和保持条件:进程已经获得了至少一个资源,但又对其他资源发出请求,而该资源已被其他进程占有,此时该进程的请求被阻塞,但又对自己获得的资源保持不放。

(3)不可剥夺条件:进程已获得的资源在未使用完毕之前,不可被其他进程强行剥夺,只能由自己释放。

(4)循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{P1,P2,...,Pn},其中Pi等待的资源被P(i+1)占有(i=0,1,...,n-1),Pn等待的资源被P0占有。

一个线程需要同时获得多把锁。

死锁诊断(idea自带):jps(查看运行的线程)、jstack(查看线程运行的情况)、可视化工具:jconsole、Visual VM。

28、怎么预防死锁?

预防死锁的方式就是打破四个必要条件中的任意一个即可。

(1)打破互斥条件:在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是肯定不会发生。但一般来说在所列的四个条件中,“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件。

(2)打破请求和保持条件:1)采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待。 2)每个进程提出新的资源申请前,必须先释放它先前所占有的资源。

(3)打破不可剥夺条件:当进程占有某些资源后又进一步申请其他资源而无法满足,则该进程必须释放它原来占有的资源。

(4)打破循环等待条件:实现资源有序分配策略,将系统的所有资源统一编号,所有进程只能采用按序号递增的形式申请资源。

 
posted @   守漠待花开  阅读(44)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示