进程和线程的区别
进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O):
- 进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
- 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。
另外一个重要区别是,进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位 。
上下文切换
CPU为每个进程分配一个时间段,称作它的时间片。如果在时间片结束时进程还在运行,则暂停这个进程的运行,并且CPU分配给另一个进程(这个过程叫做上下文切换)。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完。
上下文切换(有时也称做进程切换或任务切换)是指 CPU 从一个进程(或线程)切换到另一个进程(或线程)。上下文是指某一时间点 CPU 寄存器和程序计数器的内容。
CPU通过为每个线程分配CPU时间片来实现多线程机制。CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。
但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
寄存器是cpu内部的少量的速度很快的闪存,通常存储和访问计算过程的中间值提高计算机程序的运行速度。
程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体实现依赖于特定的系统。
举例说明 线程A - B
1.先挂起线程A,将其在cpu中的状态保存在内存中。
2.在内存中检索下一个线程B的上下文并将其在 CPU 的寄存器中恢复,执行B线程。
3.当B执行完,根据程序计数器中指向的位置恢复线程A。
实现多线程
我们在程序里面调用了start()方法后,虚拟机会先为我们创建一个线程,然后等到这个线程第一次得到时间片时再调用run()方法。
1.1 继承Thread类或者实现Runnable接口这两种方式,它们之间有什么优劣呢?
由于Java“单继承,多实现”的特性,Runnable接口使用起来比Thread更灵活;
Runnable接口出现更符合面向对象,将线程单独进行对象的封装;
Runnable接口出现,降低了线程对象和线程任务的耦合性;
如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable接口更为轻量;
所以,我们通常优先使用“实现Runnable接口”这种方式来自定义线程类。1.2 同时使用两种方法:
public static void main(String[] args) {
new Thread(() -> System.out.println("我来自Runnable")) {
public void run() {
System.out.println("我来自Thread");
}
}.start();
}输出:我来自Thread
线程的状态
RUNNABLE:
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,处于RUNNABLE状态的线程在Java虚拟机中运行,也有可能在等待其他系统资源(比如I/O)。
Java线程的RUNNABLE状态其实是包括了传统操作系统线程的ready和running两个状态的。
线程的生命周期
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
// 若线程启动失败,从线程组里移除该线程
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
如何正确停止线程
线程中断机制是一种协作机制。通过中断操作并不能直接终止一个线程,而是通知需要被中断的线程自行处理。
- Thread.interrupt():中断线程。这里的中断线程并不会立即停止线程,而是设置线程的中断状态为true(默认是flase);
- Thread.interrupted():测试当前线程是否被中断。线程的中断状态受这个方法的影响,意思是调用一次使线程中断状态设置为true,连续调用两次会使得这个线程的中断状态重新转为false;
- Thread.isInterrupted():测试当前线程是否被中断。与上面方法不同的是调用这个方法并不会影响线程的中断状态;
原理介绍:使用interrupt来通知,而不是强制
解读:想要停止线程,其实是如何正确的用interrupt通知那个线程,以及被停止的线程如何配合。
使用interrupt
- 普通情况下停止线程
public class RightWayStopThreadWithoutSleep implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
public void run() {
int num = 0;
while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 2) {
if (num % 10000 == 0) {
System.out.println(num + "是10000的倍数");
}
num++;
}
System.out.println("任务运行结束了");
}
}- 线程可能被阻塞的情况下被停止
public class RightWayStopThreadWithSleep {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (num < 300 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
}
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(500);
thread.interrupt();
}
}
/**
0是100的倍数
100是100的倍数
200是100的倍数
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at threadcoreknowledge.stopthreads.RightWayStopThreadWithSleep.lambda$main$0(RightWayStopThreadWithSleep.java:21)
at java.lang.Thread.run(Thread.java:748)
*/如果线程在每次迭代后被阻塞
如果在执行过程中,每次循环都会调用sleep或wait等方法,那么不需要每次迭代都检查是否已中断。
public class RightWayStopThreadWithSleepEveryLoop {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
// 不需要做是否中断的检测
while (num < 10000) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}如果while里面放try/catch,会导致中断失效
因为sleep会清除中断信号,将中断标记位设置成 false(源码分析待补全)
public class CantInterrupt {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
while (num < 10000 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
实际开发中的两种最佳实践 不应屏蔽中断
优先选择:传递中断
catch了InterruptException之后的优先选择:在方法签名中抛出异常,那么在run()就会强制try/catch
public class RightWaySropThreadInProd1 implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWaySropThreadInProd1());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
public void run() {
while (true) {
System.out.println("go");
try {
throwInMethod();
} catch (InterruptedException e) {
// 保存日志,停止程序
System.out.println("保存日志");
e.printStackTrace();
}
}
}
private void throwInMethod() throws InterruptedException {
Thread.sleep(1000);
}
}不想或无法传递:恢复中断
在catch子语句中调用Thread.currentThread().interrupt()来恢复设置中断状态,以便在后续的执行中,依然能够检查到刚才发生了中断。(使得其他代码有办法知道它发生了中断)
public class RightWaySropThreadInProd2 implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWaySropThreadInProd2());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("程序运行结束");
break;
}
reInterrupt();
}
}
private void reInterrupt() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}
响应中断的方法总结列表
Object.wait()/wait(long)/wait(long, int)
Thread.sleep(long)/sleep(long, int)
Thread.join()/join(long)/join(long, int)
java.util.cocurrent.BlockingQueue.take()/put(E)
java.util.cocurrent.locks.Lock.lockInterruptibly()
java.util.cocurrent.CountDownLatch.await()
java.util.cocurrent.CyclicBarrier.await()
java.util.cocurrent.Exchanger.exchange(V)
java.nio.channels.InterruptibleChannel相关方法
java.nio.channels.Selector相关方法
错误的停止方法
被弃用的stop,suspend,resume方法(待补全)
stop() 弃用原因:
原因1:即刻停止run()方法中剩余的全部工作,包括在catch或finally语句中,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭;
原因2:会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题;
suspend() resume()弃用原因
suspend()和resume()必须要成对出现,否则非常容易发生死锁;
不推荐使用suspend()去挂起线程的原因,是因为suspend()在导致线程暂停的同时,并不会去释放任何锁资源。其他线程都无法访问被它占用的锁。直到对应的线程执行resume()方法后,被挂起的线程才能继续,从而其它被阻塞在这个锁的线程才可以继续执行。
如果一个线程在resume目标线程之前尝试持有这个重要的系统资源锁再去resume目标线程,这两条线程就相互死锁了,也就冻结线程。
用volatile设置boolean标记位
原因:如果线程发生阻塞了,它将无法执行判断标识位的代码,阻塞无法收到停止线程的通知。线程依然不能停止。
停止线程相关重要函数解析(待补全)
interrupt方法
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}判断是否已被中断相关方法
- static boolean interrupted()
- boolean isInterrupted()
- Thread.interrupted()的目的对象
线程的各个属性
线程的异常
代码举例:
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private String name;
public MyUncaughtExceptionHandler(String name) {
this.name = name;
}
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.WARNING, "线程异常,终止啦" + t.getName());
System.out.println(name + "捕获了异常" + t.getName() + "异常");
}
}public class UseOwnUncaughtExceptionHandler implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler("捕获器1"));
new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-1").start();
Thread.sleep(300);
new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-2").start();
Thread.sleep(300);
new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-3").start();
Thread.sleep(300);
new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-4").start();
}
public void run() {
throw new RuntimeException();
}
}输出:
七月 11, 2020 4:02:09 下午 threadcoreknowledge.uncaughtexception.MyUncaughtExceptionHandler uncaughtException
警告: 线程异常,终止啦MyThread-1
捕获器1捕获了异常MyThread-1异常
七月 11, 2020 4:02:09 下午 threadcoreknowledge.uncaughtexception.MyUncaughtExceptionHandler uncaughtException
警告: 线程异常,终止啦MyThread-2
捕获器1捕获了异常MyThread-2异常
七月 11, 2020 4:02:09 下午 threadcoreknowledge.uncaughtexception.MyUncaughtExceptionHandler uncaughtException
警告: 线程异常,终止啦MyThread-3
捕获器1捕获了异常MyThread-3异常
七月 11, 2020 4:02:10 下午 threadcoreknowledge.uncaughtexception.MyUncaughtExceptionHandler uncaughtException
警告: 线程异常,终止啦MyThread-4
捕获器1捕获了异常MyThread-4异常
Process finished with exit code 0Thread及Object中线程相关的重要方法
类 方法名 简介 Thread sleep相关 相关,指重写的方法 join 等待其他线程执行完毕 yield相关 放弃以获取到的CPU资源 currentThread 获取当前执行线程的引用 start,run相关 启动线程相关 interrupt相关 中断线程 stop(),suspend(),resume()相关 已废弃 Object wait/notify/notifyAll相关 让线程暂时休眠或唤醒 currentThread():静态方法,返回对当前正在执行的线程对象的引用;
start():开始执行线程的方法,java虚拟机会调用线程内的run()方法;
yield():yield在英语里有放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续运行这个线程的;
sleep():静态方法,使当前线程睡眠一段时间;
join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的是Object类的wait方法实现的;