推荐学习:《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中文网其它相关文章!































































