如果觉得有用可以关注一下公众号:求赞求关注
二、Java线程
2.1 实现线程的三种方式
java实现线程有三种方式:继承Thread类、实现Runnable接口和实现Callable接口
2.1.1 继承Thread类
Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。 启动线程的唯一方 法就是通过 Thread 类的 start()实例方法。 start()方法是一个 native 方法,它将启动一个新线 程,并执行 run()方法。
public class MyThread extends Thread{
@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();
}
}
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) {
MyRunnable runnable = new MyRunnable();
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) {
MyCallable callable = new MyCallable();
ExecutorService threadPool = Executors.newFixedThreadPool(10);
Future<Integer> result = threadPool.submit(callable);
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() {
System.out.println("运行");
try (ServerSocket server = new ServerSocket(8080)) {
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();
}
System.out.println("结束");
}
public static void main(String[] args) {
System.out.println("新建");
Thread thread = new Thread(new SocketThread());
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)) {
while (!isInterrupted()) {
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()方法的区别
2.4.2 suspend()和resume()被废弃的原因
suspend() 方法不会释放对象的锁,直到调用resume() 方法之后,这可能会导致挂起的线程一直持有锁,而其他线程又无法释放锁,从而导致死锁。
2.4.3 interrupt()与stop()方法的区别
interrupt() 方法不会直接释放锁,而是通过中断状态或者抛出异常等方式中断线程。stop() 直接退出线程,并释放线程持有的锁,可能会导致其他线程数据不一致,产生错误或死锁等
2.4.4 isInterrupted()与Interrupted()方法的区别
2.4.5 yield()让出CPU资源,线程由运行转为就绪
yield()应该做的是让当前运行线程回到就绪状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
2.4.6 setDeamon()设置线程为守护线程
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
Thread t = new MyThread();
t.setDaemon(true);
t.start();
|