>首页> IT >

简单总结Java多线程知识点

时间:2022-10-12 15:55:24       来源:转载
本篇文章给大家带来了关于java的相关知识,其中主要介绍了关于多线程的相关问题,一个进程可以并发多个线程,每条线程并行执行不同的任务,线程是进程的基本单位,是一个单一顺序的控制流,下面一起来看一下,希望对大家有帮助。

推荐学习:《java视频教程》


(资料图片)

Java 提供了多线程编程的内置支持,让我们可以轻松开发多线程应用。

Java 中我们最为熟悉的线程就是 main 线程——主线程。

一个进程可以并发多个线程,每条线程并行执行不同的任务。线程是进程的基本单位,是一个单一顺序的控制流,一个进程一直运行,直到所有的“非守护线程”都结束运行后才能结束。Java 中常见的守护线程有:垃圾回收线程、

多线程可以帮助我们高效地执行任务,合理利用 CPU 资源,充分地发挥多核 CPU 的性能。但是多线程也并不总是能够让程序高效运行的,多线程切换带来的开销、线程死锁、线程异常等等问题,都会使得多线程开发较单线程开发更麻烦。因此,有必要学习 Java 多线程的相关知识,从而提高开发效率。

1 创建多线程

根据官方文档 Thread (Java Platform SE 8 ) (oracle.com) 中 java.lang.Thread 的说明,可以看到线程的创建方式主要有两种:

可以看到,有两种创建线程的方式:

声明一个类继承 Thread 类,这个子类需要重写 run 方法,随后创建这个子类的实例,这个实例就可以创建并启动一个线程执行任务;

声明一个类实现接口 Runnable 并实现 run 方法。这个类的实例作为参数分配给一个 Thread 实例,随后使用 Thread 实例创建并启动线程即可

除此之外的创建线程的方法,诸如使用 Callable 和 FutureTask、线程池等等,无非是在此基础上的扩展,查看源码可以看到 FutureTask 也实现了 Runnable 接口。

使用继承 Thread 类的方法创建线程的代码:

