面向对象
1.JDK JRE JVM
jdk:(Java Development Kit)是Java的开发工具包,是整个java开发的核心。
jre:(Java Runtime Environment—java运行环境)。
JVM(Java Virtual Machine–java虚拟机)jvm是jre的一部分,jvm是虚拟出的一台计算机,这台计算机不关心java源代码如何写的,它只关心java源程序编译出的字节码文件,jvm是java程序能实现跨平台的核心,它不关心真实计算机,也不关心操作系统等等,就像一个中间平台,只负责将字节码文件转换成当前计算机能理解的CPU指令集或系统调用。
jvm有自己完善的硬件架构,如处理器、栈区、寄存器等。
总结:jvm是一个虚拟的中间平台,只负责将编译后的字节码文件转换成当前计算机能理解并执行的指令,其他都不关心。jvm是java“一次编译,到处执行”的原因。
我们利用JDK(通过调用一些Java API)写出java源程序,然后储存在.java文件中。
JDK中的源码编译器javac将源代码编译成java字节码,储存在.class文件中。
JRE加载、验证、执行Java字节码。
JVM将字节码文件解析为机器码映射到CPU指令集或者供系统调用
2.==和equals比较
“==”是运算符,如果是基本数据类型,则比较存储的值;如果是引用数据类型,则比较所指向对象的地址值。
equals是Object的方法,比较的是所指向的对象的地址值,一般情况下,重写之后比较的是对象的值。
3.hashCode与equals
规则一:如果 equals 比较对象的内容相等,那么 HashCode 一定相等。
eg: Integer i=97;
String s="a";
i.hashCode()= 97;
s.hashCode() =97;
规则二:如果 equals 比较对象的内容不相等,那么 HashCode 可能相等,也可能不相等。
4.final
(1) 基础: final修饰基本数据类型变量和引用数据类型变量
(2)使用final修饰方法有两个作用, 首要作用是锁定方法, 不让任何继承类对其进行修改
(3)使用final修饰类的目的简单明确: 表明这个类不能被继承.
当程序中有永远不会被继承的类时, 可以使用final关键字修饰
被final修饰的类所有成员方法都将被隐式修饰为final方法.
5.String、StringBuffer、StringBuilder
String | StringBuffer | StringBuilder |
---|---|---|
String是不可变的,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁 | StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。StringBuffer对象是一个字符序列可变的字符串,它没有重新生成一个对象,而且在原来的对象中可以连接新的字符串。 | StringBuilder类也代表可变字符串对象。实际上,StringBuilder和StringBuffer基本相似,两个类的构造器和方法也基本相同。不同的是:StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。 |
不可变(String a="123",再次赋值后 a="456",也就是说现在内存中存在"123","456"两个等待被回收的对象,) | 可变(StringBuilder sb = new StringBuilder("123"); sb.append("456");现在内存中存在"123456",一个等待被回收的对象) | 可变 |
线程安全 (StringBuffer类中的方法都添加了synchronized关键字,也就是给这个方法添加了一个锁,用来保证线程安全)可以不需要额外的同步用于多线程中 | 线程不安全,是非同步,运行于多线程中就需要使用着单独同步处理,但是速度就比StringBuffer快多了 | |
多线程操作字符串 | 单线程操作字符串 | |
如果要操作少量的数据用 String; | 多线程操作字符串缓冲区下操作大量数据 StringBuffer; | 单线程操作字符串缓冲区下操作大量数据 StringBuilder。 |
可以通过append、indert进行字符串的操作 | 可以通过append、indert进行字符串的操作 | |
实现了三个接口:Serializable、Comparable<String>、CarSequence | StringBuilder只实现了两个接口Serializable、CharSequence,相比之下String的实例可以通过compareTo方法进行比较,其他两个不可以。 | |
都是final类,不允许被继承 | 都是final类,不允许被继承 | 都是final类,不允许被继承 |
执行速度慢 | 执行速度中等 | 执行速度快 |
6.重载和重写的区别
重载(Overload)
在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。
public class Father {
public static void main(String[] args) {
// TODO Auto-generated method stub
Father s = new Father();
s.sayHello();
s.sayHello("wintershii");
}
public void sayHello() {
System.out.println("Hello");
}
public void sayHello(String name) {
System.out.println("Hello" + " " + name);
}
}
重写(Override)
从字面上看,重写就是 重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。
public class Father {
public static void main(String[] args) {
// TODO Auto-generated method stub
Son s = new Son();
s.sayHello();
}
public void sayHello() {
System.out.println("Hello");
}
}
class Son extends Father{
@Override
public void sayHello() {
// TODO Auto-generated method stub
System.out.println("hello by ");
}
}
①重载:在同一个类的内部同名、同参的方法、构造器,它们彼此之间构成重载
②重写:子类继承父类中的方法,并对其方法体进行修改的操作称为方法重写。
相同点:重载和重写的方法名都相同
不同点:
①重载的参数类型一定不相同,而重写的参数类型一定相同。
①重写的方法的返回值类型只能是父类类型或者父类类型的子类 , 而重载的方法对返回值类型没有要求
①重写发生在父子类中,重载发生在同一个类中
①子类重写方法的返回值类型不能大于父类被重写的方法的返回值类型,而重载没有返回值类型的限制
①子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限,而重载没有访问权限的限制
①子类不能重写父类中声明为private权限的方法,而重载是可以与private的方法彼此之间构成重载的
7.接口和抽象类的区别
1.实现方式:接口只能定义抽象方法和常量,不允许有实现代码;而抽象类可以包含抽象方法、普通方法和成员变量。
2.继承关系:一个类可以同时实现多个接口,但只能继承一个抽象类。
3.构造方法:接口没有构造方法,而抽象类可以有构造方法。
4.实例化:接口不能直接被实例化,需要通过实现类来实例化;而抽象类也不能直接被实例化,需要通过子类来实例化。
5.目的:接口主要用于定义规范,强制实现类遵循某种规范;抽象类主要用于代码复用和继承。
总之,接口和抽象类都是为了让程序员更好地组织和设计代码,提高代码的可读性和可维护性。在实际开发中,需要根据具体的需求来选择使用哪种方式。
8.List和Set的区别
1.元素的顺序:List中的元素是有序的,可以根据索引进行访问和操作;而Set中的元素是无序的,不能根据索引进行访问和操作。
2.元素的唯一性:List中的元素可以重复,可以添加多个相同的元素;而Set中的元素不能重复,只能添加一个相同的元素。
3.实现方式:List的实现类包括ArrayList、LinkedList、Vector等;而Set的实现类包括HashSet、TreeSet、LinkedHashSet等。
4.性能:List的查询操作效率较高,但插入和删除操作效率较低;而Set的插入、删除和查询操作效率都比List高。
总之,List和Set都有各自的优缺点,需要根据具体的需求来选择使用哪种集合类型。如果需要有序、可重复的集合,
可以选择List;如果需要无序、不可重复的集合,可以选择Set。
9.ArravList和LinkedList区别
1、ArrayList的实现是基于数组,LinkedList的实现是基于双向链表。
2、对于随机访问,ArrayList优于LinkedList
3、对于插入和删除操作,LinkedList优于ArrayList
4、LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
10.HashMap和HashTable有什么区别?其底层实现是什么?
HashMap 和 HashTable 都是 Java 中用于存储键值对的数据结构,它们的作用都是快速的查找、插入和删除元素。它们的主要区别如下:
1.线程安全性:HashTable 是线程安全的,而 HashMap 是非线程安全的。HashTable 内部的所有方法都被 synchronized 修饰,因此可以在多线程环境下安全使用。而HashMap则不是线程安全的,需要使用ConcurrentHashMap等并发容器来实现线程安全。
2.继承关系:HashTable 是 Dictionary 类的子类,而 HashMap 是 AbstractMap 类的子类。
3.null 值:HashTable 不允许键或值为 null,否则会抛出 NullPointerException 异常,而 HashMap 则允许键和值为 null。
4.底层实现:HashMap的底层实现是基于数组和链表或红黑树实现的,而Hashtable的底层实现是基于哈希表实现的。
5.迭代器:Hashtable的迭代器是通过Enumeration实现的,而HashMap的迭代器是通过Iterator实现的。Iterator相比Enumeration更加安全、迭代器可同时进行遍历和删除操作。
6.初始容量和扩容机制:Hashtable在创建时必须指定初始容量和负载因子,而HashMap则可以在创建时指定,如果不指定,则使用默认值。在数据量达到容量的负载因子时,Hashtable会自动扩容到原来容量的2倍,而HashMap则是扩容到原来容量的2倍。
HashMap 的底层实现是一个数组和链表结合的数据结构,称为链表散列。当发生 hash 冲突时,链表散列将采用链表的方式来解决冲突。而 HashTable 的底层实现也是数组和链表结合的数据结构,称为散列表,当发生 hash 冲突时,它采用开放地址法(Open Addressing)的方式来解决冲突。
需要注意的是,HashMap 在 JDK 1.8 中进行了优化,当链表长度大于阈值(8)时,链表会自动转化为红黑树,以提高查找效率。此外,HashMap 还引入了一些新的实现方式,例如桶的数量不再固定,而是可以动态调整的。
HashMap的底层实现是基于数组和链表/红黑树实现的,具体来说,HashMap中有一个Entry数组,每个Entry对象中存储着一个key-value键值对,如果多个Entry的hash值相同,它们就会被存储在同一个链表中,当链表长度超过一定阈值时,链表会被转换为红黑树以提高查找效率。HashMap使用了哈希表的思想,因此查找元素的时间复杂度为O(1)。
HashTable的底层实现也是基于数组和链表实现的,但是没有使用红黑树,因此它的查找效率较低。同时,由于Hashtable需要保证线程安全性,因此它的put/get操作需要加锁,导致性能较低。
11.ConcurrentHashMap原理,idk7和idk8版本的区别 +
ConcurrentHashMap 是 Java 并发包中提供的一个线程安全的哈希表实现。它支持高并发的读和写操作,并且比 Hashtable 和同步的 HashMap 在多线程环境下拥有更好的性能。
ConcurrentHashMap 的实现采用了分段锁的机制。在 Java 7 中,ConcurrentHashMap 使用了一个叫做 Segment 的类来划分桶(bucket),每个 Segment 都相当于一个小的哈希表,它包含了多个桶,每个桶又是一个链表。Segment 内部使用了 ReentrantLock 锁,不同 Segment 之间并不会相互影响,因此多线程访问不同的 Segment 时不会出现竞争。
在 Java 8 中,ConcurrentHashMap 的实现发生了变化,它摒弃了之前使用 Segment 的分段锁机制,采用了 CAS(Compare and Swap)和 synchronized 来保证并发安全。具体来说,ConcurrentHashMap 在内部维护了一个基于链表和红黑树(Java 8 中引入的一种自平衡的二叉查找树)的数据结构,当链表长度超过阈值(8)时,链表就会转换成红黑树,以提高查找效率。并且,Java 8 中的 ConcurrentHashMap 也取消了之前的 sizeCtl 字段,取而代之的是 baseCount 和 cellsBusy 字段来实现并发更新。
Java 8 中的 ConcurrentHashMap 还引入了一些新的方法和功能,例如 forEach() 方法和对 reduce() 方法的支持,使得对 ConcurrentHashMap 进行迭代和聚合操作更加方便。此外,Java 8 还提供了一些新的工厂方法来创建 ConcurrentHashMap 实例,例如 newKeySet() 和 newKeySet(int) 等。
总之,Java 7 和 Java 8 中的 ConcurrentHashMap 实现有很大的不同,Java 8 中的实现更加高效和安全,并且提供了更多的功能和方法。
12.什么是字节码?采用字节码的好处是什么?
字节码(Bytecode)是Java编译器编译Java源代码后生成的一种中间形式,也是Java程序在跨平台上实现可移植性的关键所在。
Java源代码被编译器编译成字节码文件,而不是与硬件和操作系统相关的本地机器代码。这些字节码可以在Java虚拟机(JVM)上运行,使得Java程序具有跨平台性。这种跨平台性是因为Java虚拟机本身是与平台无关的,而且在不同的平台上都有相同的实现,所以只要有Java虚拟机,就可以运行Java程序,而不需要重新编译。
采用字节码的好处是:
1.跨平台性:Java字节码可以在任何装有Java虚拟机的平台上运行,保证了Java程序的可移植性。
2.安全性:Java字节码在执行时,通过Java虚拟机的安全检查机制进行访问控制,可以有效防止恶意代码的攻击,增强了Java程序的安全性。
3.优化性:Java字节码可以在运行时进行优化和解释执行,可以根据不同平台的实际情况对Java程序进行优化,提高程序的性能。
4.加载性:Java字节码在加载时,可以进行类的动态加载,可以动态扩展程序的功能。
总的来说,采用字节码可以使Java程序具有跨平台性、安全性、优化性和加载性等优势,使得Java程序更加灵活和可靠。
13.Java中的异常体系
Java中的异常体系分为Throwable、Error和Exception三个主要类别。
1.Throwable
Throwable是所有异常的基类,它有两个子类:Error和Exception。Error表示系统级错误,一般是指JVM无法解决的问题,例如OutOfMemoryError、StackOverflowError等,这些错误不应该被捕获和处理。Exception表示应用程序级别的异常,它又有两个子类:RuntimeException和Checked Exception。
2.RuntimeException
RuntimeException表示运行时异常,这些异常不需要显式地捕获和处理,Java虚拟机会自动捕获并抛出。常见的RuntimeException包括NullPointerException、IllegalArgumentException、IndexOutOfBoundsException等。
3.Checked Exception
Checked Exception是指在编译时就能够被发现的异常,这些异常必须显式地在方法签名中声明,或者在方法内部进行捕获和处理,否则编译不会通过。常见的Checked Exception包括IOException、SQLException、ClassNotFoundException等。
在Java中,异常处理通常使用try-catch-finally语句块来完成,try块中包含可能抛出异常的代码,catch块用来捕获和处理异常,finally块用来释放资源和完成清理工作。使用异常可以使代码更加健壮和可靠,提高程序的可维护性和可靠性。
14.Java类加载器
Java类加载器(Class Loader)是Java虚拟机(JVM)的一个重要组成部分,它负责将类加载到JVM中,并在运行时动态加载和卸载类。Java类加载器主要有三个层次:启动类加载器、扩展类加载器和应用程序类加载器。
1.启动类加载器
启动类加载器是JVM自带的类加载器,它负责加载Java核心类库,如java.lang、java.util等,它是JVM自身的一部分,由C++编写,无法被Java程序直接引用。
2.扩展类加载器
扩展类加载器负责加载Java扩展库,位于JRE的lib/ext目录下,它通过Java语言实现,可以被Java程序直接引用。
3.应用程序类加载器
应用程序类加载器(也称为系统类加载器)负责加载应用程序中的类,它是Java程序中最常用的类加载器,也是默认的类加载器。应用程序类加载器在启动时会从CLASSPATH环境变量指定的路径中查找类,或者在Java虚拟机启动时通过命令行参数指定的路径中查找类。
Java类加载器具有如下特点:
1.双亲委派模型:Java类加载器采用了双亲委派模型,即在类加载时先向上委派给父类加载器进行加载,只有在父类加载器无法加载时才会由当前类加载器进行加载。这种机制可以有效避免类的重复加载和安全问题。
2.动态加载:Java类加载器可以在运行时动态加载和卸载类,提高了程序的灵活性和可扩展性。
3.命名空间:每个类加载器都有自己的命名空间,同一个类在不同的类加载器中被加载,会被视为两个不同的类。
15.双亲委托模型
双亲委托模型是Java类加载器的一种工作机制,它定义了类加载器的层次关系以及类加载的流程。该模型的核心思想是在类加载的过程中,一个类加载器在尝试加载某个类时,会首先将加载请求委托给它的父类加载器去完成,直到最终委托给顶层的启动类加载器。如果父类加载器无法加载该类,才会由当前类加载器自己尝试加载。
双亲委托模型具有如下优点:
1.避免重复加载:由于父类加载器的存在,可以避免重复加载某个类。如果一个类已经被父类加载器加载过了,那么子类加载器就没有必要再加载一次。
2.安全性:由于父类加载器先于子类加载器加载类,因此可以确保核心类库不会被篡改,保证了Java程序的安全性。
3.可靠性:由于父类加载器先于子类加载器加载类,因此可以确保核心类库不会被篡改,保证了Java程序的可靠性。
在Java中,双亲委托模型的具体实现是:所有的类加载器都是从java.lang.ClassLoader类继承而来的,在加载类时,ClassLoader会首先判断该类是否已经被加载过了,如果已经被加载过,则直接返回已经加载的类;否则,将该加载请求委托给其父类加载器去完成加载,直到委托到顶层的启动类加载器为止。如果所有的父类加载器都无法完成加载,那么才由当前类加载器自己尝试加载。
16.GC如何判断对象可以被回收
在 Java 中,垃圾收集器(Garbage Collector)主要通过两种方式来判断对象是否可以被回收:
1.引用计数法:每个对象有一个引用计数器,当有一个新的引用指向这个对象时,计数器加 1,当引用失效时,计数器减 1。当计数器的值为 0 时,说明该对象不再被引用,可以被回收。但是,这种方式无法解决循环引用的问题,即 A 对象引用了 B 对象,B 对象也引用了 A 对象,导致计数器始终不为 0,无法回收这些对象。
2.可达性分析法:这是目前主流的垃圾收集算法。该算法的基本思想是,通过一系列称为 GC Roots 的根对象作为起点,遍历整个对象图,将所有与 GC Roots 不可达的对象标记为不可用对象,最后回收这些不可用对象。在 Java 中,GC Roots 主要包括以下几种:
..虚拟机栈(栈帧中的本地变量表)中引用的对象。
..方法区中类静态属性引用的对象。
..方法区中常量引用的对象。
..本地方法栈中 JNI(Java Native Interface)引用的对象。
在可达性分析法中,对象从 GC Roots 开始遍历,遇到一个对象就将其标记为已访问,并继续遍历该对象引用的所有对象,直到遍历完所有可达对象。未被标记的对象就是不可达对象,可以被回收。
需要注意的是,这种算法有一个缺点,就是在遍历对象图的过程中,需要停止应用程序的所有线程,这会导致一定的停顿时间。因此,现代的垃圾收集器都采用了一些优化手段,如增量标记、分代收集等,以降低停顿时间,提高程序的性能。
线程、并发相关
1.线程的生命周期?线程有几种状态
Java 中的线程生命周期(Thread Lifecycle)指的是线程从创建到销毁的整个过程,它包括以下几个阶段:
1.新建状态(New):当一个线程对象被创建后,它就处于新建状态。此时,它还没有开始执行,没有分配系统资源。
2.就绪状态(Runnable):当线程调用 start() 方法后,它进入就绪状态。此时,它已经分配了系统资源,可以开始执行,但是还没有得到 CPU 时间片。
3.运行状态(Running):当线程获得 CPU 时间片后,就进入运行状态。此时,线程开始执行 run() 方法中的代码。
4.阻塞状态(Blocked):当线程在某些条件下无法继续执行时,就进入阻塞状态。比如等待某个输入、输出操作完成、等待某个锁的释放、等待其他线程通知等。
5.等待状态(Waiting):当线程调用 wait() 方法时,它就进入等待状态。在等待期间,线程会释放它所持有的锁,并等待其他线程通过 notify() 或 notifyAll() 方法将其唤醒。
6.定时等待状态(Timed Waiting):当线程调用 sleep()、join() 或 wait(timeout) 方法时,它就进入定时等待状态。这个状态和等待状态类似,不同之处在于它会在指定的时间后 自动唤醒。
7.终止状态(Terminated):当线程完成 run() 方法中的代码或者发生了未捕获的异常时,就进入终止状态。此时,线程所占用的资源会被释放。
需要注意的是,线程的状态不是固定不变的,它会随着时间和线程的执行情况而不断变化。线程状态的变化是由线程调度器决定的,它会根据一定的算法来决定哪个线程可以获得 CPU 时间片,哪个线程需要等待,以及哪个线程应该被唤醒等。
2.sleep()、wait()、join()、yield()的区别
这四个方法都是用于线程控制的方法,但它们的作用不同,具体区别如下:
sleep() 方法:使当前线程暂停一段时间,让出 CPU 时间片,但不会释放锁。可以用于实现简单的定时器功能。
wait() 方法:使当前线程暂停,释放对象锁,并进入等待队列,直到被唤醒(即其他线程调用相同对象的 notify() 或 notifyAll() 方法)或等待时间超时。
join() 方法:使当前线程暂停,等待另一个线程执行完毕。如果传入参数,则等待指定的时间,或者等待另一个线程无限期执行下去。
yield() 方法:使当前线程暂停,让出 CPU 时间片,但不会释放锁。与 sleep() 方法不同的是,yield() 方法只是让出 CPU 时间片,而不是暂停一段时间。
需要注意的是,wait()、notify() 和 notifyAll() 方法只能在同步块或同步方法中使用,因为它们涉及到锁的获取和释放。而 sleep()、join() 和 yield() 方法可以在任何地方使用。
此外,这些方法还有一些其他的区别,例如:
1.wait() 方法是 Object 类中的方法,而 sleep() 和 yield() 方法是 Thread 类中的方法。
2.wait() 方法必须在同步块或同步方法中使用,并且必须在调用该方法前获得对象锁,否则会抛出 IllegalMonitorStateException 异常。
3.join() 方法必须在其他线程上调用,而 sleep()、wait() 和 yield() 方法可以在当前线程上调用。
4.sleep() 方法抛出 InterruptedException 异常,需要进行捕获或抛出,而其他方法不会抛出该异常。
5.yield() 方法不能保证让出 CPU 时间片,因为它只是向线程调度器建议让出 CPU 时间片,但是具体是否让出由线程调度器决定。
3.对线程安全的理解
线程安全是指在多线程环境下,对共享资源的访问不会引起不正确的结果或不一致的状态。简单来说,线程安全就是指在多线程并发访问时,保证程序的正确性和稳定性。
在多线程环境下,线程访问共享资源可能会出现以下问题:
1.资源竞争:多个线程同时访问同一共享资源,可能会导致数据不一致、覆盖等问题。
2.数据不一致:由于多个线程并发访问同一共享资源,可能会导致数据不一致的问题,如读脏数据、写丢数据等。
3.死锁:当多个线程相互依赖,每个线程都在等待其他线程释放资源时,就可能发生死锁。
为了解决这些问题,需要采取一些措施来保证线程安全,例如:
1.加锁:使用 synchronized 或 Lock 等机制来保证同一时刻只有一个线程能够访问共享资源。
2.原子操作:使用 AtomicInteger、ConcurrentHashMap 等线程安全的类来实现原子操作,避免资源竞争问题。
3.不可变对象:使用不可变对象来避免多线程修改同一共享对象的问题。
4.并发容器:使用线程安全的容器类,如 ConcurrentHashMap、ConcurrentLinkedQueue 等。
需要注意的是,线程安全并不是绝对的,只是在一定程度上降低了出现问题的概率。在使用多线程编程时,还需要注意编写高质量的代码、避免锁的粒度过大、减少线程间的依赖等。
4.Thread、Runable的区别
Thread 和 Runnable 都是 Java 中用于实现多线程的接口,它们的主要区别在于继承方式和代码结构。
1.继承方式:
Thread 类实现了 Runnable 接口,因此可以通过继承 Thread 类来创建新线程。在使用 Thread 类时,可以重写 run() 方法来定义线程的执行逻辑。
Runnable 接口则是一个单独的接口,它没有实现线程本身,而是通过定义 run() 方法来实现线程执行逻辑。在使用 Runnable 接口时,需要创建一个新的 Thread 对象,并将 Runnable 对象作为参数传递给 Thread 的构造函数。
2.代码结构:
通过继承 Thread 类创建新线程,需要在子类中重写 run() 方法,并在该方法中定义线程的执行逻辑。这种方式的优点是代码结构相对简单,缺点是无法继承其他类,不够灵活。
使用 Runnable 接口则需要将线程的执行逻辑封装在一个独立的类中,然后创建一个新的 Thread 对象,并将该类的实例作为参数传递给 Thread 的构造函数。这种方式的优点是能够继承其他类,灵活性较高,缺点是代码结构相对复杂。
因此,一般建议使用 Runnable 接口来实现多线程,这样可以避免 Java 单继承的限制,使代码更加灵活、可扩展。同时,使用 Runnable 接口还可以实现线程池和线程复用等功能。而 Thread 类则通常用于直接启动线程或创建一些简单的线程。
5.对守护线程的理解
守护线程(Daemon Thread)是一种特殊的线程,它的作用是为其他线程提供服务。与普通线程不同的是,当所有非守护线程结束时,守护线程也会自动退出。
在 Java 中,可以通过 Thread.setDaemon(true) 方法将一个线程设置为守护线程。例如,GC 线程就是一种守护线程,它在 JVM 启动时自动启动,并且当所有非守护线程结束时自动退出。
守护线程通常用于在后台执行某些任务,如定时扫描某个目录、发送心跳包等。它们通常不需要占用过多的系统资源,但也不应该执行长时间的任务,因为当所有非守护线程结束时,它们会被强制退出,这可能会导致未完成的任务中断或出现异常。
需要注意的是,守护线程不能访问一些必要的系统资源,如文件系统、网络连接等。因为当所有非守护线程结束时,这些资源可能已经被关闭或释放,守护线程访问这些资源可能会出现异常。因此,在编写守护线程时,需要仔细考虑其访问的资源和执行的任务,确保其安全可靠。
6.ThreadLocal的原理和使用场景
ThreadLocal 是 Java 中的一个线程本地变量工具类,它的作用是为每个线程维护一个独立的变量副本,使得每个线程之间可以独立地访问自己的变量副本,从而避免了线程安全问题。
ThreadLocal 的原理是通过为每个线程创建一个独立的变量副本来实现线程间的隔离。当一个线程访问 ThreadLocal 变量时,它实际上是访问自己的变量副本,而不是共享的变量。每个线程的变量副本都存储在 Thread 对象中,当线程结束时,变量副本也会随之销毁。
ThreadLocal 的使用场景包括:
1.在多线程环境下,为每个线程创建独立的变量副本,以避免线程安全问题。
2.在一些需要跨层传递的上下文信息中,如 Web 应用中的用户登录信息、请求 ID 等,可以使用 ThreadLocal 将这些信息存储在当前线程的变量副本中,使得整个请求处理过程中都可以方便地访问这些信息。
3.在一些需要动态控制的场景中,如日志级别、语言环境等,可以使用 ThreadLocal 将这些信息存储在当前线程的变量副本中,以便于动态控制。
需要注意的是,由于 ThreadLocal 会为每个线程创建一个变量副本,因此在一些长时间运行的程序中,可能会导致内存泄漏问题。因此,在使用 ThreadLocal 时,需要注意及时清理不再使用的变量副本,以避免出现内存泄漏。一般可以通过调用 ThreadLocal.remove() 方法来清理变量副本。
7.ThreadLocal内存泄露原因,如何避免
ThreadLocal 内存泄漏的原因是因为在使用完 ThreadLocal 变量之后,没有及时调用 remove() 方法进行清理,导致 ThreadLocal 对象仍然保留着对这些变量的引用,从而使得变量副本不能被回收,最终导致内存泄漏。
为了避免 ThreadLocal 内存泄漏,一般可以通过以下两种方式来解决:
1.使用 try-finally 块进行清理
在使用 ThreadLocal 变量时,可以使用 try-finally 块将其包裹起来,并在 finally 块中调用 remove() 方法进行清理,以确保变量副本在使用完毕后能够被及时清理。例如:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
try {
threadLocal.set("value");
// do something
} finally {
threadLocal.remove();
}
2.使用 InheritableThreadLocal 变量
InheritableThreadLocal 是 ThreadLocal 的一个子类,它可以让子线程继承父线程中的变量副本,从而避免了子线程需要单独创建变量副本的问题。在使用 InheritableThreadLocal 时,需要注意变量副本的作用范围,以免造成数据混乱或泄漏。例如:
InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
inheritableThreadLocal.set("value");
new Thread(() -> {
String value = inheritableThreadLocal.get();
// do something with value
}).start();
8.并发、并行、串行的区别
在计算机领域,常常提到并发、并行和串行这三个概念,它们表示了不同的计算机处理方式,具体如下:
1.串行(Sequential)
串行指的是一次只执行一个任务的过程,按照一定的顺序依次执行任务,每个任务必须等待前一个任务完成后才能开始执行。串行的执行方式简单,适用于一些简单的任务。
2.并发(Concurrent)
并发指多个任务交替执行的过程,多个任务之间可能存在交互和竞争。在并发模型中,处理器不断地切换执行不同的任务,从外部来看,多个任务好像同时在执行。并发可以提高程序的吞吐量,但是需要考虑多线程并发时的数据同步和竞争问题,从而避免出现线程安全问题。
3.并行(Parallel)
并行指多个任务同时执行的过程,多个任务之间互不影响。在多处理器计算机上,可以使用多个处理器同时执行多个任务,从而提高系统的执行效率。并行的执行方式通常需要对任务进行划分和分配,以确保每个处理器都有足够的任务可执行。
总的来说,并发和并行都是提高程序执行效率的方式,但是它们的实现方式不同。并发通常使用多线程技术实现,而并行通常使用多处理器技术实现。在多线程编程中,需要注意并发和并行的区别,以及如何在多线程环境下处理并发问题,避免出现线程安全问题。
网友评论