Java并发与多线程(2)——Java线程


如果觉得有用可以关注一下公众号:求赞求关注
在这里插入图片描述

二、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不能抛出异常。

  • 线程运行方式不同

    CallableRunnable都可以通过线程池提交,而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(),并不会立刻退出该线程,而是需要捕获异常或通过中断状态判断退出。

    1. 当线程中调用wait()join()sleep()处于阻塞状态时,interrupt()会清除线程中断状态,并抛出InterruptedException
    2. 如果此线程在nio中的InterruptibleChannel上的 I/O 操作中被阻塞,则interrupt()会使该通道被关闭,设置线程的中断状态,并且抛出 java.nio.channels.ClosedByInterruptException
    3. 如果此线程在 java.nio.channels.Selector中被阻塞,则interrupt()会设置该线程的中断状态,并将立即从选择操作返回,可能具有非零值,就像调用了Selector的唤醒方法一样。
    4. 上述情况以外,调用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();
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值