/** * 使用继承 Thread 类的方法创建线程 */public class CreateOne {    public static void main(String[] args) {        Thread t = new MySubThread();        t.start();    }}class MySubThread extends Thread {    @Override    public void run() {        // currentThread() 是 Thread 的静态方法,可以获取正在执行当前代码的线程实例        System.out.println(Thread.currentThread().getName() + "执行任务");    }}// ================================== 运行结果Thread-0执行任务
登录后复制

使用实现 Runnable 接口的方法创建线程的代码:

/** * 使用实现 Runnable 接口的方法创建线程 */public class CreateTwo {    public static void main(String[] args) {        RunnableImpl r = new RunnableImpl();        Thread t = new Thread(r);        t.start();    }}class RunnableImpl implements Runnable {    @Override    public void run() {        System.out.println(Thread.currentThread().getName() + "执行任务");    }}// ================================== 运行结果Thread-0执行任务
登录后复制

1.1 孰优孰劣

创建线程虽然有两种方法,但是在实际开发中,使用实现接口 Runnable 的方法更好,原因如下:

查看 Thread 的 run 方法,可以看到:

// Thread 实例的成员变量 target 是一个 Runnable 实例,可以通过 Thread 的构造方法传入private Runnable target;// 如果有传入 Runnable 实例,那么就执行它的 run 方法// 如果重写,就完全执行我们自己的逻辑public void run() {    if (target != null) {        target.run();    }}
登录后复制

查看上面的源码,我们可以知道,Thread 类并不是定义执行任务的主体,而是 Runnable 定义执行任务内容,Thread 调用执行,从而实现线程与任务的解耦。

由于线程与任务解耦,我们可以复用线程,而不是当需要执行任务就去创建线程、执行完毕就销毁线程,这样带来的系统开销太大。这也是线程池的基本思想。

此外,Java 只只支持单继承,如果继承 Thread 使用多线程,那么后续需要通过继承的方式扩展功能,那会相当麻烦。

2 start 和 run 方法

从上面可以得知,有两种创建线程的方式,我们通过 Thread 类或 Runnable 接口的 run 方法定义任务,通过 Thread 的 start 方法创建并启动线程。

我们不能通过 run 方法启动并创建一个线程,它只是一个普通方法,如果直接调用这个方法,其实只是调用这个方法的线程在执行任务罢了。

// 将上面的代码修改一下,查看执行结果public class CreateOne {    public static void main(String[] args) {        Thread t = new MySubThread();        t.run();        //t.start();    }}// ===================== 执行结果main执行任务
登录后复制

查看 start 方法的源码:

// 线程状态,为 0 表示还未启动private volatile int threadStatus = 0;// 同步方法,确保创建、启动线程是线程安全的public synchronized void start() {    // 如果线程状态不为 0,那么抛出异常——即线程已经创建了    if (threadStatus != 0)        throw new IllegalThreadStateException();// 将当前线程添加到线程组    group.add(this);    boolean started = false;    try {        // 这是一个本地方法        start0();        started = true;    } finally {        try {            if (!started) {                group.threadStartFailed(this);            }        } catch (Throwable ignore) {        }    }}// 由本地方法实现,只需要知道,该方法调用后会创建一个线程,并且会执行 run 方法private native void start0();
登录后复制

由上面的源码可以得知:

创建并启动一个线程是线程安全的

start() 方法不能反复调用,否则会抛出异常

3 怎么停止线程

线程并不是无休止地执行下去的,通常情况下,线程停止的条件有:

run 方法执行结束

线程发生异常,但是没有捕获处理

除此之外,我们还需要自定义某些情况下需要通知线程停止,例如:

用户主动取消任务

任务执行时间超时、出错

出现故障,服务需要快速停止

...

为什么不能直接简单粗暴的停止线程呢?通过通知线程停止任务,我们可以更优雅地停止线程,让线程保存问题现场、记录日志、发送警报、友好提示等等,令线程在合适的代码位置停止线程,从而避免一些数据丢失等情况。

令线程停止的方法是让线程捕获中断异常或检测中断标志位,从而优雅地停止线程,这是推荐的做法。而不推荐的做法有,使用被标记为过时的方法:stop,resume,suspend,这些方法可能会造成死锁、线程不安全等情况,由于已经过时了,所以不做过多介绍。

3.1 通知线程中断

我们要使用通知的方式停止目标线程,通过以下方法,希望能够帮助你掌握中断线程的方法:

/** * 中断线程 */public class InterruptThread {    public static void main(String[] args) throws InterruptedException {        Thread t = new Thread(() -> {            long i = 0;            // isInterrupted() 检测当前线程是否处于中断状态            while (i < Long.MAX_VALUE && !Thread.currentThread().isInterrupted()) {                i++;            }            System.out.println(i);        });                t.start();        // 主线程睡眠 1 秒,通知线程中断        Thread.sleep(1000);        t.interrupt();    }}// 运行结果1436125519
登录后复制

这是中断线程的方法之一,还有其他方法,当线程处于阻塞状态时,线程并不能运行到检测线程状态的代码位置,然后正确响应中断,这个时候,我们需要通过捕获异常的方式停止线程:

/** * 通过捕获中断异常停止线程 */public class InterruptThreadByException {    public static void main(String[] args) throws InterruptedException {        Thread t = new Thread(()->{            long i = 0;            while (i < Long.MAX_VALUE) {                i++;                try {                    // 线程大部分时间处于阻塞状态,sleep 方法会抛出中断异常 InterruptedException                    Thread.sleep(100);                } catch (InterruptedException e) {                    // 捕获到中断异常,代表线程被通知中断,做出相应处理再停止线程                    System.out.println("线程收到中断通知   " + i);                    // 如果 try-catch 在 while 代码块之外,可以不用 return 也可以结束代码                    // 在 while 代码块之内,如果没有 return / break,那么还是会进入下一次循环,并不能正确停止                    return;                }            }        });        t.start();        Thread.sleep(1000);        t.interrupt();    }}// 运行结果线程收到中断通知   10
登录后复制

以上,就是停止线程的正确做法,此外,捕获中断异常后,会清除线程的中断状态,在实际开发中需要特别注意。例如,修改上面的代码:

public class InterruptThreadByException {    public static void main(String[] args) throws InterruptedException {        Thread t = new Thread(()->{            long i = 0;            while (i < Long.MAX_VALUE) {                i++;                try {                    Thread.sleep(100);                } catch (InterruptedException e) {                    System.out.println("线程收到中断通知   " + i);                    //  添加这行代码,捕获到中断异常后,检测中断状态,中断状态为 false                    System.out.println(Thread.currentThread().isInterrupted());                    return;                }            }        });        t.start();        Thread.sleep(1000);        t.interrupt();    }}
登录后复制

所以,在线程中,如果调用了其他方法,如果该方法有异常发生,那么:

将异常抛出,而不是在子方法内部捕获处理,由 run 方法统一处理异常

捕获异常,并重新通知当前线程中断,Thread.currentThread().interrupt()

例如:

public class SubMethodException {    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(new ExceptionRunnableA());        Thread t2 = new Thread(new ExceptionRunnableB());        t1.start();        t2.start();        Thread.sleep(1000);        t1.interrupt();        t2.interrupt();    }}class ExceptionRunnableA implements Runnable {    @Override    public void run() {        try {            while (true) {                method();            }        } catch (InterruptedException e) {            System.out.println("run 方法内部捕获中断异常");        }    }    public void method() throws InterruptedException {        Thread.sleep(100000L);    }}class ExceptionRunnableB implements Runnable {    @Override    public void run() {        while (!Thread.currentThread().isInterrupted()) {            method();        }    }    public void method()  {        try {            Thread.sleep(100000L);        } catch (InterruptedException e) {            System.out.println("子方法内部捕获中断异常");            // 如果不重新设置中断,线程将不能正确响应中断            Thread.currentThread().interrupt();        }    }}
登录后复制

综上,总结出令线程正确停止的方法为:

使用 interrupt() 方法通知目标线程停止,标记目标线程的中断状态为 true

目标线程通过 isInterrupted() 不时地检测线程的中断状态,根据情况决定是否停止线程

如果线程使用了阻塞方法例如 sleep(),那么需要捕获中断异常并处理中断通知,捕获了中断异常会重置中断标记位

如果 run() 方法调用了其他子方法,那么子方法:

将异常抛出,传递到顶层 run 方法,由 run 方法统一处理

将异常捕获,同时重新通知当前线程中断

下面再说说关于中断的几个相关方法和一些会抛出中断异常的方法,使用的时候需要特别注意。

3.2 线程中断的相关方法

interrupt() 实例方法,通知目标线程中断。

static interrupted() 静态方法,获取当前线程是否处于中断状态,会重置中断状态,即如果中断状态为 true,那么调用后中断状态为 false。方法内部通过 Thread.currentThread() 获取执行线程实例。

isInterrupted() 实例方法,获取线程的中断状态,不会清除中断状态。

3.3 阻塞并能响应中断的方法

Object.wait()

Thread.sleep()

Thread.join()

BlockingQueue.take() / put()

Lock.lockInterruptibly()

CountDownLatch.await()

CyclicBarrier.await()

Exchanger.exchange()

4 线程的生命周期

线程的生命周期状态由六部分组成:

可以用一张图总结线程的生命周期,以及各个过程之间是如何转换的:

5 Thread 和 Object 中的线程方法

现在,我们已经知道了线程的创建、启动、停止以及线程的生命周期了,那么,再来看看线程相关的方法有哪些。

首先,看看 Thread 中的一些方法:

再看看 Object 中的相关方法:

运行以下代码,查看 wait() 和 sleep() 是否会释放同步锁

/*** 证明 sleep 不会释放锁,wait 会释放锁*/public class SleepAndWait {    private static Object lock = new Object();    public static void main(String[] args) {        Thread t1 = new Thread(()->{            synchronized (lock) {                System.out.println(Thread.currentThread().getName() + "获得同步锁,调用 wait() 方法");                try {                    lock.wait(2000);                    System.out.println(Thread.currentThread().getName() + "重新获得同步锁");                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });        Thread t2 = new Thread(()->{            synchronized (lock) {                System.out.println(Thread.currentThread().getName() + "获得同步锁,唤醒另一个线程,调用 sleep()");                lock.notify();                try {                    // 如果 sleep() 会释放锁,那么在此期间,上面的线程将会继续运行,即 sleep 不会释放同步锁                    Thread.sleep(2000);                    // 如果执行 wait 方法,那么上面的线程将会继续执行,证明 wait 方法会释放锁                    //lock.wait(2000);                    System.out.println(Thread.currentThread().getName() + "sleep 结束");                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });        t1.start();        t2.start();    }}
登录后复制

上面的代码已经证明了 sleep() 不会释放同步锁,此外,sleep() 也不会释放 Lock 的锁,运行以下代码查看结果:

/** * sleep 不会释放 Lock 锁 */public class SleepDontReleaseLock implements Runnable {    private static Lock lock = new ReentrantLock();    @Override    public void run() {        // 调用 lock 方法,线程会尝试持有该锁对象,如果已经被其他线程锁住,那么当前线程会进入阻塞状态        lock.lock();        try {            System.out.println(Thread.currentThread().getName() + "获得 lock 锁");            // 如果 sleep 会释放 Lock 锁,那么另一个线程会马上打印上面的语句            Thread.sleep(1000);            System.out.println(Thread.currentThread().getName() + "释放 lock 锁");        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            // 当前线程释放锁,让其他线程可以占有锁            lock.unlock();        }    }    public static void main(String[] args) {        SleepDontReleaseLock task = new SleepDontReleaseLock();        new Thread(task).start();        new Thread(task).start();    }}
登录后复制

5.1 wait 和 sleep 的异同

接下来总结 Object.wait() 和 Thread.sleep() 方法的异同点。

相同点:

都会使线程进入阻塞状态

都可以响应中断

不同点:

wait() 是 Object 的实例方法,sleep() 是 Thread 的静态方法

sleep() 需要指定时间

wait() 会释放锁,sleep() 不会释放锁,包括同步锁和 Lock 锁

wait() 必须配合 synchronized 使用

6 线程的相关属性

现在我们已经对 Java 中的多线程有一定的了解了,我们再看看 Java 中线程 Thread 的一些相关属性,即它的成员变量。

运行以下代码,了解线程的相关属性

public class ThreadFields {    public static void main(String[] args) throws InterruptedException {        Thread t = new Thread(() -> {            // 自定义线程的 ID 并不是从 2 开始            System.out.println("线程 " + Thread.currentThread().getName()                               + " 的线程 ID " + Thread.currentThread().getId());            while (true) {                // 守护线程一直运行,但是 用户线程即这里的主线程结束后,也会随着虚拟机一起停止            }        });        // 自定义线程名字        t.setName("自定义线程");        // 将其设置为守护线程        t.setDaemon(true);        // 设置优先级 Thread.MIN_PRIORITY = 1     Thread.MAX_PRIORITY = 10        t.setPriority(Thread.MIN_PRIORITY);        t.start();        // 主线程的 ID 为 1        System.out.println("线程 " + Thread.currentThread().getName() + " 的线程 ID " + Thread.currentThread().getId());        Thread.sleep(3000);    }}
登录后复制

7 全局异常处理

在子线程中,如果发生了异常我们能够及时捕获并处理,那么对程序运行并不会有什么恶劣影响。

但是,如果发生了一些未捕获的异常,在多线程情况下,这些异常打印出来的堆栈信息,很容易淹没在庞大的日志中,我们可能很难察觉到,并且不好排查问题。

如果对这些异常都做捕获处理,那么就会造成代码的冗余,编写起来也不方便。

因此,我们可以编写一个全局异常处理器来处理子线程中抛出的异常,统一地处理,解耦代码。

7.1 源码查看

在讲解如何处理子线程的异常问题前,我们先看看 JVM 默认情况下,是如何处理未捕获的异常的。

查看 Thread 的源码:

public class Thread implements Runnable {    【1】当发生未捕获的异常时,JVM 会调用该方法,并传递异常信息给异常处理器        可以在这里打下断点,在线程中抛出异常不捕获,IDEA 会跳转到这里    // 向处理程序发送未捕获的异常。此方法仅由JVM调用。private void dispatchUncaughtException(Throwable e) {        【2】查看第 9 行代码,可以看到如果没有指定异常处理器,默认是线程组作为异常处理器        【3】调用这个异常处理器的处理方法,处理异常,查看第 15 行        getUncaughtExceptionHandler().uncaughtException(this, e);    }        public UncaughtExceptionHandler getUncaughtExceptionHandler() {        return uncaughtExceptionHandler != null ?            uncaughtExceptionHandler : group;    }        【4】UncaughtExceptionHandler 是 Thread 的内部接口,线程组也是该接口的实现,        只有一个方法处理异常,接下来查看第 25 行,看看 Group 是如何实现的    @FunctionalInterface    public interface UncaughtExceptionHandler {        void uncaughtException(Thread t, Throwable e);    }}public class ThreadGroup implements Thread.UncaughtExceptionHandler {    【5】默认异常处理器的实现    public void uncaughtException(Thread t, Throwable e) {        // 如果有父线程组,交给它处理        if (parent != null) {            parent.uncaughtException(t, e);        } else {            // 获取默认的异常处理器,如果没有指定,那么为 null            Thread.UncaughtExceptionHandler ueh =                Thread.getDefaultUncaughtExceptionHandler();            if (ueh != null) {                ueh.uncaughtException(t, e);            }             // 没有指定异常处理器,打印堆栈信息            else if (!(e instanceof ThreadDeath)) {                System.err.print("Exception in thread \""                                 + t.getName() + "\" ");                e.printStackTrace(System.err);            }        }    }}
登录后复制

7.2 自定义全局异常处理器

通过上面的源码讲解,已经可以知道 JVM 是如何处理未捕获的异常的了,即只打印堆栈信息。那么,要如何自定义异常处理器呢?

具体方法为:

实现接口 Thread.UncaughtExceptionHandler 并实现方法 uncaughtException()

为创建的线程指定异常处理器

示例代码:

public class MyExceptionHandler implements Thread.UncaughtExceptionHandler{    @Override    public void uncaughtException(Thread t, Throwable e) {        System.out.println("发生了未捕获的异常,进行日志处理、报警处理、友好提示、数据备份等等......");        e.printStackTrace();    }    public static void main(String[] args) {        Thread t = new Thread(() -> {            throw new RuntimeException();        });        t.setUncaughtExceptionHandler(new MyExceptionHandler());        t.start();    }}
登录后复制

8 多线程带来的问题

合理地利用多线程能够带来性能上的提升,但是如果因为一些疏漏,多线程反而会成为程序员的噩梦。

例如,多线程开发,我们需要考虑线程安全问题、性能问题。

首先,讲讲线程安全问题。

什么是线程安全?所谓线程安全,即

因此,在编写多线程程序时,就需要考虑某个数据是否是线程安全的,如果这个对象满足:

被多个线程共享

操作具有时序要求,先读后写

这个对象的类有他人编写,并且没有声明是线程安全的

那么我们就需要考虑使用同步锁、Lock、并发工具类(java.util.concurrent)来保证这个对象是在多线程下是安全的。

再看看多线程带来的性能问题。

多个线程的调度需要上下文切换,这需要耗费 CPU 资源。

所谓上下文,即处理器中寄存器、程序计数器内的信息。

上下文切换,即 CPU 挂起一个线程,将其上下文保存到内存中,从内存中获取另一个运行线程的上下文,恢复到寄存器中,根据程序计数器中的指令恢复线程运行。

一个线程被挂起,另一个线程恢复运行,这个时候,被挂起的线程的数据缓存对于运行线程来说是无效的,减缓了线程的运行速度,新的线程需要重新缓存数据提升运行速度。

通常情况下,密集的 IO 操作、抢锁操作都会带来密集的上下文切换。

以上,是上下文切换带来的性能问题,Java 的内存模型也会带来性能问题,为了保证数据的可见性,JVM 会强制令数据缓存失效,保证数据是实时最新的,这也牺牲了缓存带来的性能提升。

9 总结

这里总结下上面的内容。

创建线程有两种方式,继承 Thread 和实现 Runnable

start 方法才能正确创建和启动线程,run 方法只是一个普通方法

start 方法不能反复调用,反复调用会抛出异常

正确停止线程的方法是通过 interrupt() 通知线程

线程不时地检查中断状态并判断是否停止线程,使用方法 isInterrupt()

如果线程阻塞,捕获中断异常,判断是否停止线程

线程调用的子方法最好将异常抛出,由 run 方法统一捕获处理

线程调用的子方法如果捕获异常,需要重新通知线程中断

线程的生命周期为

NEW

RUNNABLE

BLOCKED

WAITING

TIMED WAITING

TERMINATED

wait()/notify()/notifyAll() 必须配合同步锁使用

wait() 会释放锁,sleep() 不会释放锁,包括同步锁和 Lock 锁

线程的一些属性

线程ID,无法修改

线程名 name,可以自定义

守护线程 daemon,线程类型会继承自父线程,通常不指定线程为守护线程

优先级 priority,通常使用默认优先级,不改变优先级

可以自定义全局异常处理器,处理非主线程中的未捕获的异常,如备份数据、日志处理、报警等等

多线程开发会带来线程安全问题、性能问题,开发过程需要特别注意

推荐学习:《java视频教程》

以上就是简单总结Java多线程知识点的详细内容,更多请关注php中文网其它相关文章!

关键词: 中断状态 中断异常