JUC面试题(难)
一、volatile
1、了解volatile吗
volatile是Java虚拟机提供的轻量级的同步机制,有3个特性,分别是:保证可见性、不保证原子性、禁止指令重排
2、什么是指令重排
计算机在执行程序时,为了提高性能,编译器在编译java代码和处理器jvm字节码的时候常常会做指令重排多线程中使用线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测,所以需要使用轻量级的同步机制volatile。
3、在哪些地方用到过volatile?
多线程单例模式,通过引入DCL (Double Check Lock) 双端检锁机制
就是在进来和出去的时候,进行检测
public class SingletonDemo {
/**
instance = new SingletonDemo();可以分为以下三步进行完成:
memory = allocate(); // 1、分配对象内存空间
instance(memory); // 2、初始化对象
instance = memory; // 3、设置instance指向刚刚分配的内存地址
可能出现指令重排,故要加上volatile
*/
private static volatile SingletonDemo instance = null;
private SingletonDemo () {
System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
}
public static SingletonDemo getInstance() {
if(instance == null) {
// a 双重检查加锁多线程情况下会出现某个线程虽然这里已经为空,但是另外一个线程已经执行到d处
synchronized (SingletonDemo.class) //b
{
//c不加volitale关键字的话有可能会出现尚未完全初始化就获取到的情况。原因是内存模型允许无序写入
if(instance == null) {
// d 此时才开始初始化
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
//模拟多线程环境
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}
二、CAS
1、CAS是什么?与synchronized有什么区别?cas有什么缺点?并如何解决
cas——Compare and Swap(比较并交换)是一种系统原语;它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁,所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
缺点:循环时间长,开销大,只能保证一个共享变量的原子操作,会产生ABA问题;
{ABA例子:假设有一个遵循CAS原理的提款机,小慧有100元存款,要使用这个提款机来提款50,由于提款机硬件出了点小问题,小灰的提款操作被同时提交两次,开启了两个线程,两个线程都是获取当前值100元,要更新成50元。理想情况下,应该一个线程更新成功,另一个线程更新失败,小灰的存款只被扣一次。线程1首先执行成功,把余额从100改成50。线程2因为某种原因阻塞了。这时候,小灰的妈妈刚好给小灰汇款50元。线程2仍然是阻塞状态,线程3执行成功,把余额从50改成100。线程2恢复运行,由于阻塞之前已经获得了“当前值”100,并且经过compare检测,此时存款实际值也是100,所以成功把变量值100更新成了50}
解决办法:1、使用AtomicReference原子引用(保证修改对象引用时的线程安全性),2、使用AtomicStampedReference时间戳原子引用修改版本号(每次修改原子值,版本号加1)
2、int变量在多线程下如何保证其原子性
使用AtomicInteger,调用其api进行增删改查操作,例如:
atomicInteger.compareAndSet(1, 2)
二、集合
1、ArrayList是否线程安全?会报出现什么异常?导致原因?怎么解决?
不安全;java.util.ConcurrentModificationException
1、使用new Vectore<>(),
2、Collections.synchronizedList(new ArrayList<>())
3、使用CopyOnWriteArrayList()
2、hashSet底层是什么实现的?
hashmap
3、hashmap使用put方法添加key-value键值对,但是hashset的add方法就添加了一个数,这怎么解释
hashset的add方法实际调用了hashmap的put方法,只不过添加的值维key,而value是一个常量PRESENTpivate static final Object PRESENT = null;)
三、锁
1、乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
乐观锁,每次操作时不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止
悲观锁是会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
乐观锁可以使用volatile+CAS原语实现,带参数版本来避免ABA问题,在读取和替换的时候进行判定版本是否一致
悲观锁可以使用synchronize的以及Lock
2、死锁产生的四个条件
互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
循环等待条件:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所申请的资源。
3、理解可重入锁
可重入锁就是递归锁
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取到该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
也就是说:线程可以进入任何一个它已经拥有的锁所同步的代码块
ReentrantLock / Synchronized 就是一个典型的可重入锁
4、当我们在getLock方法加两把锁会是什么情况呢?
最后得到的结果也是一样的,因为里面不管有几把锁,其它他们都是同一把锁,也就是说用同一个钥匙都能够打开
当我们在getLock方法加两把锁,但是只解一把锁会出现程序直接卡死,线程不能出来,也就说明我们申请几把锁,最后需要解除几把锁
当我们只加一把锁,但是用两把锁来解锁的时候,运行程序会直接报错
5、什么是自旋锁、有什么优缺点?请手写一个自旋锁;
自旋锁:spinlock,是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU,原来提到的cas,底层使用的就是自旋,自旋就是多次尝试,多次访问,不会阻塞的状态就是自旋。
优点:循环比较获取直到成功为止,没有类似于wait的阻塞
缺点:当不断自旋的线程越来越多的时候,会因为执行while循环不断的消耗CPU资源
/**
* 手写一个自旋锁
*
* 循环比较获取直到成功为止,没有类似于wait的阻塞
*
* 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到
* @author: 陌溪
* @create: 2020-03-15-15:46
*/
public class SpinLockDemo {
// 现在的泛型装的是Thread,原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock() {
// 获取当前进来的线程
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t come in ");
// 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋
while(!atomicReference.compareAndSet(null, thread)) {
}
}
/**
* 解锁
*/
public void myUnLock() {
// 获取当前进来的线程
Thread thread = Thread.currentThread();
// 自己用完了后,把atomicReference变成null
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
// 启动t1线程,开始操作
new Thread(() -> {
// 开始占有锁
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 开始释放锁
spinLockDemo.myUnLock();
}, "t1").start();
// 让main线程暂停1秒,使得t1线程,先执行
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 1秒后,启动t2线程,开始占用这个锁
new Thread(() -> {
// 开始占有锁
spinLockDemo.myLock();
// 开始释放锁
spinLockDemo.myUnLock();
}, "t2").start();
}
}
6、为什么需要读写锁?如何实现?
使用ReentrantLock创建锁的时候,是独占锁,也就是说一次只能一个线程访问,但是有一个读写分离场景,读的时候想同时进行,因此原来独占锁的并发性就没这么好了,因为读锁并不会造成数据不一致的问题,因此可以多个人共享读
/**
* 读写锁
* 多个线程 同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行
* 但是,如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写
*
* @author: 陌溪
* @create: 2020-03-15-16:59
*/
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 资源类
*/
class MyCache {
/**
* 缓存中的东西,必须保持可见性,因此使用volatile修饰
*/
private volatile Map<String, Object> map = new HashMap<>();
/**
* 创建一个读写锁
* 它是一个读写融为一体的锁,在使用的时候,需要转换
*/
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
/**
* 定义写操作
* 满足:原子 + 独占
* @param key
* @param value
*/
public void put(String key, Object value) {
// 创建一个写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写入完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 写锁 释放
rwLock.writeLock().unlock();
}
}
/**
* 获取
* @param key
*/
public void get(String key) {
// 读锁
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 读锁释放
rwLock.readLock().unlock();
}
}
/**
* 清空缓存
*/
public void clean() {
map.clear();
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 线程操作资源类,5个线程写
for (int i = 1; i <= 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, String.valueOf(i)).start();
}
// 线程操作资源类, 5个线程读
for (int i = 1; i <= 5; i++) {
// lambda表达式内部必须是final
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, String.valueOf(i)).start();
}
}
}
四、你认识哪些高并发下的计数器
1、CountDownLatch:减法,减到0
/**
现在有这样一个场景,假设一个自习室里有7个人,其中有一个是班长,班长的主要职责就是在其它6个同学走了后,关灯,锁教室门,然后走人,因此班长是需要最后一个走的,那么有什么方法能够控制班长这个线程是最后一个执行,而其它线程是随机执行的
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 计数器
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 上完自习,离开教室");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "\t 班长最后关门");
}
}
2、CyclicBarrier:和CountDownLatch相反,需要集齐七颗龙珠,召唤神龙。也就是做加法,开始是0
public class CyclicBarrierDemo {
public static void main(String[] args) {
/**
* 定义一个循环屏障,参数1:需要累加的值,参数2 需要执行的方法
*/
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("召唤神龙");
});
for (int i = 0; i < 7; i++) {
final Integer tempInt = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t 收集到 第" + tempInt + "颗龙珠");
try {
// 先到的被阻塞,等全部线程完成后,才能执行方法
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
3、Semaphore:信号量
信号量主要用于两个目的
-
一个是用于共享资源的互斥使用
-
另一个用于并发线程数的控制
/** 模拟一个抢车位的场景,假设一共有6个车,3个停车位 那么我们首先需要定义信号量为3,也就是3个停车位 */ public class SemaphoreDemo { public static void main(String[] args) { /** * 初始化一个信号量为3,默认是false 非公平锁, 模拟3个停车位 */ Semaphore semaphore = new Semaphore(3, false); // 模拟6部车 for (int i = 0; i < 6; i++) { new Thread(() -> { try { // 代表一辆车,已经占用了该车位 semaphore.acquire(); // 抢占 System.out.println(Thread.currentThread().getName() + "\t 抢到车位"); // 每个车停3秒 try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t 离开车位"); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 释放停车位 semaphore.release(); } }, String.valueOf(i)).start(); } } }
五、jvm
1、画出JVM基本机构图
- java栈:存放局部变量,栈由一系列帧组成
- java堆:存放所有new出来的东西
- 方法区:被虚拟机加载的类信息、常量、静态常量等。
- 程序计数器(和系统相关):每个线程拥有一个PC寄存器,在线程创建时创建,指向下一条指令地址
- 本地方法栈:
2、谈谈你对GC Root的理解
用于标记回收算法:从GC root进行遍历,把可达对象都标记,剩下那些不可达的进行回收,这种方式需要中断其他线程,并且可能产生内存碎片。
java中可以作为GC Roots的对象有:虚拟机栈中引用的对象、方法区中的类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(natice方法)引用的对象。
3、 如何排查死锁
当我们出现死锁的时候,首先需要使用jps命令查看运行的程序jps -l
再使用jstack查看堆栈信息
jstack xxxx # 后面参数是 jps输出的该类的pid
通过查看最后一行,我们看到 Found 1 deadlock,即存在一个死锁
4、常见的GC算法
1)引用计数法
每个对象有一个计数器,当对象被引用一次则计数器加1,但对象引用失效一次减1,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。
缺点:每次对象复制时均要维护引用计数器,且计数器本身也有一定的消耗;较难处理循环引用。
在双端循环,互相引用的时候,容易报错,目前很少使用这种方式了
2)复制算法
复制算法在年轻代的时候,进行使用,复制时候有交换
分对象会在From和To区域来回复制,如此交换15次(由jvm参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活就存入老年代中。
算法优点:没有产生内存碎片
3)标记清除
先标记后清除,缺点是会产生内存碎片,用于老年代多一些。
4)标记整理
标记清除整理
5、你说你做过JVM调优和参数配置,请问如何盘点查看JVM系统默认值
使用jps和jinfo进行查看
1、jps:查看java的后台进程
2、jinfo:查看正在运行的java程序
生活常用调优参数
- -Xms:初始化堆内存,默认为物理内存的1/64,等价于 -XX:initialHeapSize
- -Xmx:最大堆内存,默认为物理内存的1/4,等价于-XX:MaxHeapSize
- -Xss:设计单个线程栈的大小,一般默认为512K~1024K,等价于 -XX:ThreadStackSize
- 使用 jinfo -flag ThreadStackSize 会发现 -XX:ThreadStackSize = 0
- 这个值的大小是取决于平台的
- Linux/x64:1024KB
- OS X:1024KB
- Oracle Solaris:1024KB
- Windows:取决于虚拟内存的大小
- -Xmn:设置年轻代大小
- -XX:MetaspaceSize:设置元空间大小
- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现,不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存,因此,默认情况下,元空间的大小仅受本地内存限制。
- -Xms10m -Xmx10m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal
- 但是默认的元空间大小:只有20多M
- 为了防止在频繁的实例化对象的时候,让元空间出现OOM,因此可以把元空间设置的大一些
- -XX:PrintGCDetails:输出详细GC收集日志信息
- GC
- Full GC
6、oom和StackoverflowError
JVM中常见的两个错误,均属于Error,不是Exception异常
1)StackoverFlowError :栈溢出
2)OutofMemoryError: java heap space:堆溢出
java heap space
创建了很多对象,导致堆空间不够存储
GC overhead limit exceeded
GC回收时间过长时会抛出OutOfMemoryError,过长的定义是,超过了98%的时间用来做GC,并且回收了不到2%的堆内存
连续多次GC都只回收了不到2%的极端情况下,才会抛出。假设不抛出GC overhead limit 错误会造成GC清理的这点内存很快会再次被填满,迫使GC再次执行,这样就形成了恶性循环,CPU的使用率一直都是100%,而GC却没有任何成果。
unable to create new native thread
不能够创建更多的新的线程了,也就是说创建线程的上限达到了
在高并发场景的时候,会应用到高并发请求服务器时,经常会出现如下异常
java.lang.OutOfMemoryError:unable to create new native thread
,准确说该native thread异常与对应的平台有关导致原因:
- 应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
- 服务器并不允许你的应用程序创建这么多线程,linux系统默认运行单个进程可以创建的线程为1024个,如果应用创建超过这个数量,就会报
java.lang.OutOfMemoryError:unable to create new native thread
解决方法:
- 想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
- 对于有的应用,确实需要创建很多线程,远超过linux系统默认1024个线程限制,可以通过修改linux服务器配置,扩大linux默认限制
Metaspace
java.lang.OutOfMemoryError:Metaspace
元空间内存不足,Matespace元空间应用的是本地内存
-XX:MetaspaceSize
的处理化大小为20M
六、线程基础面试题
1、你知道有哪些线程池?
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService。
1、newCachedThreadPool创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
2、newFixedThreadPool创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。
3、newScheduledThreadPool创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
4、newSingleThreadExecutor,Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程) ,这个线程
池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!
2、如何停止一个正在运行的线程
1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的
方法。
3、使用interrupt方法中断线程。
class MyThread extends Thread {
volatile boolean stop = false;
public void run() {
while (!stop) {
System.out.println(getName() + " is running");
try {
sleep(1000);
} catch (InterruptedException e) {
System.out.println("week up from blcok...");
stop = true; // 在异常处理代码中修改共享变量的状态
}
}
System.out.println(getName() + " is exiting...");
}
}
class InterruptThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
MyThread m1 = new MyThread();
System.out.println("Starting thread...");
m1.start();
Thread.sleep(3000);
System.out.println("Interrupt thread...: " + m1.getName());
m1.stop = true; // 设置共享变量为true
m1.interrupt(); // 阻塞时退出阻塞状态
Thread.sleep(3000); // 主线程休眠3秒以便观察线程m1的中断情况
System.out.println("Stopping application...");
}
}
3、sleep()和wait() 有什么区别?
- 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。
- sleep()方法导致了程序暂停执行指定的时间,让出 cpu 给其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态
- 在调用 sleep()方法的过程中, 线程不会释放对象锁。
- 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
4、什么是线程安全,上下文
线程安全就是说多线程访问同一代码,不会产生不确定的结果;
5、什么是上下文,什么是多线程中的上下文切换,上下文切换的活动,还有引起线程上下文切换的原因
- 上下文是指某一时间点 CPU 寄存器和程序计数器的内容;
- 多线程会共同使用一组计算机上的 CPU,而线程数大于给程序分配的 CPU 数量时,为了让各个线程都有执行的机会,就需要轮转使用 CPU。不同的线程切换使用 CPU发生的切换数据等就是上下文切换。
- 1.挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
2.在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
3.跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序
中。 - 原因:
-
当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务;
-
当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务;
-
多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
-
用户代码挂起当前任务,让出 CPU 时间;
-
硬件中断;
6、在 java 中守护线程和本地线程区别
java 中的线程分为两种:守护线程(Daemon)和用户线程(User)。
任何线程都可以设置为守护线程和用户线程,通过方法 Thread.setDaemon(bool
on);true 则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon()
必须在 Thread.start()之前调用,否则运行时会抛出异常。
两者的区别:
唯一的区别是判断虚拟机(JVM)何时离开,Daemon 是为其他线程提供服务,如果
全部的 User Thread 已经撤离,Daemon 没有可服务的线程,JVM 撤离。也可
以理解为守护线程是 JVM 自动创建的线程(但不一定),用户线程是程序创建的
线程;比如 JVM 的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产
生垃圾,守护线程自然就没事可干了,当垃圾回收线程是 Java 虚拟机上仅剩的线
程时,Java 虚拟机会自动离开
7、什么是 Callable 和 Future?
Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能
更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。可以认为
是带有回调的 Runnable。
Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable
用于产生结果,Future 用于获取结果。