一、如何创建线程?哪种好?
有 4 种方式可以用来创建线程:
- 继承 Thread 类
- 实现 Runnable 接口
- 应用程序可以使用 Executor 框架来创建线程池
- 实现 Callable 接口
⚠️创建线程最好使用setName方法设置线程名称,便于后期排查
实现 Runnable 接口比继承 Thread 类所具有的优势:
- 适合多个相同的程序代码的线程去处理同一个资源
- 可以避免 java 中的单继承的限制
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
- 线程池只能放入实现 Runable 或 callable 类线程,不能直接放入继承 Thread 的类
- runnable 实现线程可以对线程进行复用,因为 runnable 是轻量级的对象,重复 new 不会耗费太大资源,而 Thread 则不然,它是重量级对象,而且线程执行完就完了,无法再次利用
二、线程状态
- 新建状态(New):新创建了一个线程对象
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的 start()方法。 该状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权
- 运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止 运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- 等待阻塞:运行的线程执行 wait()方法,JVM 会把该线程放入等待池中。(wait 会释放持有的锁)
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池中
- 其他阻塞:运行的线程执行 sleep()或 join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处 理完毕时,线程重新转入就绪状态。(注意,sleep 是不会释放持有的锁)
- 死亡状态(Dead):线程执行完了或者因异常退出了 run()方法,该线程结束生命周期。
三、一般线程和守护线程的区别
所谓守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此, 当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
区别:
唯一的区别是判断虚拟机(JVM)何时离开,Daemon 是为其他线程提供服务,如果全部的 User Thread 已经撤离,Daemon 没有可服务的线程,JVM 撤离。也可以理解为守护线程是 JVM 自动创建的线程(但不一定),用户线程是程序创建的线程;比如 JVM 的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是 Java 虚拟机上仅剩的线程时,Java 虚拟机会自动离开。
在使用守护线程时需要注意一下几点:
- thread.setDaemon(true)必须在 thread.start()之前设置,否则会抛出一个 IllegalThreadStateException 异常。你不能把正在运行的常规线程设置为守护线程
- 在 Daemon 线程中产生的新线程也是 Daemon 的
- 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断
四、sleep wait yield notify notifyAll join
1. Sleep 与 wait 区别
- sleep 是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用 sleep 不会释放对象锁。sleep() 使当前线程进入阻塞状态,在指定时间内不会执行
- wait 是 Object 类的方法,对此对象调用 wait 方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出 notify 方法(或 notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态
区别比较:
- 继承:这两个方法来自不同的类分别是 Thread 和 Object
- 释放锁:最主要是 sleep 方法没有释放锁,而 wait 方法释放了锁,使得其他线程可以使用同步控制块或者方法
- 使用范围:wait,notify 和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用
- 处理异常:sleep 必须捕获异常,而 wait,notify 和 notifyAll 不需要捕获异常
sleep
sleep 方法属于 Thread 类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了 sleep 方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。
但在 sleep 的过程中过程中有可能被其他对象调用它的 interrupt(),产生 InterruptedException 异常,如果你的程序不捕获这个异常,线程就会异常终止,进入 TERMINATED 状态;如果你的程序捕获了这个异常,那么程序就会继续执行 catch 语句块(可能还有 finally 语句块)以及以后的代码。
注意 sleep()方法是一个静态方法,也就是说他只对当前对象有效。通过 t.sleep()让 t
对象进入 sleep,这样的做法是错误的,它只会是使当前线程被 sleep 而不是 t 线程。
wait
wait 属于 Object 的成员方法,一旦一个对象调用了 wait 方法,必须要采用 notify()
和 notifyAll()方法唤醒该进程。如果线程拥有某个或某些对象的同步锁,那么在调用了 wait()
后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了 wait()方法的对象。wait()方法也同样会在 wait 的过程中有可能被其他对象调用 interrupt()方法而产生。
yield join notify notifyAll
yield**方法是停止当前线程,让同等优先权的线程或更高优先级的线程有执行的机会。如果没有的话,那么 yield()方法将不会起作用,并且由可执行状态后马上又被执行。
join方法**是用于在某一个线程的执行过程中调用另一个线程执行,等到被调用的线程执行结束后,再继续执行当前线程。如:t.join();``//``主要用于等待 ``t ``线程运行结束
,若无此句,
main 则会执行完毕,导致结果不可预测。
notify**方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。
notifyAll方法**会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系
统的实现
五、中断线程
中断线程的方法
- 使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止。
- 通过 return 退出 run 方法
- 通过对有些状态中断抛异常退出 thread.interrupt() 中断。
- 使用 stop 方法强行终止线程(过期)
中断线程可能出现的问题
使用Thread.interrupt()
并不能使得线程被中断,线程还是会执行。最靠谱的方法就是设置一个全局的标记位,然后再Thread中去检查这个标记位,发现标记位改变则中断线程。
class Example extends Thread {
/**
* 全局中断线程标记位
*/
volatile boolean stop = false;
public static void main(String args) throws Exception {
Example thread = new Example();
System.out.println("Starting thread..");
thread.start();
Thread.sleep(3000);
System.out.println("Asking thread to stop..");
thread.stop = true;
Thread.sleep(3000);
System.out.println("stopping application..");
//System. exit(0);
}
@Override
public void run() {
while (!stop) {
System.out.println("Thread is running..");
long time = System.currentTimeMillis();
while ((System.currentTimeMillis() - time < 1000) && (!stop)) {
}
}
System.out.println("Thread exiting under request");
}
}
<br />
六、多线程如何避免死锁
什么是死锁?
所谓死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。死锁产生的 4 个必要条件:
互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求, 而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。
如何确保 N 个线程可以访问 N 个资源同时又不导致死锁?
使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。
- 加锁顺序:线程按照一定的顺序加锁
- 加锁时限:线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁
- 死锁检测
<br />
七、多线程的好处以及问题
- 发挥多核 CPU 的优势
- 防止阻塞
- 便于建模
这是另外一个没有这么明显的优点了。假设有一个大的任务 A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务 A 分解成几个小任务, 任务 B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
问题
线程安全问题
八、多线程共用一个数据变量注意什么?
多线程如何进行信息交互
Object中的方法,wait(),notify(),notifyAll();
多线程共用一个数据变量需要注意什么
- 当我们在线程对象(Runnable)中定义了全局变量,run方法会修改该变量时,如果有多个线程同时使用线程对象,那么就会造成全局变量的值被同时修改,造成错误
- ThreadLocal是JDK引入的一种机制,它用于解决线程共享变量,使用ThreadLocal声明的变量,即使在线程中属于全局变量,针对每个线程来讲,这个变量也是独立的
- volatile变量每次被线程访问时,都强迫线程从主内存中重读该变量的最新值,而当该变量发生修改变化时,也会强迫线程将最新的值刷新回主内存中。这样一来,不同的线程都能及时的看到该变量的最新值
JVM线程死锁,你该如何判断是因为什么?如果用VisualVM,dump线程信息出来,会有哪些信息
- 常常需要在隔两分钟后再次收集一次thread dump,如果得到的输出相同,仍然是大量thread都在等待给同一个地址上锁,那么肯定是死锁了。
九、线程通信方式
1. 同步
同步是指多个线程通过 synchronized 关键字这种方式来实现线程间的通信。
2. wait/notify 机制
十、线程池
什么是线程池
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
设计一个动态大小的线程池,如何设计,应该有哪些方法
1. 一个线程池包括以下四个基本组成部分:
- 线程管理器(ThreadPool):用于创建并管理线程池,包括创建线程迟,销毁线程池,添加新任务;
- 工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
- 任务接口(Task):毎个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
- 任务队列(TaskQueue):用于存放没有处理的任务。提供一种缓冲机制
所包含的方法
- private ThreadPool()创建线程池
- public static ThreadPool getThreadPool()获得一个默认线程个数的线程池
- public void execute(Runnable task)执行任务,其实只是把任务加入任务队列,什么时候执行有线程池管理器决定public void execute(Runnable[] task)批量执行任务,其实只是把任务加入任务队列,什么时候执行有线程池管理器决定
- public void destroy()销毁程池,该方法保证在所有任务都完成的情况下才销毁所有线程,否则等待任务完成才销毁
- public int getWorkThreadNumber()返回工作线程的个数
- public int getFinishedTaskNumber()返回已完成任务的个数,这里的已完成是只出了任务队列的任务个数,可能该任务并没有实际执行完成
- public void addThread()在保证线程池中所有线程正在执行,并且要执行线程的个数大于某一值时。增加线程池中线程的个数
- public void reduceThread()在保证线程池中有很大一部分线程处于空闲状态,并且空闲状态的线程在小于某值时,减少线程池中线程的个数
Java线程池的实现
一般线程池有 corePoolSize,maximumPoolSize,任务队列,等待时间这几个比较重要参数。
- 当线程数小于 corePoolSize时,直接创建线程执行task
- 当线程数大于等于corePoolSize时,将task放入队列
- 当队列放不了task时,又创建线程来执行task
- 当线程数大于 maximumPoolSize时,根据不同策略抛弃task之类的。
Java中 Executor框架对线程池的实现
FixedThreadPool,SingleThreadExecutor和CachedThreadPool之间区别,和它们构造时,上述几个参数的不同。
CachedThreadPool如果需要就创建线程,使用完的线程会暂时缓存,不会立刻释放,只有当空闲时间超出一段时间(默认为60s)后,线程池才会销毁该线程适用于耗时较短的任务 任务处理速度>任务提交速度
FixedThreadPool定长大小的线程池
SingleThreadExecutor可以确保任何线程中都只有唯一的任务在运行,相当于FiⅸedThreadPool(1)
线程中抛出异常怎么办
当单线程的程序发生一个未捕获的异常时我们可以采用 `try....catch` 进行异常的捕获,但是在多线程环境中,线程抛出的异常是不能用 `try....catch` 捕获的,这样就有可能导致一些问题的出现,比如异常的时候无法回收一些系统资源,或者没有关闭当前的连接等等。
public class WitchcaughtThread {
public static void main(String args) {
Thread thread = new Thread(new Task() {
@Override
protected Object call() throws Exception {
return null;
}
});
thread.setUncaughtExceptionHandler(new ExceptionHandler());
thread.start();
}
}
class ExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("==Exception");
}
}
简单的说,如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候 JVM 会使用 Thread.getUncaughtExceptionHandler()来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 uncaughtException()方法进行处理。
线程池相关问题
- java 线程池达到提交上限的具体情况
线程池用法
Java 多线程,线程池有哪几类,每一类的差别。要你设计的话,如何实现一个线程池线
程池的类型,固定大小的线程池内部是如何实现的,等待队列是用了哪一个队列实现线程池种类和工作流程(重点讲 newcached 线程池)
线程池工作原理
- 比如 corePoolSize 和 maxPoolSize 这两个参数该怎么调线程池使用了什么设计模式
线程池使用时一般要考虑哪些问题
线程池的配置 Excutor 以及 Connector 的配置
AysncTask 每来一个任务都会创建一个线程来执行吗?(否,线程池的方式实现的)
介绍下 AsyncTask 的实现原理