同時実行性とは、複数のプログラムを並行して実行する、またはプログラムの複数の部分を並行して実行する機能です。プログラム内の時間のかかるタスクを非同期または並列で実行できれば、プログラム全体のスループットと対話性が大幅に向上します。最近の PC には複数の CPU または 1 つの CPU 内に複数のコアが搭載されており、複数のコアを適切に使用できるかどうかが、大規模なアプリケーションの鍵となります。
スレッドの基本的な使い方
スレッドの実行中に実行されるコードを記述する方法は 2 つあります。1 つは Thread サブクラスのインスタンスを作成して run メソッドをオーバーライドする方法、もう 1 つはスレッドの実行時に Runnable インターフェイスを実装する方法です。クラスを作成しています。もちろん、Callableを実装するという手もありますが、CallableとFutureの組み合わせではタスク実行後に戻り値を取得できますが、RunnableメソッドやThreadメソッドはタスク実行後に結果を取得できません。
public class ThreadMain { public static void main(String[] args) { MyThread myThread = new MyThread(); new Thread(myThread).start(); new MyThreas2().start(); } } // 第一种方式,实现Runable接口 class MyThread implements Runnable { @Override public void run() { System.out.println("MyThread run..."); } } // 第二种方式,继承Thread类,重写run()方法 class MyThreas2 extends Thread { @Override public void run() { System.out.println("MyThread2 run..."); } }
スレッドが開始されると、start()メソッドは、runメソッドが別のCPUで実行されたかのように、run()メソッドの実行完了を待たずにすぐに戻ります。
注: スレッドを作成して実行するときによくある間違いは、以下に示すように、start() メソッドの代わりにスレッドの run() メソッドを呼び出すことです。 .run (); // start(); である必要があります
run() メソッドは実際に期待どおりに呼び出されるため、最初は何も違和感はありません。ただし、実際には、 run() メソッドは作成されたばかりの新しいスレッドによって実行されるのではなく、現在のスレッドによって実行されます。つまり、上記の 2 行のコードを実行するスレッドによって実行されます。作成した新しいスレッドで run() メソッドを実行したい場合は、新しいスレッドの start メソッドを呼び出す必要があります。
public static void main(String[] args) { ExecutorService exec = Executors.newSingleThreadExecutor(); Future<String> future = exec.submit(new CallTask()); System.out.println(future.get()); } class CallTask implements Callable { public String call() { return "hello"; } }スレッド名をスレッドに設定します:
MyTask myTask = new MyTask(); Thread thread = new Thread(myTask, "myTask thread"); thread.start(); System.out.println(thread.getName());スレッドを作成するときに、スレッドに名前を付けることができます。これは、異なるスレッドを区別するのに役立ちます。 volatile マルチスレッド同時プログラミングでは、synchronized と volatile の両方が重要な役割を果たし、Volatile はマルチプロセッサ開発における共有変数の「可視性」を保証します。可視性とは、あるスレッドが共有変数を変更すると、別のスレッドが変更された値を読み取ることができることを意味します。場合によっては、synchronized よりもコストが低くなりますが、volatile では変数のアトミック性を保証できません。 揮発性変数が書き込まれるとき(アセンブリの下にロック命令がある)、マルチコア システムではロック命令には 2 つの機能があります: 現在の CPU キャッシュ ラインをシステム メモリに書き戻す。 このライトバック操作により、他の CPU によってキャッシュされたデータがアドレスを変更して無効になります。 マルチCPUはキャッシュ一貫性の原則に従っており、各CPUはバス上で送信されるデータを傍受することによってキャッシュ値が期限切れになっているかどうかをチェックし、キャッシュに対応するメモリアドレスが変更されていることが判明すると、対応するキャッシュラインが更新されます。無効ステータスに設定されると、次のデータ操作がシステム メモリから再読み取りされます。 Volatile についてさらに詳しく知りたい場合は、ここをクリックして Volatile の実装原理の詳細な分析をご覧ください。 synchronized Synchronized は常にマルチスレッド同時プログラミングのベテランです。しかし、Java SE1.6 では Synchronized がさまざまに最適化されており、場合によってはそれほど重くなくなりました。 。 Java のすべてのオブジェクトはロックとして使用できます。スレッドが同期されたコード ブロックにアクセスしようとするときは、まずロックを取得し、終了するか例外をスローするときにロックを解放する必要があります。 同期メソッドの場合、ロックは現在のインスタンス オブジェクトです。 静的同期メソッドの場合、ロックは現在のオブジェクトの Class オブジェクトです。 同期メソッドブロックの場合、ロックは同期ブラケットで設定されたオブジェクトです。 synchronizedキーワードは継承できないということは、基本クラスのsynchronizedメソッドがサブクラスではデフォルトで同期されないことを意味します。スレッドが同期されたコード ブロックにアクセスしようとすると、まずロックを取得し、終了するか例外をスローするときにロックを解放する必要があります。 Java のすべてのオブジェクトはロックとして使用できます。では、ロックはどこにあるのでしょうか?ロックは Java オブジェクト ヘッダーに格納されます。オブジェクトが配列型の場合、仮想マシンはオブジェクト ヘッダーの格納に 3 ワード (ワード幅) を使用します。オブジェクトが非配列型の場合、仮想マシンは 2 ワードを使用します。 (ワード幅) オブジェクトヘッダーを格納します。同期に関するさらに詳しい情報については、「Java SE1.6 での同期」をクリックしてください。 スレッドプール スレッドプールはワーカースレッドの管理を担当し、実行を待っているタスクのキューが含まれています。スレッドプールのタスクキューはRunnableの集合であり、ワーカースレッドはタスクキューからRunnableオブジェクトを取り出して実行する役割を担います。
ExecutorService executor = Executors.newCachedThreadPool();for (int i = 0; i < 5; i++) { executor.execute(new MyThread2()); } executor.shutdown();Java は Executor を通じて 4 種類のスレッド プールを提供します: newCachedThreadPool: 新しいタスクの場合、アイドル状態のスレッドがない場合、アイドル状態のスレッドが一定の時間を超えると、新しいスレッドが作成されます。リサイクルされることになります。
newFixedThreadPool:创建一个固定数量线程的线程池。
newSingleThreadExecutor:创建一个单线程的线程池,该线程池只用一个线程来执行任务,保证所有任务都按照FIFO顺序执行。
newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。
以上几种线程池底层都是调用ThreadPoolExecutor来创建线程池的。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。,可以选择的阻塞队列有以下几种:
workQueue(任务队列):用于保存等待执行的任务的阻塞队列。
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
当提交新任务到线程池时,其处理流程如下:
先判断基本线程池是否已满?没满则创建一个工作线程来执行任务,满了则进入下个流程。
其次判断工作队列是否已满?没满则提交新任务到工作队列中,满了则进入下个流程。
最后判断整个线程池是否已满?没满则创建一个新的工作线程来执行任务,满了则交给饱和策略来处理这个任务。