概要
今回はスレッドセーフについて説明します。
スレッドセーフなコードは、
予期しない動作やデータの不整合を発生させないように設計されています。
本記事ではJavaを例に、スレッドセーフについて知っていただき
サンプルコードを紹介したいと思います!
前提知識
前提知識として、スレッド・マルチスレッドについて軽く触れます。
スレッド・マルチスレッド
- スレッド:プログラム内で独立して実行される軽量なプロセスの一部のこと
- マルチスレッド:スレッドを複数同時に実行すること。これにより処理の効率化、高速化の実現が可能になります!
ざっと以下のようなイメージです。
スレッドセーフとは
いよいよスレッドセーフについて説明していきます。
・・・これだけですとピンとこないと思いますので
もう少し深掘りしてみましょう。
まずは下の図をご覧ください!
こんな感じでマルチスレッドの場合は、裏で色々な処理が並行して
実行され、画面に情報が返ります。
しかし以下の場合はどうでしょうか?
(※赤い矢印に注目!)
2つのスレッド処理が同時に画面に返っています。
このように複数のスレッドから同じリソースへのアクセスが
同時に実施されると、競合状態となってしまいます。
複数の処理が競合すると、想定外な結果を招き思わぬバグを
生み出す原因となってしまいます。
このような事態を防ぐ仕組みをスレッドセーフと呼びます。
スレッドセーフでない状態は、スレッドが競合し
データの破損や想定外の不整合が発生する原因になる!
スレッドセーフなコードにするには?
少々シンプルですが、まずはスレッドセーフに対応していない
実装例になります。
スレッドセーフ未対応
public class Counter {
private int count = 0;
public void increment() {
count++;
}
}
「Counter」クラスは仮に複数のスレッドから同時にincrement()が呼ばれた場合、
データ競合の原因になってしまいます。
スレッドセーフの実装例
// スレッドセーフな例
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
スレッドセーフにするだけであれば、シンプルですね!
「synchronized」を使用することで、スレッドごとに排他制御が行われ
データの整合性が保つことができます!
では裏ではどのように動いているのでしょうか?
それはメモリの動きに秘密があります。
スレッドセーフ時のメモリの動き
Java ではオブジェクトはヒープ領域に格納され、
メソッド呼び出しやローカル変数はスタック領域に格納されます。
さてここで聞き慣れない単語が出てきました。
覚えることが多いですが、ここは重要になります!
ヒープ領域とは
動的メモリの確保や解放を繰り返し行えるメモリ領域です。
プログラム実行中に必要に応じてメモリを動的に割り当てることができます。
スタック領域とは
保存が必要な期間だけメモリ領域を確保し、
不要になったら解放するように処理が行われます。
このように、それぞれのメモリ領域の役割やスレッド間のリソース共有の有無が
スレッドセーフなプログラムを作成する際に重要となってます!
ヒープ領域ではスレッド間でデータが共有されますが、スタックはされず。
そのためヒープ領域内の共有データを扱うときは、
排他制御(synchronizedなど)を使って競合を避ける必要があります。
データがどのメモリ領域にあるかで、処理の考え方が重要に!
難しい概念ですが、こんな考え方が大事なのか程度で大丈夫です。
synchronized
1つのスレッドが同期メソッドを処理する際、
他のスレッドは実行を一時停止するため、複数のスレッドから同時に呼び出されなくなります。
排他制御と呼ばれる処理を加えることで、
Javaではスレッドセーフにできるのですね!
メモリ領域のまとめ
特徴 | ヒープ | スタック |
---|---|---|
メモリ割り当て方法 | 動的メモリ割り当て | 静的メモリ割り当て |
アクセス速度 | 遅い(ランダムアクセス) | 速い(連続したメモリアクセス) |
管理 | プログラマーが手動で管理(ガーベジコレクションあり) | 自動的に管理 |
サイズの変動 | サイズが変動する | サイズが限られる |
構造 | フラットメモリ | LIFO構造 |
メモリのサイズ確認
余談ですが、Javaではメモリのサイズの確認が可能です。
以下のコードを使用して、プログラムのメモリ消費を監視できます。
Runtime runtime = Runtime.getRuntime();
System.out.println("Memory Usage: " + (runtime.totalMemory() - runtime.freeMemory()) + " bytes");
- totalMemory = 初期サイズのヒープサイズ
- freeMemory = totalMemoryから現使用領域を引いたサイズ
- maxMemory = 最大サイズのヒープサイズ
参考:
まとめ
スレッドセーフの概念や仕組みについてイメージできましたでしょうか?
実装には必ず必要な概念になりますので、参考になれば嬉しいです!
少々複雑で言語によって仕組みも変わってきますが、正しいコーディング方法や
レビューができるようになれば、レベルアップに繋がります!
エラーや想定外の結果に直面した際も、この考え方で解消することも
あるかと思うので思い出していただければと思います。
参考