简述线程、进程、程序的基本概念?
程序
程序,是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程
进程,是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
线程
线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
Java 线程是重量级的,每个线程默认使用 1024KB 的内存,所以一个 Java 进程是无法开启大量线程的
三者之间的关系
- 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
- 进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
线程有什么优缺点?
-
优点
- 使用多线程可以把程序中占据时间长的任务放到后台去处理,如图片、视屏的下载。
- 发挥多核处理器的优势,并发执行让系统运行的更快、更流畅,用户体验更好。
-
缺点
- 大量的线程降低代码的可读性。
- 更多的线程需要更多的内存空间。
- 当多个线程对同一个资源出现争夺时候要注意线程安全的问题。
你了解守护线程吗?它和非守护线程有什么区别?
Java 中的线程分为两种:守护线程(Daemon)和用户线程(User)。
- 任何线程都可以设置为守护线程和用户线程,通过方法Thread#setDaemon(boolean on) 设置。true则把该线程设置为守护线程,反之则为用户线程。
- Thread#setDaemon(boolean on) 方法,必须在Thread#start() 方法之前调用,否则运行时会抛出异常。
守护线程(Deamon Thread):守护线程是一个低优先级的线程,在后台运行以执行垃圾回收等任务.
守护线程的相关方法
setDeamon
此方法用于将当前线程标记为守护程序线程或用户线程。例如,如果我有一个用户线程tU,那么tU.setDaemon(true)会使它成为守护程序线程。另一方面,如果我有一个守护程序线程tD,那么通过调用tD.setDaemon(false)会使它成为用户线程。
public final void setDaemon(boolean on)
parameters:
on : 如果参数为True则设置为守护线程,如果为False则设置为用户线程
exceptions:
IllegalThreadStateException: 如果这个线程是活动的
SecurityException: 如果当前线程不能修改此线程
isDeamon
此方法用于检查当前是守护进程。如果线程是守护进程,则返回true,否则返回false。
public final boolean isDaemon()
返回:
如果此线程是守护程序线程,则此方法返回true;
否则是假的
方法使用案例
package com.chen.thread;
/**
* @program: JavaInterviewQuestion
* @description: 守护线程案例
* @author: Mr.Wang
* @create: 2019-08-06 16:02
**/
public class DeamonThreadDemo extends Thread {
public DeamonThreadDemo(String name) {
super(name);
}
@Override
public void run() {
//判断当前线程是否为Deamon Thread
if (Thread.currentThread().isDaemon()) {
System.out.println(getName() + "为守护线程(Deamon Thread)");
} else {
System.out.println(getName() + "为用户线程(User Thread)");
}
}
public static void main(String[] args) {
DeamonThreadDemo threadDemo1 = new DeamonThreadDemo("1号线程");
DeamonThreadDemo threadDemo2 = new DeamonThreadDemo("2号线程");
DeamonThreadDemo threadDemo3 = new DeamonThreadDemo("3号线程");
//将User Thread 设置为 Deamon Thread
threadDemo1.setDaemon(true);
threadDemo1.start();
threadDemo2.start();
//将User Thread 设置为 Deamon Thread
threadDemo3.setDaemon(true);
threadDemo3.start();
}
}
异常IllegalThreadStateException
package com.chen.thread;
/**
* @program: JavaInterviewQuestion
* @description: 如果在启动线程之后再将某线程设置为守护线程会引发异常IllegalThreadStateException
* @author: Mr.Wang
* @create: 2019-08-06 16:02
**/
public class DeamonThreadExceptionDemo extends Thread {
@Override
public void run() {
System.out.println("当前线程名: " + Thread.currentThread().getName());
System.out.println("检查当前线程是否为守护线程: "
+ Thread.currentThread().isDaemon());
}
public static void main(String[] args) {
DeamonThreadExceptionDemo t1 = new DeamonThreadExceptionDemo();
DeamonThreadExceptionDemo t2 = new DeamonThreadExceptionDemo();
t1.start();
// 线程启动之后再设置该线程为守护线程时会引发异常
t1.setDaemon(true);
t2.start();
}
}
守护线程(Deamon Thread)和用户线程(UserThread)之间的唯一的区别
程序运行完毕,JVM 会等待非守护线程完成后关闭,但是 JVM 不会等待守护线程。
-
判断虚拟机(JVM)何时离开,Daemon 是为其他线程提供服务,如果全部的 User Thread 已经撤离,Daemon没有可服务的线程,JVM 撤离。
-
也可以理解为守护线程是 JVM 自动创建的线程(但不一定),用户线程是程序创建的线程。比如,JVM 的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是 Java虚拟机上仅剩的线程时,Java 虚拟机会自动离开。
Thread Dump 打印出来的线程信息,含有 daemon 字样的线程即为守护进程。可能会有:服务守护进程、编译守护进程、Windows 下的监听 Ctrl + break 的守护进程、Finalizer 守护进程、引用处理守护进程、GC 守护进程。
什么是线程组,为什么在 Java 中不推荐使用?(了解)
ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。
为什么不推荐使用?ThreadGroup API 比较薄弱,它并没有比 Thread 提供了更多的功能。它有两个主要的功能:一是获取线程组中处于活跃状态线程的列表;二是设置为线程设置未捕获异常处理器(uncaught exception handler)。但在 Java5 中 Thread 类也添加了 #setUncaughtExceptionHandler(UncaughtExceptionHandler eh) 方法,所以 ThreadGroup 是已经过时的,不建议继续使用
什么是多线程上下文切换?
多线程会共同使用一组计算机上的 CPU ,而线程数大于给程序分配的 CPU 数量时,为了让各个线程都有执行的机会,就需要轮转使用 CPU 。
不同的线程切换使用 CPU 发生的切换数据等,就是上下文切换。
- 在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称作“切换桢”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。
- 上下文切换是存储和恢复 CPU 状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。
Java 中用到的线程调度算法是什么?
假设计算机只有一个 CPU ,则在任意时刻只能执行一条机器指令,每个线程只有获得 CPU 的使用权才能执行指令。
- 所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。
- 在运行池中,会有多个处于就绪状态的线程在等待 CPU ,Java 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。
有两种调度模型:分时调度模型和抢占式调度模型。
- 分时调度模型是指让所有的线程轮流获得 CPU 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。
- Java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用 CPU ,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用 CPU 。处于运行状态的线程会一直运行,直至它不得不放弃 CPU .
什么是线程饥饿?
饥饿,一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
Java 中导致饥饿的原因:
- 高优先级线程吞噬所有的低优先级线程的 CPU 时间。
- 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
- 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。
你对线程优先级的理解是什么?
每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。
- 我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从1-10),1代表最低优先级,10 代表最高优先级。
- Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。
线程的生命周期?
线程一共有五个状态,分别如下:
- 新建(new):当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。例如:Thread t1 = new Thread() 。
- 可运行(runnable):线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start 方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权。例如:t1.start() 。
- 运行(running):线程获得 CPU 资源正在执行任务(#run() 方法),此时除非此线程自动放弃 CPU 资源或者有优先级更高的线程进入,线程将一直运行到结束。
- 死亡(dead):当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。
- 自然终止:正常运行完 #run()方法,终止。
- 异常终止:调用 #stop() 方法,让一个线程终止运行
- 堵塞(blocked):由于某种原因导致正在运行的线程让出 CPU 并暂停自己的执行,即进入堵塞状态。直到线程进入可运行(runnable)状态,才有机会再次获得 CPU 资源,转到运行(running)状态。阻塞的情况有三种:
- 正在睡眠:调用 #sleep(long t) 方法,可使线程进入睡眠方式。
一个睡眠着的线程在指定的时间过去可进入可运行(runnable)状态。
- 正在等待:调用 #wait() 方法。
调用 notify() 方法,回到就绪状态。
- 被另一个线程所阻塞:调用 #suspend() 方法。
调用 #resume() 方法,就可以恢复。
- 中间一行是线程的顺畅的执行过程的四个状态。其上下两侧,是存在对应的情况,达到阻塞状态和恢复执行的过程。
- 新建(new)和死亡(dead)是单向的状态,不可重复。
- 理解线程的状态,可以用早起坐地铁来比喻这个过程:
- 还没起床:sleeping 。
- 起床收拾好了,随时可以坐地铁出发:Runnable 。
- 等地铁来:Waiting 。
- 地铁来了,但要排队上地铁:I/O 阻塞 。
- 上了地铁,发现暂时没座位:synchronized 阻塞。
- 地铁上找到座位:Running 。
- 到达目的地:Dead 。
如何结束一个一直运行的线程?
一般来说,有两种方式:
- 方式一,使用退出标志,这个 flag 变量要多线程可见。
在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的线程用来作为是否中断的信号,通知中断线程的执行。
- 方式二,使用 interrupt 方法,结合 isInterrupted 方法一起使用。
如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?这种情况经常会发生,比如当一个线程由于需要等候键盘输入而被阻塞,或者调用 Thread#join() 方法,或者 Thread#sleep(…) 方法,在网络中调用ServerSocket#accept() 方法,或者调用了DatagramSocket#receive() 方法时,都有可能导致线程阻塞,使线程处于处于不可运行状态时。即使主程序中将该线程的共享变量设置为 true ,但该线程此时根本无法检查循环标志,当然也就无法立即中断。
这里我们给出的建议是,不要使用 Thread#stop()· 方法,而是使用 Thread 提供的#interrupt()` 方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,退出堵塞代码。
所以,方式一和方式二,并不是冲突的两种方式,而是可能根据实际场景下,进行结合。
一个线程如果出现了运行时异常会怎么样?
- 如果这个异常没有被捕获的话,这个线程就停止执行了。
- 另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放。
创建线程的方式及实现?
Java 中创建线程主要有三种方式:
- 方式一,继承 Thread 类创建线程类。
- 方式二,通过 Runnable 接口创建线程类。
- 方式三,通过 Callable 和 Future 创建线程。
创建线程的三种方式的对比:
- 使用方式一
- 优点:编写简单,如果需要访问当前线程,则无需使用 Thread#currentThread() 方法,直接使用 this 即可获得当前线程。
- 缺点:线程类已经继承了 Thread 类,所以不能再继承其他父类。
- 使用方式二、或方式三
- 优点
-
线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
-
多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
Runnable runner = new Runnable(){ ... }; // 通过new Thread(target, name) 方法创建新线程 new Thread(runna,"新线程1").start(); new Thread(runna,"新线程2").start();
-
【最重要】可以使用线程池。
-
- 缺点:编程稍微复杂,如果要访问当前线程,则必须使用Thread#currentThread() 方法。
- 优点
start 和 run 方法有什么区别?
- 当你调用 start 方法时,你将创建新的线程,并且执行在 run 方法里的代码。
- 但是如果你直接调用 run 方法,它不会创建新的线程也不会执行调用线程的代码,只会把 run 方法当作普通方法去执行。
一个线程运行时发生异常会怎样?
如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候 JVM 会使用 Thread#getUncaughtExceptionHandler() 方法来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的 #uncaughtException(exception) 方法进行处理。
如何使用 wait + notify 实现通知机制?
在 Java 发展史上,曾经使用 suspend、resume 方法对于线程进行阻塞唤醒,但随之出现很多问题,比较典型的还是死锁问题。
解决方案可以使用以对象为目标的阻塞,即利用 Object 类的 wait 和 notify方法实现线程阻塞。
- 首先,wait、notify 方法是针对对象的,调用任意对象的 wait 方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify 方法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执行。
- 其次,wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。
通过 wait + notify 的组合,可以通知机制,不过我们也可以使用其它工具.
- Condition
- CountDownLatch
- Queue
- Future
Thread类的 sleep 方法和对象的 wait 方法都可以让线程暂停执行,它们有什么区别?
- sleep 方法,是线程类 Thread的静态方法。调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态)
- wait 方法,是 Object 类的方法。调用对象的 #wait() 方法,会导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的 #notify() 方法(或#notifyAll()方法)时,才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。
请说出与线程同步以及线程调度相关的方法?
- wait 方法,使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁。
- sleep 方法,使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常。
- notify 方法,唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关。
- notityAll 方法,唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态。
notify 和 notifyAll 有什么区别?
当一个线程进入 wait 之后,就必须等其他线程 notify/notifyAll 。
- 使用 notifyAll,可以唤醒所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify只能唤醒一个。
- 如果没把握,建议 notifyAll ,防止 notify 因为信号丢失而造成程序错误。
参考资源:《wait 和 notify 的坑》 文章。
为什么 wait, notify 和 notifyAll 这三方法不在 Thread 类里面?
一个很明显的原因是 Java 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。
由于 wait,notify 和 notifyAll 方法都是锁级别的操作,所以把它们定义在 Object 类中,因为锁属于对象。
为什么 wait 和 notify 方法要在同步块中调用?
- Java API 强制要求这样做,如果你不这么做,你的代码会抛出 IllegalMonitorStateException 异常。
- 还有一个原因是为了避免 wait 和 notify 之间产生竞态条件。
为什么你应该在循环中检查等待条件?
处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。
所以,我们不能写 if (condition) 而应该是 while (condition) ,特别是 CAS 竞争的时候。示例代码如下:
// The standard idiom for using the wait method
synchronized (obj) {
while (condition does not hold) {
obj.wait(); // (Releases lock, and reacquires on wakeup)
}
... // Perform action appropriate to condition
}