前言
众多周知啊,在我们java基础中,有一个基础是我们面试必不可少的话题,但是实际工作中,又很少使用,乃至根本不怎么用。那么这个就是多线程,但也有朋友说根据场景来使用的,也不是用在哪里都好,只有结合时间复杂度以及性能、异步、线程的调度才能完美演绎。那么今天就来讲下多线程好处以及实际工作用到的场景
一、概念
1、学习思路
为什么学习线程?为了解决CPU利用率问题,提高CPU利用率。 =》 什么是进程?什么是线程? =》 怎么创建线程?有哪几种方式?有什么特点? =》 分别怎么启动线程? =》 多线程带来了数据安全问题,该怎么解决? =》 怎么使用synchronized(同步)决解? =》使用同步可能会产生死锁,该怎么决解? =》 线程之间是如何通信的? =》 线程有返回值吗?该如何拿到? =》 怎么才能一次性启动几百上千个的线程?
2、为什么学多线程?
平时面试我们面试都会这么说,提高利用cpu利用率
3、进程和线程区别?
例如打开QQ、打开微信、秀基宝小程序,这种操作系统中执行不同程序就是进程
例如我们QQ中,发送了一个消息,那么后台接受请求,开辟一个主线程,这样就是线程,你也知道电脑不可能只有一个线程,那么多核服务器中,多个线程同时工作,此时就是多线程的场景。
进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个寿命期都在该进程中。
4、其他问题
– Java的多线程
• Java 中的多线程是通过java.lang.Thread类来实现的.
• 一个Java应用程序java.exe,其实至少有三个线程: main()主线程, gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
• 使用多线程的优点。
– 背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短(因为单线程的可以减少cup的调度消耗的时间),为何仍需多线程呢?
– 多线程程序的优点:
1.提高应用程序的响应。对图形化界面更有意义,可增强用户体验。同时做多个事情。比如:一边听歌、一边写代码。
2.提高计算机系统CPU的利用率。
3.改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
• 何时需要多线程
– 程序需要同时执行两个或多个任务。
– 需要一些后台运行的程序时。比如:Java后台运行的GC功能。
主线程
– 概念
• 即使Java程序没有显示的来声明一个线程,Java也会有一个线程存在该线程叫做主线程
• 可以调用Thread.currentThread()来获得当前线程
二、线程创建方法
- 继承Thread类
- 覆盖run方法
- 实现Runnable接口
- Runnable有一个run方法,定义逻辑
- 实现 Callable
其实还有其他方法,总共四种。
三、启动和终止
启动
- 调用Thread的start方法
终止
当run方法返回,线程终止,一旦线程终止后不能再次启动。
- 1、设置一个标记来控制线程的终止。
public volatile boolean exit = false;
public void run()
{
while (!exit);
}
public static void main(String[] args) throws Exception
{
ThreadFlag thread = new ThreadFlag();
thread.start();
sleep(5000); // 主线程延迟5秒
thread.exit = true; // 终止线程thread
thread.join();
System.out.println("线程退出!");
}
在上面代码中定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false.在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值
-
2、stop终止
终止线程、暴力终止,可能导致工作没有完全完成,哈可能导致对锁定的内容进行解锁,从而造成数据不同步 -
3、调用线程的interrupt方法
interrup只是终止睡眠吧,并不会终止线程
public class MyThread4 extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 50000; i++) {
if (this.isInterrupted()) {
System.out.println( "线程已经结束,我要退出" );
break;
}
System.out.println( "i=" + (i + 1) );
}
System.out.println( "我是for下面的语句,我被执行说明线程没有真正结束" );
}
}
public static void main(String[] args) {
try {
MyThread4 myThread4 = new MyThread4();
myThread4.start();
Thread.sleep( 20);
myThread4.interrupt();
} catch (InterruptedException e) {
System.out.println( "main catch" );
e.printStackTrace();
}
}
根据打印结果发现for后面的内容依旧会执行,为了解决这种问题,可以采用抛异常的方式,或return的方式终止线程。一般推荐抛异常的方式,这样才能使得线程停止得以扩散。
public class MyThread4 extends Thread {
@Override
public void run() {
super.run();
try {
for (int i = 0; i < 50000; i++) {
if (this.isInterrupted()) {
System.out.println( "线程已经结束,我要退出" );
// return;
throw new InterruptedException();
}
System.out.println( "i=" + (i + 1) );
}
System.out.println( "我是for下面的语句,我被执行说明线程没有真正结束" );
} catch (InterruptedException e) {
System.out.println( "进入MyThread.java类中run方法的catch异常了" );
e.printStackTrace();
}
}
}
另一种就是睡眠中停止,具体方式就是先sleep,然后interrupt
try {
for (int i = 0; i < 10000; i++) {
System.out.println( "i=" +(i + 1) );
}
System.out.println( "run begin" );
Thread.sleep( 200 );
System.out.println( "run end" );
} catch (InterruptedException e) {
System.out.println( "先停止,后sleep" );
e.printStackTrace();
}
public static void main(String[] args) {
MyThread5 thread5 = new MyThread5();
thread5.start();
thread5.interrupt();
}
四、线程基本方法
4.1、有关方法
- start():启动线程并执行对象的run()方法
- run() 线程被调度执行
- String getName():返回线程的名称
- void setName(String name):设置该线程名称
- static Thread currentThread():返回当前线程。
4.2、基本方法
- isAlive 判断线程是否存活
- getPriority 获得线程优先级
- setPriority 写入优先级
- sleep 睡眠
- join 加入线程队列,接着执行
- yield 让出CPU
- wait 等待,让其他线程先执行
- notify/notifyAll 通知/通知所有
五、线程优先级
- 线程优先级越高、占用CPU时间越高
- 最高10级,最低1级,默认5级
六、线程状态
- 创建
- 就绪
- 运行
- 阻塞
- 死亡
七、线程同步
在我们工作中,有时间同一资源可能会被多个线程抢占使用,会造成脏数据,这个时候我们肯定就需要用到同步,常见问题就是银行转账的问题
7.1、线程安全
当多个线程访问同一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方式代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。
- 同一时间
- 多个线程
- 同一资源
7.2、解决
为了解决线程安全,我们常用的就是synchronized和lock
synchronized 是Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
1、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
2、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
3、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
4、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
7.3、如何避免
- 数据单线程内可见。
- 例如:ThreadLocal
- 只读对象
- 使用 final 关键字修饰类,避免被继承;没有任何更新方法;返回值不能为可变对象。
- 线程安全类
- 它采用 synchronized 关键字来修饰相关方法,例如StringBuffer
- 同步与锁机制
- 自定义实现相关的安全同步机制
7.4、要不只读,要不加锁
核心理念就是“要不只读,要不加锁”。JDK 提供的并发包,主要分成以下几个类族:
- 线程同步类
这些类使线程间的协调更加容易,支持了更加丰富的线程协调场景,逐步淘汰了使用 Object 的 wait() 和 notify() 进行同步的方式。主要代表有 CountDownLatch、Semaphore、CyclicBarrier 等。
- 并发集合类
集合并发操作的要求是执行速度快,提取数据准。最典型的莫过于 ConcurrentHashMap,经过不断的优化,有刚开始的分段式锁到后来的 CAS ,不断的提高并发性能。
- 线程管理类
虽然 Thread 和 ThreadLocal 在 JDK1.0 就已经引入,但是真正把 Thread 的作用发挥到极致的是线程池。根据实际场景的需要,提供了多种创建线程池的快捷方式,如使用 Executors 静态工厂或者使用 ThreadPoolExecutors 等。另外,通过 ScheduledExecutorService 来执行定时任务。
- 锁相关类
锁以 Lock 接口为核心,派生出一些实际场景中进行互斥操作的锁相关类。最有名的是 ReentrantLock 。
八、死锁
上门讲了锁可以给我们带来多线程场景下会导致数据不一致,脏数据等问题,所以我们给他们加上锁,那么也有一个坑,那就是当多个线程都对一个资源进行不释放,都在等待对方释放需要同步的资源,这样会导致线程的死锁。
概念
多个并发进程因争夺系统资源而产生相互等待的现象。
8.1、危害
这个时候程序也不会抛异常,最终会造成程序阻塞、无法继续进行
8.2、四个必要条件
- 互斥:一个资源每次只能被一个进程使用;
- 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
- 不剥夺:进程已获得的资源,在末使用完之前,不能强行剥夺;
- 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系;
8.3、解决
- 避免死锁 ----- 在使用前进行判断,只允许不会产生死锁的进程申请资源
- 减少需要同步的资源定义
- 尽量避免嵌套同步
- 死锁检测与解除 ----- 在检测到运行系统进入死锁,进行恢复。
如果利用死锁检测算法检测出系统已经出现了死锁 ,那么,此时就需要对系统采取相应的措施。常用的解除死锁的方法:
1、抢占资源:从一个或多个进程中抢占足够数量的资源分配给死锁进程,以解除死锁状态。
2、终止(或撤销)进程:终止或撤销系统中的一个或多个死锁进程,直至打破死锁状态。
九、线程的通信
概念
在我们线程执行中,我们线程难免会造成阻塞、或者同步、等待各种情况,他们之间其实可以互相通讯的,会被提高优先级、改变线程的状态,这个时候就会用到通讯
9.1、方式
- Object.wait 与 Object.notify/notifyAll
- LockSupport.park 与 LockSupport.unpark
- ReentrantLock + Condition
- 共享内存方式:volatile 关键字、辅助类(CountDownLatch、CyclicBarrier、Semaphore)
// LockSupport.park 与 LockSupport.unpark
public class ThreadDemo {
public static void main(String[] args) {
final Thread threadA = new Thread(() -> {
System.out.println("开始阻塞线程");
LockSupport.park();
// park之后线程已被阻塞,只有被唤醒之后下面的句子才能输出
System.out.println("阻塞唤醒完毕");
});
threadA.start();
System.out.println("开始唤醒线程");
LockSupport.unpark(threadA);
}
}
// 输出:
// 开始阻塞线程
// 开始唤醒线程
// 阻塞唤醒完毕
9.2、方法
- wait 进入阻塞状态,并释放同步监视器。
- notify 唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的。
- notifyAll 醒所有被wait的线程。
9.3、前提
- 大多数情况下,线程间的通信机制指的是线程间的交互,即线程的唤醒和阻塞。并不是字面意义上的多个线程之间互相共享和交换数据的“通信”。
- 这三个方法的调用者必须是同步代码块或同步方法中的同步监视器否则,会出现IllegaLMonitorStateException异常.
public class ProducerConsumer {
public static void main(String[] args) {
BaoziStack baoziStack = new BaoziStack();
Producer p1 = new Producer(baoziStack);
Consumer c1 = new Consumer(baoziStack);
p1.start();
c1.start();
}
}
// 包子类
class Baozi {
int id;
public Baozi(int id) {
this.id = id;
}
@Override
public String toString() {
return "包子 : " + id;
}
}
// 包子筐
class BaoziStack {
Baozi[] bz = new Baozi[10];
int index = 0;
// 装包子
public synchronized void pushBZ(Baozi baozi) {
if (index >= bz.length) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
bz[index] = baozi;
index++;
notify();
}
// 取包子
public synchronized Baozi popBZ() {
if (index <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
index--;
Baozi baozi = bz[index];
notify();
return baozi;
}
}
// 生产者:生产包子,放到包子筐里
class Producer extends Thread {
private BaoziStack baoziStack;
public Producer(BaoziStack baoziStack) {
this.baoziStack = baoziStack;
}
@Override
public void run() {
// 生产包子(一天生产100个包子)
for (int i = 1; i <= 100; i++) {
Baozi baozi = new Baozi(i);
System.out.println("生产者生产了一个包子ID为: " + i);
baoziStack.pushBZ(baozi);
try {
sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者
class Consumer extends Thread {
private BaoziStack baoziStack;
public Consumer(BaoziStack baoziStack) {
this.baoziStack = baoziStack;
}
@Override
public void run() {
// 一天的消费量为100个包子
for (int i = 1; i <= 100; i++) {
Baozi baozi = baoziStack.popBZ();
System.out.println("消费者消费了一个包子ID为:" + baozi.id + "的包子");
try {
sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
十、拓展
1、Runnable和Callable的区别
- Callable规定的方法是call(),Runnable规定的方法是run().
- Callable的任务执行后可返回值,而Runnable的任务是不能有返回值。
- call方法可以抛出异常,run方法不可以。
2、使用线程池
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
下面一篇会好好讲解这篇的
JDK5.0起提供了线程池相关API: ExecutorService 和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command):执行任务1命令,没有返回值,-般用来执行Runnable
< T > Future< T > submit(Callable< T > task):执行任务, 有返回值,一般来执行Callable
void shutdown():关闭连接池
Executors: 工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool(): 创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor(): 创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n): 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
好处:
1.提高响应速度(减少创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理
corePoolSize:核心池的大小
maximumPoolsize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
3、多线程传参
由于多线程是由继承 Thread 或实现 Runnable 并重写 run() 方法,通过 thread.start() 进行运行的,而本身重写的 run() 方法是不具备传参能力的,
class ThreadA extends Thread{
private String age;
public ThreadA(String age){
this.age = age;
}
@Override
public void run() {
System.out.println("age=" + age);
}
}
public static void main(String[] args) {
String age = new String("12");
ThreadA a = new ThreadA(age);
a.start();
}
结论:
无论 extends Thread 还是 implements Runnable ,传参都需要使用线程初始化的有参构造形式,从而达到多线程传参的目的。也可以做到重载有参构造,传入各式对象。
十一、常见面试题
11.1、普通题目
- 1、 现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
- 2、在Java中Lock接口比synchronized块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?
- 3、在java中wait和sleep方法的不同?
- 4、用Java实现阻塞队列。
- 5、用Java写代码来解决生产者——消费者问题。
- 6、用Java写一个会导致死锁的程序,你将怎么解决?
- 7、什么是原子操作,Java中的原子操作是什么?
- 8、Java中的volatile关键是什么作用?怎样使用它?在Java中它跟synchronized方法有什么不同?
- 9、什么是竞争条件(race condition)?你怎样发现和解决的?
- 10、你将如何使用thread dump?你将如何分析Thread dump?
- 11、java线程的状态转换介绍
- 12、为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
- 13、Java中你怎样唤醒一个阻塞的线程?
- 14、在Java中CycliBarriar和CountdownLatch有什么区别?
- 15、什么是不可变对象,它对写并发应用有什么帮助?
- 16、你在多线程环境中遇到的常见的问题是什么?你是怎么解决它的?
11.2、新题目
- 1、 synchronized的实现原理以及锁优化?
- 2、 锁状态
偏向锁:在无竞争的情况下,把整个同步都消除掉,CAS操作都不做。
轻量级锁:在没有多线程竞争时,相对重量级锁,减少操作系统互斥量带来的性能消耗。但是,如果存在锁竞争,除了互斥量本身开销,还额外有CAS操作的开销。
自旋锁:减少不必要的CPU上下文切换。在轻量级锁升级为重量级锁时,就使用了自旋加锁的方式
锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
锁消除: 虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。
- 3、 ThreadLocal原理,使用注意点,应用场景有哪些?
Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量。
ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
- 4、synchronized和ReentrantLock的区别?
- 5、说说CountDownLatch与CyclicBarrier区别
- 6、Fork/Join框架的理解
- 7、为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
JVM执行start方法,会另起一条线程执行thread的run方法,这才起到多线程的效果~ 「为什么我们不能直接调用run()方法?」如果直接调用Thread的run()方法,其方法还是运行在主线程中,没有起到多线程效果。
- 8、CAS?CAS 有什么缺陷,如何解决?
- 9、如何保证多线程下i++ 结果正确?
- 10、如何检测死锁?怎么预防死锁?死锁四个必要条件
「死锁的四个必要条件:」
互斥:一次只有一个进程可以使用一个资源。其他进程不能访问已分配给其他进程的资源。
占有且等待:当一个进程在等待分配得到其他资源时,其继续占有已分配得到的资源。
非抢占:不能强行抢占进程中已占有的资源。
循环等待:存在一个封闭的进程链,使得每个资源至少占有此链中下一个进程所需要的一个资源。
「如何预防死锁?」
加锁顺序(线程按顺序办事)
加锁时限 (线程请求所加上权限,超时就放弃,同时释放自己占有的锁)
死锁检测
11.3、牛逼面试题
- 什么是线程?
- 线程和进程有什么区别?
- 如何在Java中实现线程?
- 用Runnable还是Thread?
- Thread 类中的start() 和 run() 方法有什么区别?
- Java中Runnable和Callable有什么不同?
- Java中CyclicBarrier 和 CountDownLatch有什么不同?
- ava内存模型是什么?
- Java中的volatile 变量是什么?
- 什么是线程安全?Vector是一个线程安全类吗?
- Java中什么是竞态条件?
- Java中如何停止一个线程?
- 一个线程运行时发生异常会怎样?
- 如何在两个线程间共享数据?
- Java中notify 和 notifyAll有什么区别?
- 为什么wait, notify 和 notifyAll这些方法不在thread类里面?
- 什么是ThreadLocal变量?
- 什么是FutureTask?
- Java中interrupted 和 isInterruptedd方法的区别?
- 为什么wait和notify方法要在同步块中调用?
- 为什么你应该在循环中检查等待条件?
- Java中的同步集合与并发集合有什么区别?
- Java中堆和栈有什么不同?
- 什么是线程池? 为什么要使用它?
- 如何写代码来解决生产者消费者问题?
- 如何避免死锁?
- Java中活锁和死锁有什么区别?
- 怎么检测一个线程是否拥有锁?
- 你如何在Java中获取线程堆栈?
- JVM中哪个参数是用来控制线程的栈堆栈小的
- Java中synchronized 和 ReentrantLock 有什么不同?
- 有三个线程T1,T2,T3,怎么确保它们按顺序执行(确保main()方法所在的线程是Java程序最后结束的线程)?
- Thread类中的yield方法有什么作用?
- Java中ConcurrentHashMap的并发度是什么?
- Java中Semaphore是什么?
- 如果你提交任务时,线程池队列已满。会时发会生什么?
- Java线程池中submit() 和 execute()方法有什么区别?
- 什么是阻塞式方法?
- 你对线程优先级的理解是什么?
- 什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?
- 在多线程中,什么是上下文切换(context-switching)?
- 如何在Java中创建Immutable对象?
- Java中的ReadWriteLock是什么?
- 多线程中的忙循环是什么?
- volatile 变量和 atomic 变量有什么不同?
- 如果同步块内的线程抛出异常会发生什么?
- 单例模式的双检锁是什么?
- 如何在Java中创建线程安全的Singleton?
- 写出3条你遵循的多线程最佳实践
以下三条最佳实践大多数Java程序员都应该遵循:
给你的线程起个有意义的名字。
这样可以方便找bug或追踪。OrderProcessor, QuoteProcessor or TradeProcessor 这种名字比 Thread-1. Thread-2 and Thread-3 好多了,给线程起一个和它要完成的任务相关的名字,所有的主要框架甚至JDK都遵循这个最佳实践。
避免锁定和缩小同步的范围
锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。
多用同步类少用wait 和 notify
首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用wait和notify很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
多用并发集合少用同步集合
这是另外一个容易遵循且受益巨大的最佳实践,并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。如果下一次你需要用到map,你应该首先想到用ConcurrentHashMap。
- 如何强制启动一个线程?
- Java中的fork join框架是什么?
- Java多线程中调用wait() 和 sleep()方法有什么不同?
- 什么是Thread Group?为什么不建议使用它?
- 什么是Java线程转储(Thread Dump),如何得到它?
- 什么是Java Timer类?如何创建一个有特定时间间隔的任务?
- 什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?
- Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?
- 什么是Executor框架?
- Executors类是什么?
- 什么是阻塞队列?如何使用阻塞队列来实现生产者-消费者模型?
- 什么是Callable和Future?
- 什么是FutureTask?
- 什么是并发容器的实现?
- 用户线程和守护线程有什么区别?
- 有哪些不同的线程生命周期?
- 线程之间是如何通信的?
- 为什么Thread类的sleep()和yield()方法是静态的?
- 如何确保线程安全?
- 同步方法和同步块,哪个是更好的选择?
- 如何创建守护线程?
- 线程调度策略?
- 在线程中你怎么处理不可捕捉异常?