目录
web开发基础
说一下你熟悉的设计原则和设计模式
说说你对红黑树的理解
Java基础
抽象类和接口的区别
hashcode()值相同,equals就一定为true
为什么重写equals(),就要重写hashcode()?
short s = 1;s = s + 1;(程序1)和 short s = 1; s += 1;(程序2)是否都能正常运行
说出下面程序的运行结果,及原因
Error和Exception有什么区别
NoClassDefFoundError和ClassNotFoundException区别
如果try{} 里有一个 return 语句,那么finally{} 里的代码会不会被执行,什么时候被执行,在 return 前还是后?
说说final关键字的作用
finally是什么
finalize简介
说说Java中== 和 equals
反射的机制和应用场景
Java集合
List、Set、Map的区别
List、Set、Map常用集合有哪些?
ArrayList的初始容量是多少?扩容机制是什么?扩容过程是怎样?
set有什么特点?为什么set不允许重复?
什么是hashmap
什么是哈希冲突/哈希碰撞?
Hashmap的底层实现机制
简单说说hashmap的加载因子为什么是0.75?
为什么HashMap的加载因子不能是0.8或0.6?
能否自定义HashMap的加载因子?
为什么HashMap的初始容量和扩容都是2的次幂
HashMap如果指定了不是2的次幂的容量会发生什么?
HashMap为什么线程不安全
如何解决Hashmap的线程安全问题
为什么用synchronized代替ReentrantLock
HashMap为什么使用链表
HashMap为什么使用红黑树
HashMap为什么不一上来就使用红黑树
Java并发与多线程
说说并发与并行的区别
说说你对线程安全的理解
什么是进程和线程?
线程有几种创建方式?
一个8G的系统能创建多少个线程?
启动一个Java程序,里面会有哪些线程?
调用 start()方法时会执行 run()方法,那怎么不直接调用 run()方法?
start() 与 run() 的区别
为什么不能直接调用 run() 方法
说说线程中的等待与通知
说说sleep(long millis)方法
说说yield()方法
说说线程中断
说说线程有几种状态
什么是上下文切换?
什么是守护线程?
线程间的通信方式有哪些?
说说sleep和wait方法的区别
如何保证线程安全?
有个int的变量为0,十个线程轮流对其进行++操作(循环10000次),结果是大于小于还是等于10万,为什么?
有一个 key 对应的 value 是一个json 结构,json 当中有好几个子任务,这些子任务如果对 key 进行修改的话,会不会存在线程安全的问题?如何解决?如果是多个节点的情况,应该怎么加锁?
说一个线程安全的使用场景?
什么是ThreadLocal?
除了 ThreadLocal,还有什么解决线程安全问题的方法?
ThreadLocal是怎么实现的?
举例说明一些Threadlocal的应用场景
ThreadLocal 内存泄露是怎么回事?
那怎么解决内存泄漏问题呢?
ThreadLocalMap 的底层实现机制
ThreadLocalMap 怎么解决 Hash 冲突的?
SOLID原则:
Single responselibitlity
Open-closed
Liskov Substitution
Interface segregation
Dependency injection
单例模式: 保证被创建一次,节省系统开销。
工厂模式: 解耦代码。
观察者模式: 定义了对象之间的一对多的依赖,这样一来,当一个对象改变时,它的所有的依赖者都会收到通知并自动更新。
代理模式: 代理对象具备被代理对象的功能,并代替被代理对象完成相应操作,并能够在操作执行的前后,对操作进行增强处理。
模板模式: 较少代码冗余。例如:redis模板。
①根节点是黑色。
②节点是黑色或红色。
③叶子节点是黑色。
④红色节点的子节点都是黑色。
⑤从任意节点到其子节点的所有路径都包含相同数目的黑色节点。红黑树从根到叶子节点的最长路径不会超过最短路径的2倍。保证了红黑树的高效。
相同点:都是不断抽取出来的抽象概念
区别:
注:JDK1.8中对接口增加了新的特性:
(1)默认方法(default method):JDK 1.8允许给接口添加非抽象的方法实现,但必须使用default关键字修饰;定义了default的方法可以不被实现子类所实现,但只能被实现子类的对象调用;如果子类实现了多个接口,并且这些接口包含一样的默认方法,则子类必须重写默认方法;
(2)静态方法(static method):JDK 1.8中允许使用static关键字修饰一个方法,并提供实现,称为接口静态方法。接口静态方法只能通过接口调用(接口名.静态方法名)。
不一定,因为 "重地"和"通话"的hashcode值就相同,但是equals()就为false。
但是equals()为true,那么hashcode一定相同。
保证同一对象,如果不重写hashcode,可能会出现equals比较一样,但是hashcode不一样的情况。
程序1会编译报错,因为 s + 1的1是int类型,因为类型不兼容。强制转换失败。
程序2可以正常运行,因为java在复合赋值解释是 E1 += E2,等价于 E1 = (T)(E1 + E2),T是E1的类型,因此s += 1等价于 s = (short)(s + 1),所以进行了强制类型的转换,所以可以正常编译。
public static void main(String[] args) {
Integer a = 128, b = 128, c = 127, d = 127;
System.out.println(a == b);
System.out.println(c == d);
}
结果:false,true
因为Integer = a,相当于自动装箱(基础类型转为包装类),因为Integer引入了IntegerCache来缓存一定的值,IntegerCache默认是 -128~127,所以128超过了范围,a和b不是相同对象,c和d是相同对象。可以通过jvm启动时,修改缓存的上限。
会执行,在return之前执行,如果finally有return那么try的return就会失效。
1、final可以用来修饰的结构:类、方法、变量
2、final用来修饰一个类:此类不能被其它类继承。当我们需要让一个类永远不被继承,此时就可以用final修饰,但要注意:final类中所有的成员方法都会隐式的定义为final方法。
比如:String类、System类、StringBuffer类
3、final 用来修饰方法 :表明此方法不可以被重写
作用
(1) 把方法锁定,以防止继承类对其进行更改。
(2) 效率,在早期的java版本中,会将final方法转为内嵌调用。但若方法过于庞大,可能在性能上不会有多大提升。因此在最近版本中,不需要final方法进行这些优化了。
final方法意味着“最后的、最终的”含义,即此方法不能被重写。
比如:Object类中的getClass( )
4、final 用来修饰变量 ,此时变量就相当于常量
final用来修饰属性:可以考虑赋值的位置有:显式初始化、代码块中初始化、构造器中初始化
final修饰局部变量:尤其是使用final修饰形参时,表明此形参是一个常量。当我们调用此方法时,给常量形参赋一个实参,一旦赋值之后,就只能在方法体内使用此形参的值,不能重新进行赋值。
如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了或者说他的地址不能发生变化了(因为引用的值是一个地址,final要求值,即地址的值不发生变化),但该引用所指向的对象的内容是可以发生变化的。本质上是一回事。
5、static final 用来修饰属性:属于类的资源-全局变量,static就是类在被加载进内存的时候(也就是应用程序启动的时候)就要已经为此属性分配了内存,所以此时属性已经存在,它又被final修饰,所以必须在属性定义了以后就给其初始化值。
而构造函数是在当类被实例化的时候才会执行,所以用构造函数,这时候这个属性没有被初始化,程序就会报错。
而static块是类被加载的时候执行,且只执行这一次,所以在static块中可以被初始化。
异常处理机制:try ——>catch ——>finally(在方法中)
异常的处理:抓抛模型。
说明:
1、finally是可选的,
2、使用try将可能出现异常代码包装起来,在执行过程中,一旦出现异常,就会生成一个对应一个异常类的对象。
一旦try中
catch中的异常类型如果没有子父类关系,则谁声明在上,谁声明在下没有关系。
catch中的异常类型如果满足子父类关系,则要求子类一定要声明在父类的上面,否则,报错、
常用的异常对象的处理方式:
String getMessage()
printStackTrace()
常用的异常对象处理的方式,再出了try结构以后,就不能再被调用。
try—catch是可以嵌套的。
finally的使用:一定会被执行的代码:
可以不写
即使catch 中又出现异常了,try中有return语句,catch中有return语句等情况。
finally使用的场景:
数据库连接
输入输出流
网络编程Socket等资源,JVM是不能自动的回收的。我们需要手动的进行资源释放。此时就需要放在finally中
1. finalize定义
finalize()是在java.lang.Object里定义的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。也就是说每一个对象都有这么个方法。
这个方法在GC启动,该对象被回收的时候被调用。其实GC可以回收大部分的对象(凡是new出来的对象,GC都能搞定,一般情况下我们又不会用new以外的方式去创建对象),所以一般是不需要程序员去实现finalize的。
特殊情况下,需要程序员实现finalize,当对象被回收的时候释放一些资源,比如:一个socket链接,在对象初始化时创建,整个生命周期内有效,那么就需要实现finalize,关闭这个链接。
使用finalize还需要注意一个事,调用super.finalize();
一个对象的finalize()方法只会被调用一次,而且finalize()被调用不意味着GC会立即回收该对象,所以有可能调用finalize()后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会调用finalize(),产生问题。 所以,推荐不要使用finalize()方法,它跟析构函数不一样。
2. finalize的执行过程(生命周期)
(1) 首先,描述finalize大致流程:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。
否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。
执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。
(2) 具体的finalize
各状态含义如下:
unfinalized:新建对象会先进入此状态,GC并未准备执行其finalize方法,因为该对象是可达的
finalizable:表示GC可对该对象执行finalize方法,GC已检测到该对象不可达。正如前面所述,GC通过F-Queue队列和一专用线程完成finalize的执行
finalized:表示GC已经对该对象执行过finalize方法
reachable:表示GC Roots引用可达
finalizer-reachable(f-reachable):表示不是reachable,但可通过某个finalizable对象可达
unreachable:对象不可通过上面两种途径可达
在 Java 中,"==" 和 "equals" 有着不同的作用:
"==" 运算符:
在基本数据类型(如 int、char 等)中,"==" 用于比较它们的值是否相等。
在引用类型中,"==" 比较的是对象引用(即内存地址)是否相同,即是否指向同一块内存。
"equals" 方法:
"equals" 是一个方法,用于比较对象的内容是否相等,它是 Object 类的方法,可以被子类覆盖重写。
默认情况下,Object 类中的 "equals" 方法是使用 "==" 比较两个对象的引用,因此如果没有在类中重写 "equals" 方法,那么使用 "equals" 和 "==" 会有相同的行为。
在源码中,"==" 实际上是在比较对象的引用,即比较两个对象的内存地址是否相同。这在 Java 虚拟机中通过比较对象的引用地址来实现。这是因为 "==" 操作符比较的是两个对象的引用,如果两个对象的引用指向的是同一个内存地址,则返回 true。
对于 "equals" 方法,它是一个可以被覆盖重写的方法。在 Object 类中的默认实现是直接使用 "==" 操作符来比较两个对象的引用地址。但是,许多类(如 String、Integer 等)会覆盖 "equals" 方法以实现内容比较而非引用比较。因此,"equals" 方法的实现可以根据类的需求而有所不同。
"==" 比较对象的引用,而 "equals" 方法用于比较对象的内容,但前提是需要根据需要在类中重写 "equals" 方法。
基本概念
Java中的反射机制是指在运行时访问类的信息和操作类的能力。通过反射,程序可以获取类的所有属性和方法,创建对象的实例,调用方法,甚至修改私有属性的值。反射机制提供了一种动态处理类的方式,使得程序能够在运行时灵活地创建和使用对象。
反射机制的组成部分
反射机制的用途
经典应用场景
java集合框架详解
HashMap底层原理详解
List集合有序、可重复的单例集合。
Set集合无序、不可重复的单例集合。
Map集合无序、k不可重复,v可重复的双例集合。
List
Set
Map
注意:扩容后的ArrayList底层数组不是原来的数组。
set存储元素是无序不重复的。
hashCode()方法:
因为当我们向Set中添加元素时,首先会调用该对象的hashCode()方法,计算并返回一个整数(哈希值)。为了提高比较效率,我们需要先进行hash计算。如果两个对象的hash值不同,则表示当前是两个对象,但是如果两个对象的hash值是相同的,则这两个对象可能相同,此时在进行equals。
equals()方法:
如果两个对象的哈希值相同,Set会进一步调用equals()方法,来确定这两个对象是否真正相同。只有当equals()方法返回false时,才将新对象添加到集合中
Set的实现机制
Java中的Set接口有多种实现类,如HashSet、LinkedHashSet和TreeSet等。这些实现类通过不同的机制来保证元素的唯一性。
HashSet:
HashSet是基于哈希表实现的。它使用哈希函数来计算元素的哈希值,并根据哈希值将元素存储在哈希表的相应位置。由于哈希表的特性,每个位置只能存储一个元素(或称为桶),因此HashSet可以保证元素的唯一性。
LinkedHashSet:
LinkedHashSet是HashSet的一个子类,它继承了HashSet的所有特性,并额外维护了一个双向链表来记录元素的插入顺序。尽管LinkedHashSet保留了元素的插入顺序,但它仍然保证了元素的唯一性。
TreeSet:
TreeSet是基于红黑树实现的。红黑树是一种自平衡的二叉搜索树,它可以在O(log n)的时间复杂度内完成元素的插入、删除和查找操作。TreeSet通过红黑树的特性来保证元素的唯一性和有序性。
根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把key值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
就是键(key)经过hash函数得到的结果作为地址去存放当前的键值对,但是却发现该地址已经被占用,这个时候就会发生冲突。这个冲突就是hash冲突了。
概括:如果两个不同对象的hashCode相同,这种现象称为哈希冲突。
解决hash冲突的方法
(1) 开放地址法
插入一个元素的时候,先通过哈希函数进行判断,若是发生哈希冲突,就以当前地址为基准,根据再寻址的方法(探查序列),去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。所以这种方法又称为再散列法。
(2) 再哈希法
再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。
(3) 链地址法
每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来。
(4)建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
加载因子是HashMap中一个重要的参数,它决定了HashMap在什么时候进行扩容。加载因子的值越小,哈希表的冲突几率就越小,查询效率越高。但是如果加载因子设置得太小,会导致哈希表频繁地进行扩容,增加了时间和空间的开销。相反,如果加载因子设置得太大,会增加哈希冲突的几率,降低查询效率。
所以,为了权衡哈希冲突的几率和查询效率的平衡,Java中的HashMap实现选择了0.75作为默认的加载因子。这个值既能有效地减少哈希冲突的几率,又能保持较高的查询效率。根据实际需求,我们也可以根据业务场景调整加载因子的大小,以获得更好的性能。
虽然加载因子可以根据实际需求进行调整,但为什么HashMap的加载因子不能选择0.8或0.6这样的值呢?这是因为在哈希冲突的情况下,加载因子的值会直接影响到HashMap的性能。
当加载因子值较大(如0.8)时,哈希表的填充率较高,冲突的几率增加。这会导致链表长度的增加,进而降低查询和插入的效率。同时,加载因子越小,扩容的频率也会增加,导致性能下降。另一方面,如果加载因子值较小(如0.6),哈希表的填充率较低,可能会导致内存的浪费。
因此,在综合考虑哈希冲突和性能的情况下,选择0.75作为HashMap的默认加载因子更加合适,能够在时间和空间的开销上取得一个比较好的平衡。
Java中的HashMap类允许我们在创建HashMap实例时自定义加载因子。通过指定不同的加载因子值,可以根据实际需求调整HashMap的性能。例如,如果我们需要更高的查询效率,可以将加载因子设定为较小的值(如0.5或0.6);如果我们更加关注内存的使用,可以将加载因子设定为较大的值(如0.8或0.9)。
HashMap的hash()算法,为什么不是h=key.hashcode(),而是key.hashcode()^ (h>>>16)
得到哈希值然后右移16位,然后进行异或运算,这样使哈希值的低16位也具有了一部分高16位的特性,增加更多的变化性,减少了哈希冲突。
(1)使用Hashtable解决,在方法加同步关键字,所以效率低下,已经被弃用。
(2)使用Collections.synchronizedMap(new HashMap<>()),不常用。
(3)ConcurrentHashMap(常用)
说说ConcurrentHashMap的原理
①节省内存开销。ReentrantLock基于AQS来获得同步支持,但不是每个节点都需要同步支持,只有链表头节点或树的根节点需要同步,所以使用ReentrantLock会带来很大的内存开销。
②获得jvm支持,可重入锁只是api级别,而synchronized是jvm直接支持的,能够在jvm运行时做出相应的优化。
③在jdk1.6之后,对synchronized做了大量的优化,而且有多种锁状态,会从 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步步转换。
AQS (Abstract Queued Synchronizer): 一个抽象的队列同步器,通过维护一个共享资源状态( Volatile Int State )和一个先进先出( FIFO )的线程等待队列来实现一个多线程访问共享资源的同步框架。
线程安全是指一段代码块或者一个方法在多线程环境中被多个线程同时执行时能够正确地处理共享数据,可以从三个要素来确保线程安全:
①、原子性:确保当某个线程修改共享变量时,没有其他线程可以同时修改这个变量,即这个操作是不可分割的。原子性可以通过互斥锁(如 synchronized)或原子操作(如 AtomicInteger 类中的方法)来保证。使用Lock
接口及其实现类:如ReentrantLock
,通过显式地加锁和解锁来确保操作
②、可见性:可见性是指当一个线程修改了共享变量的值时,这个新值对其他线程来说是立即可见的。在Java中,保证可见性的方法包括:
volatile
关键字:声明为volatile
的变量在修改后会立即对所有线程可见。synchronized
关键字:同步方法或代码块在释放锁时会刷新共享变量的值,确保其他线程能看到最新的值。final
关键字:对于不可变对象,使用final
关键字可以确保对象的引用不变,从而保证可见性。③、有序性:有序性是指程序执行的顺序必须符合预期,不能出现乱序的情况。Java内存模型通过以下机制保证有序性:
volatile
关键字可以防止指令重排序优化,确保程序的执行顺序与代码的顺序相同。synchronized
和volatile
关键字:这些关键字在编译时会被处理以确保内存操作的顺序性。Locks
和Condition
:通过显式锁定和解锁操作,可以更灵活地控制有序性。
进程说简单点就是我们在电脑上启动的一个个应用。它是操作系统分配资源的最小单位。
线程是进程中的独立执行单元。多个线程可以共享同一个进程的资源,如内存;每个线程都有自己独立的栈和寄存器。
run()
方法,然后创建 Thread 对象,将 Runnable 对象作为参数传递给 Thread 对象,调用 start()
方法启动线程。call()
方法,然后创建 FutureTask 对象,参数为 Callable 对象;紧接着创建 Thread 对象,参数为 FutureTask 对象,调用 start()
方法启动线程。在确定一个系统最多可以创建多个线程时,除了需要考虑系统的内存大小外,Java 虚拟机栈的大小也是值得考虑的因素。线程在创建的时候会被分配一个虚拟机栈,在 64 位操作系统中,默认大小为 1M。通过 java -XX:+PrintFlagsFinal -version | grep ThreadStackSize
这个命令可以查看 JVM 栈的默认大小。其中 ThreadStackSize 的单位是字节,也就是说默认的 JVM 栈大小是 1024 KB,也就是 1M。换句话说,8GB = 8 _ 1024 MB = 8 _ 1024 _ 1024 KB,所以一个 8G 内存的系统可以创建的线程数为 8 _ 1024 = 8192 个。但操作系统本身的运行也需要消耗一定的内存,所以实际上可以创建的线程数肯定会比 8192 少一些。
首先是 main 线程,这是程序开始执行的入口。
然后是垃圾回收线程,它是一个后台线程,负责回收不再使用的对象。
还有编译器线程,在及时编译中(JIT),负责把一部分热点代码编译后放到 codeCache 中,以提升程序的执行效率。
Thread: main (ID=1)
- 主线程,Java 程序启动时由 JVM 创建。Thread: Reference Handler (ID=2)
- 这个线程是用来处理引用对象的,如软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference)。负责清理被 JVM 回收的对象。Thread: Finalizer (ID=3)
- 终结器线程,负责调用对象的 finalize 方法。对象在垃圾回收器标记为可回收之前,由该线程执行其 finalize 方法,用于执行特定的资源释放操作。Thread: Signal Dispatcher (ID=4)
- 信号调度线程,处理来自操作系统的信号,将它们转发给 JVM 进行进一步处理,例如响应中断、停止等信号。Thread: Monitor Ctrl-Break (ID=5)
- 监视器线程,通常由一些特定的 IDE 创建,用于在开发过程中监控和管理程序执行或者处理中断。在 Java 中,启动一个新的线程应该调用其start()
方法,而不是直接调用run()
方法。
当调用start()
方法时,会启动一个新的线程,并让这个新线程调用run()
方法。这样,run()
方法就在新的线程中运行,从而实现多线程并发。
如果直接调用run()
方法,那么run()
方法就在当前线程中运行,没有新的线程被创建,也就没有实现多线程的效果。
在 Object 类中有一些方法可以用于线程的等待与通知。
①、wait()
:当一个线程 A 调用一个共享变量的 wait()
方法时,线程 A 会被阻塞挂起,直到发生下面几种情况才会返回 :
notify()
或者 notifyAll()
方法;interrupt()
方法,线程 A 抛出 InterruptedException 异常返回。②、wait(long timeout)
:这个方法相比 wait()
方法多了一个超时参数,它的不同之处在于,如果线程 A 调用共享对象的 wait(long timeout)
方法后,没有在指定的 timeout 时间内被其它线程唤醒,那么这个方法还是会因为超时而返回。
③、wait(long timeout, int nanos)
,其内部调用的是 wait(long timout)
方法。
唤醒线程主要有下面两个方法:
①、notify()
:一个线程 A 调用共享对象的 notify()
方法后,会唤醒一个在这个共享变量上调用 wait 系列方法后被挂起的线程。
一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
②、notifyAll()
:不同于在共享变量上调用 notify()
方法会唤醒被阻塞到该共享变量上的一个线程,notifyAll 方法会唤醒所有在该共享变量上调用 wait 系列方法而被挂起的线程。
Thread 类还提供了一个 join()
方法,意思是如果一个线程 A 执行了 thread.join()
,当前线程 A 会等待 thread 线程终止之后才从 thread.join()
返回。
sleep(long millis)
:Thread 类中的静态方法,当一个执行中的线程 A 调用了 Thread 的 sleep 方法后,线程 A 会暂时让出指定时间的执行权。
但是线程 A 所拥有的监视器资源,比如锁,还是持有不让出的。指定的睡眠时间到了后该方法会正常返回,接着参与 CPU 的调度,获取到 CPU 资源后就可以继续运行
yield()
:Thread 类中的静态方法,当一个线程调用 yield 方法时,实际是在暗示线程调度器,当前线程请求让出自己的 CPU,但是线程调度器可能会“装看不见”忽略这个暗示
Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行。被中断的线程会根据中断状态自行处理。
void interrupt()
方法:中断线程,例如,当线程 A 运行时,线程 B 可以调用线程 interrupt()
方法来设置线程的中断标志为 true 并立即返回。设置标志仅仅是设置标志, 线程 B 实际并没有被中断,会继续往下执行。boolean isInterrupted()
方法: 检测当前线程是否被中断。boolean interrupted()
方法: 检测当前线程是否被中断,与 isInterrupted 不同的是,该方法如果发现当前线程被中断,则会清除中断标志。状态 | 说明 |
---|---|
NEW | 当线程被创建后,如通过new Thread() ,它处于新建状态。此时,线程已经被分配了必要的资源,但还没有开始执行。 |
RUNNABLE | 当调用线程的start() 方法后,线程进入可运行状态。在这个状态下,线程可能正在运行也可能正在等待获取 CPU 时间片,具体取决于线程调度器的调度策略。 |
BLOCKED | 线程在试图获取一个锁以进入同步块/方法时,如果锁被其他线程持有,线程将进入阻塞状态,直到它获取到锁。 |
WAITING | 线程进入等待状态是因为调用了如下方法之一:Object.wait() 或LockSupport.park() 。在等待状态下,线程需要其他线程显式地唤醒,否则不会自动执行。 |
TIME_WAITING | 当线程调用带有超时参数的方法时,如Thread.sleep(long millis) 、Object.wait(long timeout) 或LockSupport.parkNanos() ,它将进入超时等待状态。线程在指定的等待时间过后会自动返回可运行状态。 |
TERMINATED | 当线程的run() 方法执行完毕后,或者因为一个未捕获的异常终止了执行,线程进入终止状态。一旦线程终止,它的生命周期结束,不能再被重新启动。 |
也就是说,线程的生命周期可以分为五个主要阶段:新建、可运行、运行中、阻塞/等待、和终止。线程在运行过程中会根据状态的变化在这些阶段之间切换。
为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是上下文切换。
Java 中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程)。
在 JVM 启动时会调用 main 方法,main 方法所在的线程就是一个用户线程。其实在 JVM 内部同时还启动了很多守护线程, 比如垃圾回收线程。
那么守护线程和用户线程有什么区别呢?区别之一是当最后一个非守护线程束时, JVM 会正常退出,而不管当前是否存在守护线程,也就是说守护线程是否结束并不影响 JVM 退出。换而言之,只要有一个用户线程还没结束,正常情况下 JVM 就不会退出
线程之间传递信息有多种方式,比如说使用共享对象、wait()
和 notify()
方法、Exchanger 和 CompletableFuture。
①、使用共享对象,多个线程可以访问和修改同一个对象,从而实现信息的传递,比如说 volatile 和 synchronized 关键字。
关键字 volatile 用来修饰成员变量,告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,保证所有线程对变量访问的可见性。
关键字 synchronized 可以修饰方法,或者以同步代码块的形式来使用,确保多个线程在同一个时刻,只能有一个线程在执行某个方法或某个代码块。
②、使用 wait() 和 notify(),例如,生产者-消费者模式中,生产者生产数据,消费者消费数据,通过 wait()
和 notify()
方法可以实现生产和消费的协调。
一个线程调用共享对象的 wait()
方法时,它会进入该对象的等待池,并释放已经持有的该对象的锁,进入等待状态。
一个线程调用共享对象的 notify()
方法时,它会唤醒在该对象等待池中等待的一个线程,使其进入锁池,等待获取锁。
Condition 也提供了类似的方法,await()
负责等待、signal()
和 signalAll()
负责通知。
通常与锁(特别是 ReentrantLock)一起使用,为线程提供了一种等待某个条件成真的机制,并允许其他线程在该条件变化时通知等待线程。更灵活、更强大。
③、使用 Exchanger,Exchanger 是一个同步点,可以在两个线程之间交换数据。一个线程调用 exchange()
方法,将数据传递给另一个线程,同时接收另一个线程的数据。
④、使用 CompletableFuture,CompletableFuture 是 Java 8 引入的一个类,支持异步编程,允许线程在完成计算后将结果传递给其他线程。
sleep 会让当前线程休眠,不涉及对象类,也不需要获取对象的锁,属于 Thread 类的方法;wait 会让获得对象锁的线程实现等待,要提前获得对象的锁,属于 Object 类的方法。
它们之间的区别主要有以下几点:
①、所属类不同
sleep()
方法专属于 Thread
类。wait()
方法专属于 Object
类。②、锁行为不同
当线程执行 sleep 方法时,它不会释放任何锁。也就是说,如果一个线程在持有某个对象的锁时调用了 sleep,它在睡眠期间仍然会持有这个锁。
③、使用条件不同
sleep()
方法可以在任何地方被调用。wait()
方法必须在同步代码块或同步方法中被调用,这是因为调用 wait()
方法的前提是当前线程必须持有对象的锁。否则会抛出 IllegalMonitorStateException
异常。④、唤醒方式不同
⑤、抛出异常不同
sleep()
方法在等待期间,如果线程被中断,会抛出 InterruptedException
。wait()
方法同样会在等待期间抛出 InterruptedException
。多线程安全是指在并发环境下,多个线程访问共享资源时,程序能够正确地执行,而不会出现数据不一致或竞争条件等问题。反之,如果程序出现了数据不一致、死锁、饥饿等问题,就称为线程不安全。
在这个场景中,最终的结果会小于 100000,原因在于多线程环境下,++ 操作不是一个原子操作,会出现线程安全问题。
int++ 实际上可以分解为三步:
多个线程在并发执行 ++ 操作时,可能出现以下竞态条件:
可以通过 synchronized 或 AtomicInteger 实现线程安全。
会。
在单节点环境中,可以使用 synchronized 关键字或 ReentrantLock 来保证对 key 的修改操作是原子的。
class KeyManager {
private final ReentrantLock lock = new ReentrantLock();
private String key = "{\"tasks\": [\"task1\", \"task2\"]}";
public String readKey() {
lock.lock();
try {
return key;
} finally {
lock.unlock();
}
}
public void updateKey(String newKey) {
lock.lock();
try {
this.key = newKey;
} finally {
lock.unlock();
}
}
}
在多节点环境中,可以使用分布式锁 Redisson 来保证对 key 的修改操作是原子的。
class DistributedKeyManager {
private final RedissonClient redisson;
public DistributedKeyManager() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
this.redisson = Redisson.create(config);
}
public void updateKey(String key, String newValue) {
RLock lock = redisson.getLock(key);
lock.lock();
try {
// 模拟读取和更新操作
String currentValue = readFromDatabase(key); // 假设读取 JSON 数据
String updatedValue = modifyJson(currentValue, newValue); // 修改 JSON
writeToDatabase(key, updatedValue); // 写回数据库
} finally {
lock.unlock();
}
}
private String readFromDatabase(String key) {
// 模拟从数据库读取
return "{\"tasks\": [\"task1\", \"task2\"]}";
}
private String modifyJson(String json, String newValue) {
// 使用 JSON 库解析并修改
return json.replace("task1", newValue);
}
private void writeToDatabase(String key, String value) {
// 模拟写回数据库
}
}
一个常见的使用场景是在实现单例模式时确保线程安全。
单例模式确保一个类只有一个实例,并提供一个全局访问点。在多线程环境下,如果多个线程同时尝试创建实例,单例类必须确保只创建一个实例。
饿汉式是一种比较直接的实现方式,它通过在类加载时就立即初始化单例对象来保证线程安全。
class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
懒汉式单例则在第一次使用时初始化,这种方式需要使用双重检查锁定来确保线程安全,volatile 用来保证可见性,syncronized 用来保证同步。
public class LazySingleton {
private static volatile LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (LazySingleton.class) {
if (instance == null) { // 第二次检查
instance = new LazySingleton();
}
}
}
return instance;
}
}
ThreadLocal 是 Java 中提供的一种用于实现线程局部变量的工具类。它允许每个线程都拥有自己的独立副本,从而实现线程隔离,用于解决多线程中共享对象的线程安全问题。
使用 ThreadLocal 通常分为四步:
①、创建 ThreadLocal
//创建一个ThreadLocal变量
public static ThreadLocal localVariable = new ThreadLocal<>();
②、设置 ThreadLocal 的值
//设置ThreadLocal变量的值
localVariable.set("沉默王二是沙雕");
③、获取 ThreadLocal 的值
//获取ThreadLocal变量的值
String value = localVariable.get();
④、删除 ThreadLocal 的值
//删除ThreadLocal变量的值
localVariable.remove();
ThreadLocal的优缺点
①、线程隔离:每个线程访问的变量副本都是独立的,避免了共享变量引起的线程安全问题。由于 ThreadLocal 实现了变量的线程独占,使得变量不需要同步处理,因此能够避免资源竞争。
②、数据传递方便:ThreadLocal 常用于在跨方法、跨类时传递上下文数据(如用户信息等),而不需要在方法间传递参数。
①、Java 中的 synchronized 关键字可以用于方法和代码块,确保同一时间只有一个线程可以执行特定的代码段。
public synchronized void method() {
// 线程安全的操作
}
②、Java 并发包(java.util.concurrent.locks)中提供了 Lock 接口和一些实现类,如 ReentrantLock。相比于 synchronized,ReentrantLock 提供了公平锁和非公平锁。
ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 线程安全的操作
} finally {
lock.unlock();
}
}
③、Java 并发包还提供了一组原子变量类(如 AtomicInteger,AtomicLong 等),它们利用 CAS(比较并交换),实现了无锁的原子操作,适用于简单的计数器场景。
AtomicInteger atomicInteger = new AtomicInteger(0);
public void increment() {
atomicInteger.incrementAndGet();
}
④、Java 并发包提供了一些线程安全的集合类,如 ConcurrentHashMap,CopyOnWriteArrayList 等。这些集合类内部实现了必要的同步策略,提供了更高效的并发访问。
ConcurrentHashMap map = new ConcurrentHashMap<>();
⑤、volatile 变量保证了变量的可见性,修改操作是立即同步到主存的,读操作从主存中读取。
private volatile boolean flag = false;
ThreadLocal 本身并不存储任何值,它只是作为一个映射,来映射线程的局部变量。当一个线程调用 ThreadLocal 的 set 或 get 方法时,实际上是访问线程自己的 ThreadLocal.ThreadLocalMap。ThreadLojup 是 ThreadLocal 的静态内部类,它内部维护了一个 Entry 数组,key 是 ThreadLocal 对象,value 是线程的局部变量本身。
ThreadLocal 的实现原理就是,每个线程维护一个 Map,key 为 ThreadLocal 对象,value 为想要实现线程隔离的对象。
1、当需要存线程隔离的对象时,通过 ThreadLocal 的 set 方法将对象存入 Map 中。
2、当需要取线程隔离的对象时,通过 ThreadLocal 的 get 方法从 Map 中取出对象。
3、Map 的大小由 ThreadLocal 对象的多少决定。
用来存储用户信息。很多其它场景的 cookie、session 等等数据隔离都可以通过 ThreadLocal 去实现。
数据库连接池也可以用 ThreadLocal,将数据库连接池的连接交给 ThreadLocal 进行管理,能够保证当前线程的操作都是同一个 Connnection。
通常情况下,随着线程 Thread 的结束,其内部的 ThreadLocalMap 也会被回收,从而避免了内存泄漏。但如果一个线程一直在运行,并且其 ThreadLocalMap
中的 Entry.value 一直指向某个强引用对象,那么这个对象就不会被回收,从而导致内存泄漏。当 Entry 非常多时,可能就会引发更严重的内存溢出问题。
很简单,使用完 ThreadLocal 后,及时调用 remove()
方法释放内存空间。
try {
threadLocal.set(value);
// 执行业务操作
} finally {
threadLocal.remove(); // 确保能够执行清理
}
remove()
方法会将当前线程的 ThreadLocalMap 中的所有 key 为 null 的 Entry 全部清除,这样就能避免内存泄漏问题。
ThreadLocalMap 虽然被叫做 Map,其实它是没有实现 Map 接口的,但是结构还是和 HashMap 比较类似的,主要关注的是两个要素:元素数组
和散列方法
。
元素数组
一个 table 数组,存储 Entry 类型的元素,Entry 是 ThreaLocal 弱引用作为 key,Object 作为 value 的结构。
private Entry[] table;
散列方法
散列方法就是怎么把对应的 key 映射到 table 数组的相应下标,ThreadLocalMap 用的是哈希取余法,取出 key 的 threadLocalHashCode,然后和 table 数组长度减一&运算(相当于取余)。
int i = key.threadLocalHashCode & (table.length - 1);
这里的 threadLocalHashCode 计算有点东西,每创建一个 ThreadLocal 对象,它就会新增0x61c88647
,这个值很特殊,它是斐波那契数 也叫 黄金分割数。hash
增量为 这个数字,带来的好处就是 hash
分布非常均匀。
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
我们可能都知道 HashMap 使用了链表来解决冲突,也就是所谓的链地址法。
ThreadLocalMap 没有使用链表,自然也不是用链地址法来解决冲突了,它用的是另外一种方式——开放定址法。开放定址法是什么意思呢?简单来说,就是这个坑被人占了,那就接着去找空着的坑。