本篇文章给大家带来了关于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 线程的生命周期

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

19.png

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

20.png

5 Thread 和 Object 中的线程方法

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

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

21.png

再看看 Object 中的相关方法:

22.png

运行以下代码,查看 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 的一些相关属性,即它的成员变量。

23.png

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

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多线程知识点的详细内容,更多请关注悠悠之家其它相关文章!

点赞(31)

评论列表共有 0 条评论

立即
投稿
返回
顶部