はじめに
こんにちは、matsunariです。今回はJavaのスレッドについてまとめてみます。なんとなく理解はしていましたが、きちんと整理できていなかったので今回まとめてみようと思いました。スレッドをきちんと理解することでバグのないアプリ製作に役立てられればいいなと思っています。
スレッドの基本について
プログラムを実行する処理の最小単位を”スレッド”といいます。通常アプリはメインスレッドと呼ばれる一本のスレッド(シングルスレッド)の上で動きます。しかし、ネットワーク通信などのように処理に時間がかかる動作を行う場合、シングルスレッドでは通信が終わってからようやく次の処理に移ります。これでは、ユーザーは処理が終わるまで長時間待たなければなりません。Javaの世界ではメインスレッドだけではなく、複数のスレッドを並行して実行することができます。これをマルチスレッドと呼びます。マルチスレッドを利用することで、アプリが裏で通信を行なっている間でも、同時並行的に他の動作を行うことができます。
スレッドの利用
スレッドを利用するにはThreadクラスを継承することで実現できます。
主に以下のような2種類の方法を使います。
・Threadクラスを継承する
・Runnnableクラスを実装する
Threadクラス
まずはThreadクラスを使ってスレッドを作ってみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | package com.example.thread; import android.util.Log; public class ThreadBasic extends Thread { //スレッドを識別するためにnameフィールドを設定する private String name; //スレッド名を設定 コンストラクター public ThreadBasic(String name){ this.name = name; } //スレッドの実処理(0-50の範囲でカウントアップ) public void run(){ for(int i= 0; i <= 50; i++){ Log.d("Thread名は", name); System.out.println(name + "の" + i +"番目の出力です。"); } } } |
これでThreadクラスを継承したThreadBasicクラスができました。Threadクラスを使用するときは、必ずrun()メソッドをオーバーライドする必要があります。
このrun()メソッドはJavaでいうところmainメソッドのようなもので、スレッド開始時に呼び出されるエントリーポイントです。
ここにThreadで行いたい処理を書きましょう。
今回は0-50の間でカウントアップを行う処理行なっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package com.example.thread; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //スレッドを生成する //スレッドを呼び出すにはThread派生クラスここではThreadBasicをインスタンス化してstartメソッドを呼び出します。 Thread t1 = new ThreadBasic("t1"); Thread t2 = new ThreadBasic("t2"); Thread t3 = new ThreadBasic("t3"); //スレッドを開始 t1.start(); t2.start(); t3.start(); } } |
それでは実際にスレッドを使って処理を行うとどのようになるのかみてみましょう。
MainActivityではThreadクラスをインスタンス化し、3つのスレッドを作りました。それぞれ、start()メソッドを実行して並行して実行します。
順番が交互になって処理結果が表示されています。
これで各スレッドで処理が並列的に実行されたことを確認できましたね。
Runnableインターフェース
同様にRunnableインタフェースを実装した場合はこのようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | package com.example.runnablebasic; //Runnableインターフェイスはrunメソッドだけを定義したシンプルなインターフェイス。 public class RunnableBasic implements Runnable { private String name; //スレッド名を定義 public RunnableBasic(String name){ this.name = name; } @Override public void run() { for(int i = 0; i<=50; i++ ){ System.out.println(name + ":" + i); } } } |
生成&実行処理はこちらです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package com.example.runnablebasic; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //スレッドを生成 Thread t1 = new Thread (new RunnableBasic("t1")); Thread t2 = new Thread (new RunnableBasic("t2")); Thread t3 = new Thread (new RunnableBasic("t3")); t1.start(); t2.start(); t3.start(); } } |
Threadクラスとの違いはほとんどありません。
同期処理
別々の処理を並行的に実施できるとはいえ、同一のメモリ上で処理されるので、共有のデータに対して同時に処理を行った場合、不整合が発生するケースがあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | package com.example.myapplication; public class BadCount { private int count; public void execute(){ final int TNUM = 500000; Thread[] ts = new Thread[TNUM]; for(int i = 0; i <= TNUM; i++){ ts[i] = new Thread(new MyRunnable(this)); ts[i].start(); } for(int i = 0; i <= TNUM; i++){ try { ts[i].join(); } catch (InterruptedException e) { System.out.println(e); } } //最終的なカウント数を表示 System.out.println("実行数は" + count); } //カウンターをインクリメントする public synchronized void countUp(){ count++; } //スレッドの実処理 private static class MyRunnable implements Runnable { private BadCount _counter; public MyRunnable(BadCount counter) { this._counter = counter; } //BadCountクラスのカウンターをインクリメント @Override public void run(){ _counter.countUp(); } } } |
これは50万個のスレッドを並行してカウントアップするプログラムです。
このプログラムを実行すると正しく動作しない可能性があります。
理由としては以下のようなケースが起こり得るからです。
BadCountクラスのcountUp()メソッドの処理で「変数の現在地を取得→値を加算→演算結果の再代入」という手順を行なっています。
処理の途中で他のスレッドの割り込みが入ると結果が正しく反映されない可能性があります。
synchronized修飾子
上述した不整合を防ぐために、synchronized修飾子を利用します。
1 2 3 | public synchronized void countUp(){ count++; } |
synchronized修飾子が指定されると、そのメソッドが複数のスレッドから同時に呼び出されることがなくなります。ほぼ同時に呼び出されたとしても、先に呼び出した方の処理が優先され、先に呼び出された方で処理が完了するまで、次のスレッドで呼び出されず待ちの状態となります。このように特定の処理を占有することロックを獲得すると呼んだりもします。ロックを獲得して処理を行うことを同期処理と呼びます。
おわりに
今回はJavaのスレッド処理に関してまとめてみました。スレッドはまだ奥が深そうな分野なので、引き続き学習してブログでも発信していきたいと思っています。