Java 虚拟机是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件。
Java 被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。
Java 虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
JDK
:Java 开发工具包,包含了 JRE、编译器和其它工具(如:javadoc、Java 调试器)。
JRE
:Java 运行环境,包含 Java 虚拟机和 Java 程序所需的核心类库。
如果只是想跑 Java 程序,那么只需安装 JRE,如果要写 Java 程序并且运行,那就需要 JDK 了。
static
关键字是什么意思?Java 中是否可以覆盖一个 private
或者是 static
的方法?如果一个类的变量或者方法前面有 static
修饰,那么表明这个方法或者变量属于这个类,也就是说可以在不创建对象的情况下直接使用。
当父类的方法被 private
修饰时,表明该方法为父类私有,对其他任何类都是不可见的,因此如果子类定了一个与父类一样的方法,这对于子类来说相当于是一个新的私有方法,且如果要进行向上转型,然后去调用该“覆盖方法”,会产生编译错误:
class Parent {
private fun() {
}
}
class Child extends Parent {
private fun() {
}
}
class Test {
public static void main(String[] args) {
Parent c = new Child();
c.fun(); // 编译出错
}
}
static
方法是编译时静态绑定的,属于类,而覆盖是运行时动态绑定的(动态绑定的多态),因此不能覆盖。
Java 支持的基本数据类型有以下 9 种:byte
、shot
、int
、long
、float
、double
、char
、boolean
、void
。
自动拆装箱是 Java 从 jdk1.5 引用,目的是将原始类型自动的装换为相对应的对象,也可以逆向进行,即拆箱。这也体现 Java 中一切皆对象的宗旨。
所谓自动装箱就是将原始类型自动的转换为对应的对象,而拆箱就是将对象类型转换为基本类型。Java 中的自动拆装箱通常发生在变量赋值的过程中,如:
Integer object = 3; // 自动装箱
int o = object; // 拆箱
在 Java 中,应该注意自动拆装箱,因为有时可能因为 Java 自动装箱机制,而导致创建了许多对象,对于内存小的平台会造成压力。
覆盖也叫重写,发生在子类与父类之间,表示子类中的方法可以与父类中的某个方法的名称和参数完全相同,通过子类创建的实例对象调用这个方法时,将调用子类中的定义方法,这相当于把父类中定义的那个完全相同的方法给覆盖了,这也是面向对象编程的多态性的一种表现。
重载是指在一个类中,可以有多个相同名称的方法,但是他们的参数列表的个数或类型不同,当调用该方法时,根据传递的参数类型调用对应参数列表的方法。当参数列表相同但返回值不同时,将会出现编译错误,这并不是重载,因为 jvm 无法根据返回值类型来判断应该调用哪个方法。
在 Java 中是单继承的,也就是说一个类只能继承一个父类。
Java 中实现多继承有两种方式,一是接口,二是内部类。
// 实现多个接口 如果两个接口的变量相同 那么在调用该变量的时候 编译出错
interface Interface1 {
static String field = "dd";
public void fun1();
}
interface Interface2 {
static String field = "dddd";
public void fun2();
}
class child implements Interface1, Interface2 {
static String field = "dddd";
@Override
public void fun2() {
}
@Override
public void fun1() {
}
}
// 内部类,间接多继承
class Child {
class Father {
private void strong() {
System.out.println("父类");
}
}
class Mother {
public void getCute() {
System.out.println("母亲");
}
}
public void getStrong() {
Father f = new Father();
f.strong();
}
public void getCute() {
Mother m = new Mother();
m.getCute();
}
}
值传递 就是在方法调用的时候,实参是将自己的一份拷贝赋给形参,在方法内,对该参数值的修改不影响原来实参,常见的例子就是刚开始学习 C 语言的时候那个交换方法的例子了。
引用传递 是在方法调用的时候,实参将自己的地址传递给形参,此时方法内对该参数值的改变,就是对该实参的实际操作。
在 Java 中只有一种传递方式,那就是值传递。可能比较让人迷惑的就是 Java 中的对象传递时,对形参的改变依然会影响到该对象的内容。
下面这个例子来说明 Java 中是值传递。
public class Test {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer("hello ");
getString(sb);
System.out.println(sb);
}
public static void getString(StringBuffer s) {
// s = new StringBuffer("ha");
s.append("world");
}
}
在上面这个例子中,当前输出结果为 hello world
。这并没有什么问题,可能就是大家平常所理解的引用传递,那么当然会改变 StringBuffer
的内容。
但是如果把上面的注释去掉,那么就会输出 hello
。此时 sb
的值并没有变成 ha hello
。假如说是引用传递的话,那么形参的 s
也就是 sb
的地址,此时在方法里 new StringBuffer()
,并将该对象赋给 s
,也就是说 s
现在指向了这个新创建的对象。
按照引用传递的说法,此时对 s
的改变就是对 sb
的操作,也就是说 sb
应该也指向新创建的对象,那么输出的结果应该为 ha world
。但实际上输出的仅是 hello
。这说明 sb
指向的还是原来的对象,而形参 s
指向的才是创建的对象,这也就验证了 Java 中的对象传递也是值传递。
final
的。抽象类可以包含非 final
的变量;public
的。抽象类的成员函数可以是 private
、protected
或者是 public
;main
方法的话是可以被调用的。构造方法是不能被子类重写的,但是构造方法可以重载,也就是说一个类可以有多个构造方法。
Math.round(11.5)
等于多少? Math.round(-11.5)
等于多少?Math.round(11.5) == 12
Math.round(-11.5) == -11
round
方法返回与参数最接近的长整数,参数加 0.5
后求其 floor
。
String
、StringBuffer
和 StringBuilder
的区别。String
的长度是不可变的;StringBuffer
的长度是可变的,如果你对字符串中的内容经常进行操作,特别是内容要修改时,那么使用 StringBuffer
,如果最后需要 String
,那么使用 StringBuffer
的 toString()
方法;线程安全;StringBuilder
是从 JDK 5 开始为 StringBuffer
类补充了一个单个线程使用的等价类;通常应该优先使用 StringBuilder
类,因为它支持所有相同的操作,但由于它不执行同步,所以速度更快。使用字符串的时候要特别小心,如果对一个字符串要经常改变的话,就一定不要用 String
,否则会创建许多无用的对象出来。
来看一下比较:
String s = "hello" + "world" + "i love you";
StringBuffer sb = new StringBuilder("hello").append("world").append("i love you");
这个时候 s
有多个字符串进行拼接,按理来说会有多个对象产生,但是 JVM 会对此进行一个优化,也就是说只创建了一个对象,此时它的执行速度要比 StringBuffer
拼接快。再看下面这个:
String s2 = "hello";
String s3 = "world";
String s4 = "i love you";
String s1 = s2 + s3 + s4;
上面这种情况,就会多创建出来三个对象,造成了内存空间的浪费。
方法区:
虚拟机栈:
returnAddress
类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定;本地方法栈
本地方法栈和虚拟机栈类似,只不过本地方法栈为 Native 方法服务。
堆
Java 堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。
程序计数器
内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个 Java 虚拟机规范没有规定任何 OOM 情况的区域。
判断一个对象是否存活有两种方法:
引用计数法
所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收。
引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对象 B,对象 B 又引用者对象 A,那么此时 A,B 对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
可达性算法(引用链法)
该算法的思想是:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。
在 Java 中可以作为 GC Roots 的对象有以下几种:
虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象比不一定会被回收。
当一个对象不可达 GC Root 时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记。
如果对象在可达性分析中没有与 GC Root 的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行 finalize()
方法。当对象没有覆盖 finalize()
方法或者已被虚拟机调用过,那么就认为是没必要的。
如果该对象有必要执行 finalize()
方法,那么这个对象将会放在一个称为 F-Queue 的对队列中,虚拟机会触发一个 Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果 finalize()
执行缓慢或者发生了死锁,那么就会造成 F-Queue 队列一直等待,造成了内存回收系统的崩溃。GC 对处于 F-Queue 中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。
在 Java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。
在 JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
标记-清除
这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:
复制算法
为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一般的内存。
于是将该算法进行了改进,内存区域不再是按照 1:1 去划分,而是将内存划分为 8:1:1 三部分,较大那份内存交 Eden
区,其余是两块较小的内存区叫 Survior
区。每次都会优先使用 Eden
区,若 Eden
区满,就将对象复制到第二块内存区上,然后清除 Eden
区,如果此时存活的对象太多,以至于 Survivor
不够时,会将这些对象通过分配担保机制复制到老年代中(Java 堆又分为新生代和老年代)。
标记-整理
该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。
分代收集
现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。
Java 内存模型(JMM)是线程间通信的控制机制。JMM 定义了主内存和线程之间抽象关系。线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。Java 内存模型的抽象示意图如下:
从上图来看,线程 A 与线程 B 之间如要通信的话,必须要经历下面 2 个步骤:
Java 类加载需要经历以下几个过程:
加载
加载时类加载的第一个过程,在这个阶段,将完成一下三件事情:
验证
验证的目的是为了确保 Class 文件的字节流中的信息不回危害到虚拟机。在该阶段主要完成以下四钟验证:
准备
准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。
// 在准备阶段 value 初始值为 0,在初始化阶段才会变为 123
public static int value = 123;
解析
该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。
初始化
初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。主要有以下 4 种类加载器:
启动类加载器
(Bootstrap ClassLoader):用来加载 Java 核心类库,无法被 Java 程序直接引用;扩展类加载器
(Extensions ClassLoader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类;系统类加载器
(System ClassLoader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()
来获取它;用户自定义类加载器
:通过继承 java.lang.ClassLoader
类的方式实现。Java 内存分配与回收策率:
当 Eden 区没有足够的空间进行分配时,虚拟机会执行一次 Minor GC。Minor GC 通常发生在新生代的 Eden 区,在这个区的对象生存期短,往往发生 GC 的频率较高,回收速度比较快;Full GC/Major GC 发生在老年代,一般情况下,触发老年代 GC 的时候不会触发 Minor GC,但是通过配置,可以在 Full GC 之前进行一次 Minor GC 这样可以加快老年代的回收速度。
HashMap
的工作原理是什么?HashMap
内部是通过一个数组实现的,只是这个数组比较特殊,数组里存储的元素是一个 Entry
实体(JDK 8 为 Node
),这个 Entry
实体主要包含 key
、value
以及一个指向自身的 next
指针。
HashMap
是基于 hashing
实现的,当我们进行 put 操作时,根据传递的 key 值得到它的 hashcode
,然后再用这个 hashcode
与数组的长度进行模运算,得到一个 int
值,就是 Entry
要存储在数组的位置(下标);当通过 get
方法获取指定 key
的值时,会根据这个 key
算出它的 hash
值(数组下标),根据这个 hash
值获取数组下标对应的 Entry
,然后判断 Entry
里的 key
,hash
值或者通过 equals()
比较是否与要查找的相同,如果相同,返回 value
,否则遍历该链表(有可能就只有一个 Entry
,此时直接返回 null
),直到找到为止,否则返回 null
。
HashMap
之所以在每个数组元素存储的是一个链表,是为了解决 hash
冲突问题,当两个对象的 hash
值相等时,那么一个位置肯定是放不下两个值的,于是 HashMap
采用链表来解决这种冲突,hash
值相等的两个元素会形成一个链表。
HashMap
与 HashTable
的区别是什么?HashTable
基于 Dictionary
类,而 HashMap
是基于 AbstractMap
。Dictionary
是任何可将键映射到相应值的类的抽象父类,而 AbstractMap
是基于 Map 接口的实现,它以最大限度地减少实现此接口所需的工作。(在 Java 8 中 HashTable
并没有继承 Dictionary
,而且里面也没有同步方法);
HashMap
的 key
和 value
都允许为 null
,而 HashTable
的 key
和 value
都不允许为 null
。HashMap
遇到 key
为 null
的时候,调用 putForNullKey
方法进行处理,而对 value
没有处理;HashTable
遇到 null
,直接返回 NullPointerException
;
HashTable
是同步的,而 HashMap
是非同步的,但是也可以通过 Collections.synchronizedMap(hashMap)
使其实现同步。
CorrentHashMap
的工作原理?JDK 1.6 版:
ConcurrenHashMap
可以说是 HashMap
的升级版,ConcurrentHashMap
是线程安全的,但是与 HashTable
相比实现线程安全的方式不同。HashTable
是通过对 hash
表结构进行锁定,是阻塞式的,当一个线程占有这个锁时,其他线程必须阻塞等待其释放锁。ConcurrentHashMap
是采用分离锁的方式,它并没有对整个 hash
表进行锁定,而是局部锁定,也就是说当一个线程占有这个局部锁时,不影响其他线程对 hash
表其他地方的访问。
具体实现:ConcurrentHashMap
内部有一个 Segment
数组,该 Segment
对象可以充当锁。Segment
对象内部有一个 HashEntry
数组,于是每个 Segment
可以守护若干个桶(HashEntry
),每个桶又有可能是一个 HashEntry
连接起来的链表,存储发生碰撞的元素。
每个 ConcurrentHashMap
在默认并发级下会创建包含 16 个 Segment
对象的数组,每个数组有若干个桶,当我们进行 put
方法时,通过 hash
方法对 key
进行计算,得到 hash
值,找到对应的 segment
,然后对该 segment
进行加锁,然后调用 segment
的 put
方法进行存储操作,此时其他线程就不能访问当前的 segment
,但可以访问其他的 segment
对象,不会发生阻塞等待。
JDK 1.8 版
ConcurrentHashMap
不再使用 Segment
分离锁,而是采用一种乐观锁 CAS
算法来实现同步问题,但其底层还是“数组 + 链表-红黑树”的实现。
List
有哪些不同的方式? List strList = new ArrayList<>();
// for-each
for(String str : strList) {
System.out.print(str);
}
// iterator:尽量使用这种,更安全(fail-fast)
Iterator it = strList.iterator();
while(it.hasNext) {
System.out.printf(it.next());
}
Iterator 的 fail-fast 属性与当前的集合共同起作用,因此它不会受到集合中任何改动的影响。Java.util 包中的所有集合类都被设计为 fail->fast 的,而 java.util.concurrent 中的集合类都为 fail-safe 的。当检测到正在遍历的集合的结构被改变时,Fail-fast 迭代器抛出 ConcurrentModificationException,而 fail-safe 迭代器从不抛出 ConcurrentModificationException。
Array
和 ArrayList
有何区别?什么时候更适合用 Array
?Array
可以容纳基本类型和对象,而 ArrayList
只能容纳对象;Array
是指定大小的,而 ArrayList
大小是固定的。ArrayList
、HashMap
、TreeMap
和 HashTable
类提供对元素的随机访问。
HashSet
的底层实现是什么?通过看源码知道 HashSet
的实现是依赖于 HashMap
的,HashSet
的值都是存储在 HashMap
中的。在 HashSet
的构造法中会初始化一个 HashMap
对象,HashSet
不允许值重复,因此 HashSet
的值是作为 HashMap
的 key
存储在 HashMap
中的,当存储的值已经存在时返回 false
。
LinkedHashMap
的实现原理?LinkedHashMap
也是基于 HashMap
实现的,不同的是它定义了一个 Entry
header,这个 header
不是放在 Table
里,它是额外独立出来的。LinkedHashMap
通过继承 HashMap
中的 Entry
,并添加两个属性 Entry
before
,after
,和 header
结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。LinkedHashMap
定义了排序模式 accessOrder
,该属性为 boolean
型变量,对于访问顺序,为 true
;对于插入顺序,则为 false
。一般情况下,不必指定排序模式,其迭代顺序即为默认为插入顺序。
ArrayList
是基于数组实现,LinkedList
是基于链表实现;ArrayList
在查找时速度快,LinkedList
在插入与删除时更具优势。线程可定义为进程内的一个执行单位,或者定义为进程内的一个可调度实体。在具有多线程机制的操作系统中,处理机调度的基本单位不是进程而是线程。一个进程可以有多个线程,而且至少有一个可执行线程。
比如,进程好比工厂(计算机)里的车间,一个工厂里有多个车间(进程)在运转,每个车间里有多个工人(线程)在协同工作,这些工人就可以理解为线程。
线程和进程的关系:
start()
和 run()
方法有什么区别?start()
方法被用来启动新创建的线程,而且 start()
内部调用了 run()
方法,这和直接调用 run()
方法的效果不一样。当调用 run()
方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()
方法才会启动新线程。
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为。
线程安全的核心是“正确性”,也就是说当多个线程访问某个类时,能够得到预期的结果,那么就是线程安全的。
自旋锁
自旋锁在 JDK1.6 之后就默认开启了。基于之前的观察,共享数据的锁定状态只会持续很短的时间,为了这一小段时间而去挂起和恢复线程有点浪费,所以这里就做了一个处理,让后面请求锁的那个线程在稍等一会,但是不放弃处理器的执行时间,看看持有锁的线程能否快速释放。为了让线程等待,所以需要让线程执行一个忙循环也就是自旋操作。
在 JDK1.6 之后,引入了自适应的自旋锁,也就是等待的时间不再固定了,而是由上一次在同一个锁上的自旋时间及锁的拥有者状态来决定
偏向锁
在 JDK1.6 之后引入的一项锁优化,目的是消除数据在无竞争情况下的同步原语。进一步提升程序的运行性能。 偏向锁就是偏心的偏,意思是这个锁会偏向第一个获得他的线程,如果接下来的执行过程中,改锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。偏向锁可以提高带有同步但无竞争的程序性能,也就是说他并不一定总是对程序运行有利,如果程序中大多数的锁都是被多个不同的线程访问,那偏向模式就是多余的,在具体问题具体分析的前提下,可以考虑是否使用偏向锁。
轻量级锁
为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在 Java SE1.6 里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率,下文会详细分析
synchronized
内置锁Java 中以 synchronize
的形式,为防止资源冲突提供了内置支持。当任务要执行被 synchronize
关键字保护的代码段时,它将检查锁是否可用,然后获取锁——执行代码——释放锁。
所有对象都自动含有单一的锁。当一个线程正在访问一个对象的 synchronized
方法,那么其他线程不能访问该对象的其他 synchronized
方法,但可以访问非 synchronized
方法。因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他 synchronized
方法。
synchronized 代码块:
synchronized(synObject) {
}
当在某个线程中执行这段代码块,该线程会获取对象 synObject
的锁,从而使得其他线程无法同时访问该代码块。synObject
可以是 this
,代表获取当前对象的锁,也可以是类中的一个属性,代表获取该属性的锁。
针对每一个类,也有一个锁,所以 static synchronize
方法可以在类的范围内防止对 static
数据的并发访问。如果一个线程执行一个对象的非 static synchronized
方法,另外一个线程需要执行这个对象所属类的 static synchronized
方法,此时不会发生互斥现象,因为访问 static synchronized
方法占用的是类锁,而访问非 static synchronized
方法占用的是对象锁,所以不存在互斥现象。
对于 synchronized 方法或者 synchronized 代码块,当出现异常时,JVM 会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。
ThreadLocal
理解ThreadLocal
是一个创建线程局部变量的类。通常情况下我们创建的变量,可以被多个线程访问并修改,通过 ThreadLocal
创建的变量只能被当前线程访问。
ThreadLocal
内部实现ThreadLocal
提供了 set
和 get
方法。
set
方法会先获取当前线程,然后用当前线程作为句柄,获取 ThreadLocaMap
对象,并判断该对象是否为空,如果为空则创建一个,并设置值,不为空则直接设置值。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocal
的值是放入了当前线程的一个 ThreadLocalMap
实例中,所以只能在本线程中访问,其他线程无法访问。
ThreadLocal
并不会导致内存泄露,因为 ThreadLocalMap
中的 key
存储的是 ThreadLocal
实例的弱引用,因此如果应用使用了线程池,即便之前的线程实例处理完之后出于复用的目的依然存活,也不会产生内存泄露。
wait
、notify
和 notifyAll
这些方法不在 thread
类里面?这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在 Object
类里是有意义的,还有不把它放在 Thread
类里的原因。
一个很明显的原因是 Java 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的 wait()
方法就有意义了。如果 wait()
方法定义在 Thread
类中,线程正在等待的是哪个锁就不明显了。
简单的说,由于 wait
、notify
和 notifyAll
都是锁级别的操作,所以把他们定义在 Object
类中因为锁属于对象。
内容声明 | |
---|---|
标题: Java 常见面试题及答案 | |
链接: https://zixizixi.cn/articles/2017/02/13/1486974778312.html | 来源: iTanken |
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可,转载请保留此声明。
|