如果觉得有用可以关注一下公众号:求赞求关注
二、Java线程
2.1 实现线程的三种方式
java实现线程有三种方式:继承Thread类、实现Runnable接口和实现Callable接口
2.1.1 继承Thread类
Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。 启动线程的唯一方
法就是通过 Thread 类的 start()实例方法。 start()方法是一个 native 方法,它将启动一个新线
程,并执行 run()方法。
public class MyThread extends Thread{
// 重写run()方法,放入线程执行的内容
@Override
public void run() {
int i = 0;
while(i < 100) {
System.out.println("Thread running: " + i++);
}
}
public static void main(String[] args) {
MyThread thread = new MyThread();
// thread.start()启动线程,会自动调用thread.run()
thread.start();
}
}
2.1.2 实现Runnable接口
由于Java只允许单继承,所以如果一个类已经继承了其他类,则无法再继承Thread。此时可以实现Runnable接口,然后根据Runnable对象创建一个Thread,可以理解Thread去承载这个对象的运行,实际执行的是Runnable对象的run()方法。
public class MyRunnable implements Runnable{
@Override
public void run() {
int i = 0;
while(i < 100) {
System.out.println("Thread running: " + i++);
}
}
public static void main(String[] args) {
// 创建实现了Runnable的对象
MyRunnable runnable = new MyRunnable();
// 根据Runnable的对象创建Thread,thread.start()后会执行runnable.run()方法,
Thread thread = new Thread(runnable);
thread.start();
}
}
2.1.3 实现Callable接口,允许有返回值的线程
前面的Thread本质上也是实现了Runnable接口,Runnable接口实现线程不允许有返回值(run()方法返回值类型为void)。有返回值的任务必须实现 Callable 接口,无返回值的任务必须 Runnable 接口 。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService(关于线程池知识后面会详细介绍) ,就可以实现传说中有返回结果的多线程了。
package org.numb.concurrency.chapter02;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int i = 0;
while(i < 100) {
System.out.println("Thread running: " + i++);
}
return i;
}
public static void main(String[] args) {
// 创建callable对象,每一个callable对象可以看做一个任务
MyCallable callable = new MyCallable();
// 创建一个线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// Callable必须使用ExecutorService.submit来提交callable任务,返回值用Future对象去接收
Future<Integer> result = threadPool.submit(callable);
// get()方法获取线程返回值
try {
int i = (int) result.get();
System.out.println(i);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
2.1.4 Runnable与Callable的区别
-
有无返回值
有返回值的线程只能实现
Callable
;无返回值的线程只能实现Runnable
-
能否抛出异常
Callable
可以抛出异常;Runnable
不能抛出异常。 -
线程运行方式不同
Callable
和Runnable
都可以通过线程池提交,而Thread
方式只接受Runnable
。
2.2 线程的生命周期
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换 。
2.2.1 新建(New)
使用Thread thread = new Thread()
后,thread线程就处于新建状态,此时JVM为其分配内存,并初始化成员变量值。
2.2.2 就绪(Runnable)
使用thread.start()
后,thread
线程处于就绪状态,此时并不一定马上回执行该线程。JVM首先会为其创建方法调用栈和程序技术器,等待系统的调度运行。
2.2.3 运行(Running)
就绪的线程(调用thread.start()后)获得到CPU后开始执行线程执行体,即执行thread.run()
方法体。这个过程由系统决定,用户并不感知,只需要调用thread.start()将线程设为就绪即可,在CPU资源充足时一般都是很快就从就绪状态切换为运行状态。
2.2.4 阻塞(Blocked)
阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了cpu的时间片( cpu timeslice),暂时停止运行。直到线程进入就绪(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
-
等待阻塞(Wait Set|等待池)
运行(running)的线程执行
o.wait()
方法, JVM 会把该线程放入等待队列(Wait Se)中。 -
同步阻塞(Entry Set|锁池)
运行(running)的线程在
获取对象的同步锁
时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(Entry Set)中 -
其他阻塞
运行(running)的线程执行
Thread.sleep(long ms)
或thread.join()
方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、 join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
2.2.5 死亡(Dead)
线程会以下面三种方式结束,结束后就是死亡状态。
-
正常结束
run()
或call()
方法执行完成,线程正常结束。 -
异常结束
线程抛出一个未捕获的 Exception 或 Error。 -
调用 stop
直接调用该线程的
stop()
方法来结束线程,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。 导致了该线程所持有的所有锁的突然释放(不可控制) ,通常容易导致死锁,不推荐使用。
public class SocketThread implements Runnable {
@Override
public void run() {
///3、线程运行中
System.out.println("运行");
try (ServerSocket server = new ServerSocket(8080)) {
///4、调用accept(), 线程阻塞
System.out.println("阻塞");
Socket accept = server.accept();
BufferedReader reader = new BufferedReader(new InputStreamReader(accept.getInputStream()));
System.out.println(reader.readLine());
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
// 5、结束
System.out.println("结束");
}
public static void main(String[] args) {
// 1、新建线程
System.out.println("新建");
Thread thread = new Thread(new SocketThread());
///2、调用start()后,线程就绪
System.out.println("就绪");
thread.start();
}
}
上述程序运行后很快会输出“新建->就绪->运行->阻塞”,直到收到socket连接后,输出“结束”,完成整个线程生命周期。
2.3 结束线程的方法
-
正常结束
run()方法或者call()方法结束,线程执行完成
-
异常结束
运行过程中抛出未捕获的Exception或者Error
-
调用interrupt()中断线程(推荐)
当前线程自身始终可以调用
interrupt()
方法。如果非当前线程调用,则首先会调用checkAccess()
方法判断当前运行中的线程能否操作此线程,如果不能会抛出SecurityException
。调用interrupt(),并不会立刻退出该线程,而是需要捕获异常或通过中断状态判断退出。- 当线程中调用
wait()
、join()
或sleep()
处于阻塞状态时,interrupt()会清除线程中断状态,并抛出InterruptedException
- 如果此线程在nio中的
InterruptibleChannel
上的 I/O 操作中被阻塞,则interrupt()会使该通道被关闭,设置线程的中断状态,并且抛出java.nio.channels.ClosedByInterruptException
。 - 如果此线程在
java.nio.channels.Selector
中被阻塞,则interrupt()会设置该线程的中断状态,并将立即从选择操作返回,可能具有非零值,就像调用了Selector的唤醒方法一样。 - 上述情况以外,调用interrupt()会将中断状态置位(如Socket中的
accept()
和connect()
等)
例程1.
sleep()
阻塞时,调用interrupt(),会抛出InterruptedException
,但中断状态不会置位。因此若想退出线程必须在处理异常中退出。public class InterruptedThread extends Thread { private final SimpleDateFormat DATE = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Override public void run() { System.out.println("线程开始运行..."); try { System.out.println("线程休眠开始时间:" + DATE.format(System.currentTimeMillis())); Thread.sleep(10 * 1000); } catch (InterruptedException e) { System.out.println("当前时间:" + DATE.format(System.currentTimeMillis())); System.out.println("线程被打断..."); System.out.println("线程中断状态:" + isInterrupted()); } System.out.println("线程休眠结束时间:" + DATE.format(System.currentTimeMillis())); } public static void main(String[] args) throws InterruptedException { Thread thread = new InterruptedThread(); thread.start(); Thread.sleep(5 * 1000); thread.interrupt(); } }
输出:
线程开始运行... 线程休眠开始时间:2022-01-16 16:33:27 当前时间:2022-01-16 16:33:32 线程被打断... 线程中断状态:false 线程休眠结束时间:2022-01-16 16:33:32 Process finished with exit code 0
例程2 .通过中断状态来退出当前线程,如果线程没有被interrupted,则一直尝试连接socket,设置单次连接超时为5秒
public class InterruptedThread extends Thread { @Override public void run() { try (ServerSocket server = new ServerSocket(8080)) { // 如果不被interrupted,则一直尝试socket连接 while (!isInterrupted()) { // 调用accept(), 线程阻塞,设置最大阻塞时间为5秒 server.setSoTimeout(5 * 1000); try { Socket accept = server.accept(); BufferedReader reader = new BufferedReader(new InputStreamReader(accept.getInputStream())); System.out.println(reader.readLine()); reader.close(); } catch (SocketTimeoutException e) { System.out.println("连接超时..."); } } System.out.println("调用interrupt(), 退出线程..."); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws InterruptedException { Thread thread = new InterruptedThread(); thread.start(); Thread.sleep(3 * 1000); thread.interrupt(); } }
输出:
连接超时... 调用interrupt(), 退出线程... Process finished with exit code 0
- 当线程中调用
-
调用stop()方法退出线程(不推荐)
程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。
2.4 线程中异常处理
Runnable
接口中的run()
方法不能抛出异常,所有受检异常(checked exception)必须在run()线程体里使用try…catch处理, 这样本身也是符合线程的设计理念的,线程本身就是被看作独立的执行片断,它应该对自己负责,所以由它来消化所有的checked异常是很正常的。对于非受检异常(RuntimeException),创建线程的父线程不受到影响,且不会处理此异常。
public class ExceptionThread extends Thread {
@Override
public void run() {
int a = 1/0;
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new ExceptionThread();
ExceptionHandler handler = new ExceptionHandler();
thread.start();
Thread.sleep(5 * 1000);
System.out.println("主线程结束...");
}
}
输出:
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
at org.numb.concurrency.chapter02.ExceptionThread.run(ExceptionThread.java:9)
主线程结束...
可以看出Thread-0发生除0异常时,主线程仍在运行,直到5s后结束。
对于子线程中的RuntimeException,可以使用UncaughtExceptionHandler
为线程设置“未捕获异常处理器”。
public class ExceptionThread extends Thread {
@Override
public void run() {
int a = 1/0;
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new ExceptionThread();
ExceptionHandler handler = new ExceptionHandler();
thread.setUncaughtExceptionHandler(handler);
thread.start();
}
}
class ExceptionHandler implements UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("处理异常:" + e.toString());
}
}
输出:
处理异常:java.lang.ArithmeticException: / by zero
Process finished with exit code 0
2.5 Thread中的方法
2.4.1 start()与run()方法的区别
-
start()
将线程设置为就绪状态,此时线程没有马上运行,而是等待获取cpu资源,这个过程一般很快。 -
run()
线程的方法体,就绪线程获得cpu资源后,变为运行态,开始执行run()方法,即线程体。
2.4.2 suspend()和resume()被废弃的原因
suspend()
方法不会释放对象的锁,直到调用resume()
方法之后,这可能会导致挂起的线程一直持有锁,而其他线程又无法释放锁,从而导致死锁。
2.4.3 interrupt()与stop()方法的区别
interrupt()
方法不会直接释放锁,而是通过中断状态或者抛出异常等方式中断线程。stop()
直接退出线程,并释放线程持有的锁,可能会导致其他线程数据不一致,产生错误或死锁等
2.4.4 isInterrupted()与Interrupted()方法的区别
-
isInterrupted()
根据线程的中断状态判断是否被中断 -
Interrupted()
判断线程是否被中断,并清除当前中断状态
2.4.5 yield()让出CPU资源,线程由运行转为就绪
yield()应该做的是让当前运行线程回到就绪状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
2.4.6 setDeamon()设置线程为守护线程
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
Thread t = new MyThread();
t.setDaemon(true);
t.start();