2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スレッドセーフを知って安全なアプリを安全にしよう!

Last updated at Posted at 2024-11-10

概要

今回はスレッドセーフについて説明します。

スレッドセーフなコードは、
予期しない動作やデータの不整合を発生させないように設計されています。

本記事ではJavaを例に、スレッドセーフについて知っていただき
サンプルコードを紹介したいと思います!

前提知識

前提知識として、スレッド・マルチスレッドについて軽く触れます。

スレッド・マルチスレッド

  • スレッド:プログラム内で独立して実行される軽量なプロセスの一部のこと
  • マルチスレッド:スレッドを複数同時に実行すること。これにより処理の効率化、高速化の実現が可能になります!

ざっと以下のようなイメージです。

スクリーンショット 2024-11-11 1.09.05.png

スレッドセーフとは

いよいよスレッドセーフについて説明していきます。

スレッドセーフとは?

複数のスレッドが同時に同じリソース(変数・オブジェクト)にアクセスしても
データが壊れたり不整合が発生したりしないようにするための仕組み

・・・これだけですとピンとこないと思いますので
もう少し深掘りしてみましょう。

まずは下の図をご覧ください!

スクリーンショット 2024-11-11 1.15.55.png

こんな感じでマルチスレッドの場合は、裏で色々な処理が並行して
実行され、画面に情報が返ります。

しかし以下の場合はどうでしょうか?
(※赤い矢印に注目!)

スクリーンショット 2024-11-11 1.18.01.png

2つのスレッド処理が同時に画面に返っています。

このように複数のスレッドから同じリソースへのアクセスが
同時に実施されると、競合状態となってしまいます。

複数の処理が競合すると、想定外な結果を招き思わぬバグを
生み出す原因となってしまいます。

このような事態を防ぐ仕組みをスレッドセーフと呼びます。

スレッドセーフでない状態は、スレッドが競合し
データの破損や想定外の不整合が発生する原因になる!

スレッドセーフなコードにするには?

少々シンプルですが、まずはスレッドセーフに対応していない
実装例になります。

スレッドセーフ未対応

NoThreadSafe.java
public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }
}

「Counter」クラスは仮に複数のスレッドから同時にincrement()が呼ばれた場合、
データ競合の原因になってしまいます。

スレッドセーフの実装例

ThreadSafe.java
// スレッドセーフな例
public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}

スレッドセーフにするだけであれば、シンプルですね!

「synchronized」を使用することで、スレッドごとに排他制御が行われ
データの整合性が保つことができます!

では裏ではどのように動いているのでしょうか?
それはメモリの動きに秘密があります。

スレッドセーフ時のメモリの動き

Java ではオブジェクトはヒープ領域に格納され、
メソッド呼び出しやローカル変数はスタック領域に格納されます。

さてここで聞き慣れない単語が出てきました。
覚えることが多いですが、ここは重要になります!

ヒープ領域とは

動的メモリの確保や解放を繰り返し行えるメモリ領域です。
プログラム実行中に必要に応じてメモリを動的に割り当てることができます。

その他ヒープ領域の特徴

  • ガーベジコレクション:Javaは不要になったオブジェクトを自動的に回収することが可能
  • アクセス速度:スタックメモリに比べアクセス速度が遅い
  • サイズの変化:プログラムの実行中にメモリを追加したり解放したりできるため、サイズが変動する
  • 全スレッドで共有して使用する空間

スタック領域とは

保存が必要な期間だけメモリ領域を確保し、
不要になったら解放するように処理が行われます。

その他スタック領域の特徴

  • 各スレッドごとに独立して割り当てられ、スレッド間で共有さない
  • メモリの自動管理:スタックメモリの場合メソッドが終了すると、そのスタックフレームが破棄され、ローカル変数も自動的に解放される
  • サイズの制限:スタックはメモリ量が限られており、過剰なメモリ消費を防ぐためにサイズが制限されている。

→過剰な関数呼び出し(スタックオーバーフロー)により、プログラムがクラッシュする可能性も

このように、それぞれのメモリ領域の役割やスレッド間のリソース共有の有無が
スレッドセーフなプログラムを作成する際に重要となってます!

ヒープ領域ではスレッド間でデータが共有されますが、スタックはされず。
そのためヒープ領域内の共有データを扱うときは、
排他制御(synchronizedなど)を使って競合を避ける必要があります。

データがどのメモリ領域にあるかで、処理の考え方が重要に!
難しい概念ですが、こんな考え方が大事なのか程度で大丈夫です。

synchronized

1つのスレッドが同期メソッドを処理する際、
他のスレッドは実行を一時停止するため、複数のスレッドから同時に呼び出されなくなります。

排他制御と呼ばれる処理を加えることで、
Javaではスレッドセーフにできるのですね!

メモリ領域のまとめ

特徴 ヒープ スタック
メモリ割り当て方法 動的メモリ割り当て 静的メモリ割り当て
アクセス速度 遅い(ランダムアクセス) 速い(連続したメモリアクセス)
管理 プログラマーが手動で管理(ガーベジコレクションあり) 自動的に管理
サイズの変動 サイズが変動する サイズが限られる
構造 フラットメモリ LIFO構造

メモリのサイズ確認

余談ですが、Javaではメモリのサイズの確認が可能です。
以下のコードを使用して、プログラムのメモリ消費を監視できます。

sample.java
Runtime runtime = Runtime.getRuntime();
System.out.println("Memory Usage: " + (runtime.totalMemory() - runtime.freeMemory()) + " bytes");
  • totalMemory = 初期サイズのヒープサイズ
  • freeMemory = totalMemoryから現使用領域を引いたサイズ
  • maxMemory = 最大サイズのヒープサイズ

参考:

まとめ

スレッドセーフの概念や仕組みについてイメージできましたでしょうか?
実装には必ず必要な概念になりますので、参考になれば嬉しいです!

少々複雑で言語によって仕組みも変わってきますが、正しいコーディング方法や
レビューができるようになれば、レベルアップに繋がります!

エラーや想定外の結果に直面した際も、この考え方で解消することも
あるかと思うので思い出していただければと思います。

参考

2
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?