Java 基础常见问题
这里我简单记录我认为比较重要的点。
1. 基本概念
1.1 “Java 语言中的方法属于类中的成员” 为什么不对?
类成员需要是静态的,而方法可能是静态方法,也可能是非静态方法。静态方法是类成员,非静态方法是实例成员。
1.2 什么是反射机制?
反射机制允许程序在运行时进行自我检查,同时也允许对其内部的成员进行操作,反射的主要功能有:得到一个对象所属的类;获取一个类的所有成员变量和方法;在运行时创建对象;在运行时创建对象的方法。
1.3 Java 如何实现函数指针?
Java 没有指针的概念,如何能让 Java 实现类似函数指针的功能?用接口作为函数的参数,把参数实现传进去进行调用即可。
2. 面向对象技术
2.1 面向对象与面向过程的区别?
- 出发点不同,面向对象方法是用符合常规思维的方式来处理客观世界的问题,强调把问题域的要领直接映射到对象及对象之间的接口上;面向对象强调是过程的抽象化和模块化。
- 层级逻辑关系不同,面向对象方法则是用计算机逻辑来模拟客观世界的物理存在,以对象的集合类作为处理问题的基本单元;而面向过程的基本单元是能清晰表达过程的模块,用模块的层次结构概括模块或模块间的关系或功能。
- 数据处理方式与控制程序方式不同,面向对象将数据和对应的代码封装成一个整体,是“事件驱动”来激活和运行程序的;而面向过程是直接通过程序来处理数据,处理完毕即可显示处理结果,各个模块之间可能存在控制、被控制,调用、被调用的关系。
- 分析代码与编码转换的方式不同,面向对象贯穿软件声明周期的分析、设计、编码,是一种平滑的过程,是一种无缝连接;面向过程强调分析、设计、编码之间按规则进行转换,是一种有缝的连接。
2.2 面向对象的特征?
这是一个老生常谈的问题:抽象、继承、封装、多态。
- 抽象:忽略一个主题中与当前目标无关的部分。最直接的就是
abstract类。 - 继承:联结类的一种层次模型,允许和鼓励类的重用,它提供了一种明确的表述共性的方法。
- 封装:将客观事物抽象成类,每个类对自身的数据和方法实行保护。
多态:允许不同类的对象那个对同一消息进行响应。
2.3 多态的两种实现机制?
多态是面向对象中一个重要机制,它表示当同一个操作在不同的类的对象中具有不同的语义,主要有两种不同的表现方式:
- 重载
overload:同一个类中有多个同名的方法。 不能通过访问权限来区分,这样会报错,如果基类是private那么派生类中的同名方法不是重载,而是一个新的方法。 - 覆盖
override:子类覆盖父类中的方法。 覆盖的方法不能是private,异常抛出也要是一致的,同时如果要覆盖的方法被定义为final与native那么也是不能覆盖的。
2.4 抽象类和接口的区别?
- 它们都不能被实例化。
- 接口的实现类和抽象类的子类都只有实现了方法后才能被实例化。
- 接口中只有定义,其方法不能在接口中实现,抽象类有定义与实现。
- 接口被实现
implements,抽象类被继承extends。 - 接口强调特定功能的实现,理念是 “has - a” 关系;抽象类强调所属广西,理念是 “is - a” 关系。
- 接口中默认的成员变量是
public static final,其所有成员变量都是public、abstract的,并且只能被这两个关键字修饰;而抽象类中可以有自己的数据成员变量。 当功能需要累计时,用抽象类;不需要累计时,用接口。 - 接口被运用到实际中比较偏向常用的功能;抽象类更倾向于充当公共类的角色。
2.5 内部类有哪些?
1 | class outerClass { |
- 静态内部类:带有
static的内部类,它可以不依赖于外部类实例而被实例化,静态内部类不能与外部类有相同的名字,不能访问外部类的普通成员变量,只能访问外部类中的静态成员和静态方法(包括私有类型)。 - 成员内部类:去掉
static关键字的静态内部类,它可以自由地引用外部类的属性和方法,它与一个实例绑定在一起,不可以定义静态的属性和方法。 - 局部内部类:作用域是这个代码块,不能是
public、protected、private、static的,只能访问方法中定义为final类型的局部变量。把静态内部类的static去掉,将其定义放入外部类的静态方法或静态初始化代码段就成为了局部静态内部类,特性和静态内部类相似;把成员内部类的定义放入外部类的实例方法或实例初始化代码段中就成为了局部内部类,特性和成员内部类相似。 - 匿名内部类:它必须继承其他类或者其他接口,不能有构造函数,不能有静态成员、方法、类,不能是
public、protected、private、static的,只能创建匿名内部类的一个实例。
2.6 如何获取父类的类名?
这个问题之所以提出来是因为我们前面说过覆盖的要求, Object 的 getClass 方法是 final 与 native 的,它返回的是运行时态,所以下面的结果是一样的:
1 | class A {} |
3. 关键字
3.1 final 、 finally 、 finalize 的区别?
final: 它指的是不可变性,即它只能指向初始化时指向的那个对象,而不关心指向对象内容的变化。当final是方法的时候,不允许任何子类重写这个方法,但可以使用。当final是参数的时候,不允许修改。当final是类的时候,不能被继承,所有方法都不能被重写。一个方法不能既被定义为abstract,又被定义成final。finally: 作为异常处理的一部分,表示最终一定被执行。finalize: 是Object的一个方法,在垃圾回收器执行时调用被回收对象的finalize方法,可以覆盖该方法实现对其他资源的回收,比如关闭文件等。
java中不能被集成的类是那些被final关键字修饰的类,比如String、StringBuffer等。
3.2 static 有什么作用?
为某特定数据类型或对象分配单一的存储空间;实现某个方法或属性与类相关联,而不是对象。
static成员变量:static变量在内存中只有一个复制,所有实例都指向同一个内存地址,静态变量可以通过类.静态变量和对象.静态变量的方式引用。static成员方法:static方法是类的方法,只能访问所属类的静态成员变量和成员方法,不能使用this和super关键字,不能调用非static方法以及变量,一个很重要的用途是实现单例模式。static代码块:独立于成员变量和成员函数的代码块,它不在任何一个方法体内,JVM在加载类时会执行static代码块,如果有多个代码块,会顺序执行,它们只执行一次。static内部类:就是静态内部类,它可以不依赖于外部类实例对象而被实例化,不能访问外部类的普通成员变量,只能访问外部类的静态成员和静态方法。
3.3 volatile 有什么作用?
在使用 volatile 修饰成员变量以后,所有线程在任何时候看到的变量的值都是相同的。 volatile 不能保证操作的原子性,一般情况下,不能替代 sychronized ,另外,它会阻止编译器对代码的优化,因此会降低程序的执行效率,除非迫不得已,不要使用 volatile 。
3.4 strictfp 有什么作用?
精确计算浮点数运算,当一个类被 strictfp 修饰,所有方法都被被动地加上 strictfp ,并且在不同的硬件上会有一致的运行结果。
4. 基本类型与运算
4.1 什么是不可变类?
这指的是当创建了这个类的实例以后,我们不能修改它的值,也就是说,一旦对象创建,它的成员变量不能被修改,只允许别的程序来读,但是不能进行修改。
如果一个类成员不是不可变量,那么在成员初始化或者使用 get 方法获取该成员变量的时候,可以通过 clone 方法来确保类的不可变性。
比如下面的赋值一个是正确的,一个是错误的:
1 | class ImmutableClass { |
4.2 强制转换的注意事项?
Java 语言在涉及 byte 、 short 、 char 运算的时候,首先会先将这些变量强制转换为 int ,然后对 int 进行运算,最后会的值也是 int 类型。
所以两个 short 相加、两个 byte 相加,最后得到的都是一个 int 类型的值。下面的代码会报错:
1 | short s1 = 1; |
正确的写法:
1 | short s1 = 1; |
值得注意的是,有些符号 Java 编译器会进行特殊处理,下面是正确的:
1 | short s1 = 1; |
5. 字符串与数组
5.1 字符串创建与存储的机制是什么?
对于下面的语句:
1 | String s1 = "abc"; |
这段语句是将字符串s1 赋值, abc 放到常量区中,如果这个时候创建一个新的字符串 t1 也是这样 abc ,那么 s1 和 t1 都将指向常量区中的 abc ,同一块内存地址。
对于下面的语句:
1 | String s2 = new String("abc"); |
它的操作等价于两个部分: abc 和 new String() 。这两个操作,若在字符串池中没有 abc ,那么先创建一个 abc ,然后进行 new String() 在堆内存中创建一个新的对象,注意一点这里的 s1 、 t1 、 s2 都是栈空间里的内容。
5.2 String 、 StringBuffer 、 StringBuilder 、 StringTokenizer 有什么区别?
这个问题非常好,我确实不知道。
String 和 StringBuffer 的区别在于当实例化 String 的时候,可以利用构造函数或者赋值的方式来进行初始化,但是 StringBuffer 只能使用构造函数的方式来进行初始化。
String 的初始化其实是有中间步骤的,比如下面的代码:
1 | String s = "Hello"; |
它等价于以下代码:
1 | String s = "Hello"; |
所以说修改多的话,建议使用 StringBuffer 。
而 StringBuilder 是单线程的,线程不安全,如果操作的数据量小,应优先使用 String ,如果单线程大量数据,应该使用 StringBuilder ,当多个线程访问,应该使用 StringBuilder 。
而最后的 StringTokenizer 是用来分割字符串的工具。
5.3 Java 中数组是不是对象?
在 Java 中数据不仅有自己的属性,还拥有一些方法可以被调用,对象的特点是封装了一些数据,同时提供了一些属性和方法,从这个角度看,数组是对象。
5.4 数组的初始化方式?
这个问题还是很重要的,确实五花八门!
与 C++ 不同在于, Java 在创建数组的时候回根据数据类型进行初始化,并且不会给数组元素分配存储空间,因此我们需要为其设置长度:
1 | arrayName = new type[arraySize]; |
二维数组的创建有三种声明方式:
1 | type arrayName[][]; |
在 Java 中二维数组的第二维的长度可以不同。
数组是对象不是原生类。
5.5 length 、 length() 、 size() 的使用?
数组提供了 length 属性来获取数组的长度。
字符串提供了 length() 方法来计算字符串的长度。
size() 方法是针对泛型集合而言的,用于查看泛型中有多少个元素。
6. 异常处理
6.1 finally 块里的代码什么时候执行?
如果 try 里面有 return 我们的 finally 还执行吗?什么时候执行?
任何代码都要在 return 前执行,因此 finally 里的代码也会在 return 前执行,如果说现在 try-finally 和 catch-finally 里都有 return 那么 finally 里的 return 会覆盖别处的 return 语句。
1 | public class Test { |
执行的结果是:
1 | execute finally |
可以发现,我们的执行是先进行了 finally 里的操作,然后执行 try 里的 return 操作。
而如果说我们在 finally 里设置了 return 那么:
1 | public class Test { |
输出结果是:
1 | execute finally |
可以发现,现在,finally 里的 return 替换了之前的 return 。
而事实上,它做的操作是什么顺序呢?先 finally 再 try ?
事实上,程序在执行 try 里的 return 时,会先将返回值存储到一个指定的地址,然后执行 finally ,最后再返回结果。因此,如果返回值是基本类型,那么再在 finally 修改已经没用了,而如果返回是引用类型,那么再在 finally 里修改是可以直接修改到目标内容的。
什么情况下不会执行 finally 带代码:
- 在
try以前就报错了,嗨呀大兄弟,长点心吧。 - 使用
System.exit(0)强制退出。
6.2 运行时异常和普通异常的区别?
Java 有两种错误的异常类: Error 和 Exception 。他们有共同的父类:Throwable 。
Error 是严重的错误,多是由于逻辑错误导致的,编译器不会检查,一旦发生,JVM 会终止线程。
Exception 是可以恢复的异常,是编译器可以捕捉的,它有两种类型:检查异常和运行异常。
检查异常:编译器强制对其进行捕捉并处理。所有继承自 Exception 且不是运行时异常的异常都是检查异常,最常见的有 IO 异常和 SQL 异常,这些异常都发生在便一阶段,只需要放到 try 中,异常处理放到 catch 中即可。
运行时异常:编译器没有强制对其进行捕捉并处理。如果出现,会由 JVM 来处理,出现会向上层抛出,直到遇到处理代码,多线程会直到 Thread.run() 单线程直到 main() ,到了这里,线程退出或者主线程退出。
PS: Java 的异常捕获有多态的概念,是应该先捕获子类,然后捕获基类,否则,子类捕获不会执行。
7. 输入输出流
7.1 Java Socket 是什么?
基于 TCP 的通信过程如下:
- 首先,
Server端监听指定的某个端口是否有连接请求; - 其次,
Client端向Server端发送Connect请求; - 最后,
Server端向Client发送Accept消息,一个连接就建立起来了。
7.2 NIO 是什么?
非阻塞 IO 通过 Selector 、 Channel 、 Buffer 来实现非阻塞的 IO 操作,具体使用一个线程来管理多个通道,轮询处理多线程的请求,保存的是 SelectionKey 和 Channel 之间的关系,这种轮询在处理多线程请求的时候不需要上下文切换。
在处理大量并发请求的时候,使用 NIO 比使用 Socket 效率要高很多。
7.3 什么是 Java 序列化?
对象持久化有的方式分为序列化和外部序列化。
- 序列化
将对象以一连串的字节描述的过程,用于解决在对对象流进行读写操作时所引发的问题。所有要实现序列化的类都必须实现 Serializable 接口,它没有包含任何方法,使用一个输出流来构造一个 ObjectOutputStream 对象,紧接着,使用该对象的 writeObject 方法将对象写出,要恢复可以使用其对应的输入流。
值得注意的有两点:
- 如果一个类能被序列化,那么它的子类也可以被序列化。
static和transient这两种类型的数据成员不能被序列化。
序列化会影响系统的性能,什么情况下需要使用序列化?
- 需要通过网络来发送对象,或对象的状态需要被持久化到数据库或者文件中。
- 序列化能实现深赋值,即可以复制引用的对象。
与序列化相对的是反序列化,它将流转换成对象。每个类都有一个特定的 serialVersionUID ,在反序列化的过程中,通过 serialVersionUID 来判定类的兼容性,而显式声明 serialVersionUID 有以下 3 个优点:
- 提高程序的运行效率。省去了计算的过程。
- 提高程序在不同平台上的兼容性。因为大家的计算方式不同。
- 增强各个版本的兼容性。因为后期该
ID可能会出现变化。
- 外部序列化
外部序列化与序列化主要叙别在于序列化是内置的 API ,而外部序列化需要继承 Serializable 接口,其中的读写方法需要自己实现。
如果我们想对一个类中的部分属性进行序列化,可以这么做:
- 实现继承自
Serializable的接口的新的接口的方法,根据实际情况来控制需要序列化的属性。 - 使用关键字
transient来控制序列化的属性,被该关键字修饰的属性是临时的,不会被序列化。
8. 内存管理
8.1 JVM 是怎么加载 class 的?
类 class 被加载到 JVM 中才能运行,JVM 会将编译成的 .class 文件按照需求和一定的规则加载到内存中。组织成为一个完整的 Java 应用。这个过程是使用类加载器来完成的,它本身也是一个类。实质就是从硬盘读取到内存中。
类的加载分为隐式和显式,其中隐式是 new ;显式是 class.forName() 。
类的加载是动态的,它不会一次加载全部的类,只有在需要的时候进行加载。同理,只有部分类被修改的时候,只会重新编译变化的类,而不会重新编译所有文件,加快了编译速度。
Java 中 3 种不同的类的加载器,对应:系统类、扩展类、自定义类。它们使用委托的方式实现类的加载。当一个类在父类加载器中无法搜索,那么就用它的子类(加载器的子类)来加载。
类的加载:
- 装载。根据路径查询到相应的
class文件,然后导入。- 链接:
- 检查。检查加载
class的正确性。- 准备。给类中的静态变量分配空间。
- 解析。符号引用转换成直接引用。
- 初始化。对静态变量和静态代码块执行初始化工作。
8.2 什么是 GC ?
垃圾回收。回收程序中不再使用的内存。主要有以下三个任务:
- 分配内存。
- 确保被引用对象的内存不被错误地回收。
- 回收不再被引用的对象的内存空间。
但是垃圾回收有以下缺点:
- 跟踪内存的使用情况。
- 释放没用的对象。
- 处理堆里的碎片。
垃圾回收可以用有向图来表示,如果一个节点(对象)是不可达的,那么就是可以回收的。 追踪回收
把堆中活动的对象放到堆的一端中,这样堆的另外一端就留出了一块空闲,相当于对堆中的碎片进行了处理,但是这样会有性能损失。压缩回收
把堆分成两份,只在一份存放内容,如果满了,将活动的部分放到另外一份中。复制回收
把堆分成两个或多个子堆,每一堆都是一代,经过多次复制回收以后依然存活的对象升到高一级的堆中。按代回收
8.3 Java 中是否存在内存泄露?
首先我们需要知道的是,垃圾回收器帮我们做了什么,它回收有两个标准:
- 对象赋予了
null,以后再没有使用了。 - 对象赋予了新值,重新分配了内存空间。
内存泄露指下面两种情况:
- 堆中申请的空间没有释放掉。
- 对象已不再使用,但是仍然存在在内存中。
我们可以发现这里内存泄露的第一种情况已经被 GC 的第二条解决了,剩下的是对象已经不再使用,但是仍然在内存中。
内存泄露的情况:
- 静态集合类。它们如果不释放,那么内部对象也无法释放。
- 各种连接。不
close()那么内部对象无法释放。 - 监听器。同理,需要删除监听器。
- 变量不合理的作用域。方法变量写到类中的情况。
- 单例。或者说
static对象对于类而言是一直存在的。
8.4 Java 中的堆和栈有什么区别?
基本数据类型和引用变量在栈中。
引用类型在堆中。
堆空间的变量有 GC 来帮我们处理。
9. 容器
9.1 什么是迭代器?
迭代器是一个对象,它的工作是遍历并选择序列中的对象。
- 使用
iterator()将返回一个Iterator,然后通过使用next()来返回第一个元素。 - 使用
Iterator的hasNext()方法判断容器中是否还有元素,如果有,继续使用next()获取下一个元素。 - 使用
remove()来删除迭代器中的对象。
ListIterator 只存在在 List 中,支持在迭代过程中向 List 中添加或删除元素,并且可以在 List 中双向滚动。
如果我们遇到 ConcurrentModificationException 异常,这通常是因为使用 Iterator 遍历容器的同时,对容器进行了修改,或者在多线程中,一个线程对容器进行了遍历,另外一个线程对容器进行了修改。
我们在使用 next() 的时候,会比较变量 expectedModCount 和容器中的实际对象的个数 modCount 的值是否相等,如果不相等,就会出现 ConcurrentModificationException 异常。
正确的做法应该是:
在遍历中把删除的对象放到一个集合中,等遍历结束以后调用
removeAll()方法来删除,或者使用iter.remove()方法。
在多线线程中,我们可以使用一些线程安全的容器:ConcurrentHashMap 和 CopyOnWriteArrayList ,或者使用 synchronized 代码块来存放容器的遍历操作。
9.2 ArrayList 、Vector 、LinkedList 有什么区别?
它们都在 java.util 包下。
其中 ArrayList 和 Vector 都适合访问,内存地址是连续的,但是前者默认扩充是原来的 1.5 倍,后者扩充是原来的 2 倍;前者是线程不安全的,后者是线程安全的。
而 LinkedList 采用了双向链表来实现,适合修改。
9.3 HashMap 、HashTable 、TreeMap 、WeakHashMap 有什么区别?
HashMap是HashTable的轻量级实现,非线程安全的实现,HashMap允许一条记录的key是null。HashTable是线程安全的,就效率而言,HashMap要优于HashTable。HashTable使用Enumeration,HashMap使用Iterator。HashMap的默认大小是 16,每次增加是原来的两倍加一;HashTable的默认大小是 11,每次增加是原来的两倍。TreeMap实现了SortMap接口,保证记录根据键值排序。WeakHashMap是弱引用类型,如果其中的map没有外部引用,那么将会自动删除。
下面的代码实现了一个线程安全的 HashMap :
1 | Map m = Collections.synchronizedMap(new HashMap()); |
9.4 把自定义的类作为 HashMap 的 key 需要注意什么?
当一个新的元素到 HashMap ,它会先比较 hashCode() ,如果不相等,则添加这个元素,如果相等,则需要用 equals() 方法比较 key ,如果 key 相等,则覆盖 value ,如果不相等,则说明出现了冲突。
HashMap 中使用链地址法来解决冲突,比如我们现在通过 key 来找到 value ,它是怎么办到的?
- 首先通过
key进行hashCode()方法找到值存储的首地址 - 发现存在多个
key满足组成的keyList - 遍历每个
key,用equals()来找到key相同的键值对,这里的value就是返回结果
默认的 hashCode() 返回对象存储的内存地址,默认的 equals() 比较对象是否是同一个对象。
这告诉我们,在使用自定义类实现 key 的时候,我们需要重写 equals() 和 hashCode() 。
10. 多线程
10.1 如果实现 Java 的多线程?
- 继承
Thread类,重写run()方法:注意Thread类实际上是实现了Runnable接口的一个实例,启动线程唯一的方法是Thread类的start()方法,该方法将启动一个新线程,并执行run()方法。 - 实现
Runnable接口,实现run()方法:也是通过Thread类的start()方法来启动新的线程。 - 实现
Callable接口,重写call()方法。
关于 Callable 接口和 Runnable 接口的不同点:
Callable可以在任务结束后提供一个返回值Callable的call()方法可以抛出异常- 运行
Callable可以获取一个Future对象,表示异步计算的结果,由于线程输入异步计算模型,因此无法从别的线程中得到函数的返回值,这时,可以使用Future来监控目标线程调用call()方法的情况,当调用Future的get()可以获取结果,同时当前线程将阻塞,直到call()方法返回结果。
推荐使用 Runnable 接口,这比较轻量。
10.2 run() 方法和 start() 方法有什么区别?
通常,系统通过调用线程类的 start() 方法来启动一个线程,此时线程处于就绪态,可以被 JVM 调用。在调度过程中,start() 方法异步调用线程类的 run() 方法来完成实际的操作,当 run() 方法结束了,此线程也结束了。
如果直接调用 run() ,那么仅仅是一个同步的方法。
10.3 多线程同步的实现方法有哪些?
sychronized 关键字
synchronized方法:放到方法中的关键字,但是会大大影响程序的效率。sychronized块:锁定对象,有非常高的灵活性。
wait() 方法和 notify() 方法 (这个使用基本在使用 sychronized 关键字基础上)
wait()方法释放对象锁,进入等待状态,并且可以调用notify()方法通知正在等待的其他线程。notifyAll()允许其他进程去竞争。
Lock
lock()方法以阻塞的方式获取锁,如果获得了锁,立即返回,如果别的线程持有锁,当前线程等待。tryLock()方法以非阻塞的方式获取锁,尝试性获取锁,如果得到立即返回true,否则false。lockInterruptibly()如果获取了锁,立即返回,如果没有获取锁,当前线程进入休眠状态,直到获取锁或者被中断,和lock()最大的不同在于,如果lock()获取不到锁,会一直处于阻塞状态,并且会忽略interrupt()方法。
10.4 sleep() 和 wait() 有什么区别?
- 原理不同:
sleep()是Thread的静态方法,是线程用来控制自身流程的,令线程暂停一段时间,像是一个闹钟;wait()是Object的方法,用于线程通信,使当前拥有该对象锁的进程等待,可以设定时间自动醒来或者其他线程调用notify()。 - 对锁的处理机制不同:调用
sleep()不会释放锁,而wait()会释放它占用的锁。 - 使用区域不同:
wait()具有特殊意义,使用在同步控制语句中,而sleep()则可以随时随地。
sleep() 和 yield() 的区别:
sleep()给其他线程机会不考虑优先级;yield()给其他线程机会只会给比自己相同等级或等级高的线程。sleep()会进入阻塞态,在此期间,线程不会被执行;而yield()会使当前线程回到可执行状态很可能又马上被执行。
10.5 终止线程的方法有哪些?
stop() 会释放锁,但存在不一致的状态,所以是不安全的。
suspend() 不释放锁,可能发生死锁,不安全。
有一个标志,在 while 里,终止就设置标志,但是可能是非运行态,所以有时候不可行。
我们可以通过捕获异常的方式,来安全结束进程,比如使用 InterruptedException 。
10.6 sychronized 与 Lock 有什么异同?
- 用法不一样:前者托管给
JVM,后者需要我们通过代码实现。 - 性能不一样:在资源竞争不是很激烈的时候,
sychronized的性能比Lock好,但是在竞争激烈的情况下,sychronized性能下降很快,而Lock基本不变。 - 锁的机制不一样:
sychronized是自动解锁,是相反顺序释放;Lock需要手动释放,还必须在finally里。
两种锁的机制不要一起使用!
当一个线程进入一个对象的一个 synchronized 方法后,其他线程能进入其他普通方法和带 static 标记的方法 (static sychronized 也包括)。
10.7 什么是守护线程?
设置 setDaemon 为 true 以后,表示是守护进程,当进程中只有守护进程,JVM 会自动退出。
10.8 join() 方法的作用是什么?
join() 方法的作用是让调用该方法的进程在执行完 run() 以后再执行 join() 后的方法。