この記事では、java に関する関連知識を提供します。主にマルチスレッドに関連する問題を紹介します。プロセスは複数のスレッドを同時に実行でき、各スレッドは異なるタスクを並行して実行します。スレッドは基本的なものです。処理の単位であり、一連の制御フローですので、一緒に見ていきましょう。
推奨学習: 「java ビデオ チュートリアル 」
Java には、マルチスレッド プログラミングのサポートが組み込まれており、次のことが可能です。マルチスレッドアプリケーションを簡単に開発できます。
Java で最もよく知られているスレッドは、メイン スレッド、つまりメイン スレッドです。
プロセスは複数のスレッドを同時に実行でき、各スレッドは異なるタスクを並行して実行します。スレッドはプロセスの基本単位であり、単一の順次制御フローです。プロセスは、すべての「非デーモン スレッド」の実行が終了するまで実行されます。 Java の一般的なデーモン スレッドには次のものがあります。ガベージ コレクション スレッド、
ここでは、同時実行と並列処理の次の違いについて簡単に説明します。
同時実行性: 同時に複数のタスクが実行されています
並列性: 複数のタスクが同時に同時に実行されます
複数のスレッドタスクを効率的に実行し、CPU リソースを合理的に利用し、マルチコア CPU のパフォーマンスを最大限に活用します。ただし、マルチスレッド化によってプログラムが常に効率的に実行できるわけではなく、マルチスレッドの切り替え、スレッドのデッドロック、スレッド例外などの問題によるオーバーヘッドにより、マルチスレッド開発はシングルスレッド開発よりも面倒になります。そのため、開発効率を高めるためにはJavaのマルチスレッド関連の知識を習得する必要があります。
公式ドキュメント Thread (Java Platform SE 8) (oracle.com) の java.lang.Thread の説明によると、スレッドを作成する 2 つの主な方法。種類:
新しい実行スレッドを作成するには 2 つの方法があります。1 つは、クラスを Thread のサブクラスとして宣言することです。このサブクラスは run メソッドをオーバーライドする必要がありますThread クラスのインスタンスです。その後、サブクラスのインスタンスを割り当てて開始できます。
スレッドを作成するもう 1 つの方法は、Runnable インターフェイスを実装するクラスを宣言することです。次に、そのクラスが run メソッドを実装します。インスタンスその後、クラスのクラスを割り当て、スレッドの作成時に引数として渡し、開始することができます。
ご覧のとおり、スレッドを作成するには 2 つの方法があります:
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 どちらが良いですか?
スレッドを作成する方法は2通りありますが、実際の開発ではRunnableインターフェースを実装したメソッドを使用する方が良いでしょう。次の理由があります:
スレッドの run メソッドを表示すると、次のことがわかります:
// Thread 实例的成员变量 target 是一个 Runnable 实例,可以通过 Thread 的构造方法传入 private Runnable target; // 如果有传入 Runnable 实例,那么就执行它的 run 方法 // 如果重写,就完全执行我们自己的逻辑 public void run() { if (target != null) { target.run(); } }
上記のソース コードを見ると、Thread クラスが実行を定義する本体ではないことがわかります。 task ですが、Runnable が実行タスクの内容を定義し、Thread が実行を呼び出すことで、スレッドとタスクの分離を実現します。
スレッドとタスクの分離により、タスクの実行が必要なときにスレッドを作成し、実行後にスレッドを破棄する代わりに、スレッドを再利用できます。これにより、システムのオーバーヘッドが大きくなります。これはスレッド プールの基本的な考え方でもあります。
また、Javaは単一継承しかサポートしていないため、Threadを継承してマルチスレッド化すると、後から継承による機能拡張が必要となり、非常に面倒です。
2 開始メソッドと実行メソッド
上記からわかるように、スレッドを作成するには 2 つの方法があります。ここでは 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() メソッドを繰り返し呼び出すことはできません。そうしないと例外がスローされます。
スレッドが空ではない 無限に実行される 通常、スレッドが停止する条件は、
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 线程的生命周期
线程的生命周期状态由六部分组成:
可以用一张图总结线程的生命周期,以及各个过程之间是如何转换的:
现在,我们已经知道了线程的创建、启动、停止以及线程的生命周期了,那么,再来看看线程相关的方法有哪些。
首先,看看 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 使用
现在我们已经对 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.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(); } }
合理地利用多线程能够带来性能上的提升,但是如果因为一些疏漏,多线程反而会成为程序员的噩梦。
例如,多线程开发,我们需要考虑线程安全问题、性能问题。
首先,讲讲线程安全问题。
什么是线程安全?所谓线程安全,即
在多线程情况下,如果访问某个对象,不需要额外处理,例如加锁、令线程阻塞、额外的线程调度等,调用这个对象都能获得正确的结果,那么这个对象就是线程安全的
因此,在编写多线程程序时,就需要考虑某个数据是否是线程安全的,如果这个对象满足:
被多个线程共享
操作具有时序要求,先读后写
这个对象的类有他人编写,并且没有声明是线程安全的
那么我们就需要考虑使用同步锁、Lock、并发工具类(java.util.concurrent)来保证这个对象是在多线程下是安全的。
再看看多线程带来的性能问题。
多个线程的调度需要上下文切换,这需要耗费 CPU 资源。
所谓上下文,即处理器中寄存器、程序计数器内的信息。
上下文切换,即 CPU 挂起一个线程,将其上下文保存到内存中,从内存中获取另一个运行线程的上下文,恢复到寄存器中,根据程序计数器中的指令恢复线程运行。
一个线程被挂起,另一个线程恢复运行,这个时候,被挂起的线程的数据缓存对于运行线程来说是无效的,减缓了线程的运行速度,新的线程需要重新缓存数据提升运行速度。
通常情况下,密集的 IO 操作、抢锁操作都会带来密集的上下文切换。
以上,是上下文切换带来的性能问题,Java 的内存模型也会带来性能问题,为了保证数据的可见性,JVM 会强制令数据缓存失效,保证数据是实时最新的,这也牺牲了缓存带来的性能提升。
这里总结下上面的内容。
创建线程有两种方式,继承 Thread 和实现 Runnable
start 方法才能正确创建和启动线程,run 方法只是一个普通方法
start メソッドを繰り返し呼び出すことはできません。繰り返し呼び出すと例外がスローされます
スレッドを停止する正しい方法は、interrupt() を通じてスレッドに通知することです。 )
スレッドは割り込みステータスを時々チェックし、スレッドを停止するかどうかを決定します。メソッド isInterrupt()を使用します。
スレッドがブロックされている場合は、キャプチャ割り込み例外を確認し、スレッドを停止するかどうかを決定します
スレッド呼び出しサブメソッドで例外をスローし、run メソッドがそれらを統一された方法でキャプチャして処理できるようにするのが最善です。ライフサイクルは
です。 #NEW#時間待機中
終了
wait()/notify()/notifyAll() は同期ロックとともに使用する必要があります
wait() はロックを解放しますが、sleep() は解放しません同期ロックやロック ロックなどのロックを解放します
Java ビデオ チュートリアル
>>以上がJava マルチスレッドの知識ポイントの簡単な要約の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。