グローバル変数は、オブジェクト指向型プログラミング言語が開発される前に多く使用されていた変数で、その名の通りどこからでも変数にアクセスできるのが特徴です。
Javaの世界にはグローバル変数は存在感しませんが、staticな変数をクラスに宣言することで、グローバル変数に近いことができます。
この記事では、Javaでグローバル変数のような変数を定義する方法と、ローカル変数との違いやマルチスレッド処理におけるグローバル変数の注意点を紹介していきます。
ローカル変数とグローバル変数
ローカル変数とグローバル変数の違いを確認していきましょう。
ローカル変数とは
ローカル変数は、メソッド内の限定された範囲(スコープ)でしか参照できない変数のことです。
次のサンプルコードはローカル変数の宣言例です。メソッド内で宣言した変数「var1」は、そのメソッドの中でしか参照できず、外部のメソッドなどからは参照できません。また、if文などのブロック内で宣言された変数は、そのブロック内からしか参照できず、ifブロック外から変数「var2」を参照使用とすると、コンパイルエラーになります。
public void sample() {
int var1 = 0; //メソッド内でのみ参照可能な変数
if (条件) {
int var2 = 1; //if文の中でのみ参照可能な変数
}
//ここで「var2」を参照すると、コンパイルエラーになる
}
グローバル変数とは
これに対し、グローバル変数というのは、プログラムのどこからでも参照できる変数のことを言います。
Javaにはグローバル変数という考え方はありませんが、「public」と「static」のキーワードを指定することで、外部のクラスからアクセスできるグローバルな変数を作ることができます。
Javaでグローバルな変数を作ってみよう
では、Javaでグローバル変数を作るサンプルコードを見ていきましょう。
public class SampleGlobal{
public static int GlobalInt = 100;
}
このように、Javaでのグローバル変数は、publicスコープのクラスにstaticで修飾した変数を置くことでグローバル変数と同じようなことを実現します。
staticに修飾された変数はインスタンス化する事なく参照することが可能になり、更に、publicを付けることで、他のパッケージやクラスからでも参照できるようになります。
上記で作成した、グローバル変数のようなクラスの変数へは、次のコードのようにしてアクセスすることができます。
// グローバル変数の値を参照
if (SampleGlobal.GlobalInt == 100) {
}
// グローバル変数の値を「200」に書き換え
SampleGlobal.GlobalInt = 200;
グローバル変数の多用は注意
このように、グローバル変数はどこからでもアクセスすることがてぎ、便利ななように思えますが、その反面、あまり使いすぎると管理できなくなり、思わぬバグを生み出してしまうリスクもあります。
そのため、グローバル変数を使う場合は、次のことを抑えておく必要があります。
・グローバル変数が参照されているプログラムの場所
・どのようなタイミングで変数の値が書き換わるのか
短いプログラムのであれば、グローバル変数する箇所を把握するのはそれほど難しいことではないため問題はないでしょう。
しかし、数千行〜数万に及ぶステップを持つプログラムの開発や、複数人での開発を行うプロジェクトでは、どこでどのようにグローバル変数の値が変わっていくのが分からなくなり、結果的にバグを作り込んでしまうことになます。
グローバル変数はスレッドセーフに
Javaではローカル変数のみがスレッドセーフです。
Javaのメモリ領域は、大きく分けてスタック領域とヒープ領域の2種類が存在します。
スタック領域のデータはスレッド毎に用意され、スタック領域のデータは他のスレッドの影響を受けません。そしてJavaのローカル変数はスタック領域で管理されるため、スレッドセーフになります。
反対に、ヒープ領域のデータは複数のスレッドに共有される領域であるため、同時に同じ領域にアクセスを行うと、処理の順序によっては意図しない動作を引き起こすおそれがあります。クラス変数とインスタンス変数などは、このヒープ領域で管理されるためスレッドセーフではありません。
また、Java 言語の多くはWebアプリケーションン開発で用いられておりますが、このWebアプリケーションは、同時に複数のクライアントからのリクエストを処理するため、マルチスレッド環境で動作します。そのため、グローバル変数を用いるときは、複数のスレッドから同時にアクセスされた時のことを考えた設計にする必要があります。
スレッドセーフでないと何が問題なのか?
今回紹介したグローバル変数(クラス変数)とインスタンス変数はスレッドセーフではないことは分かりましたが、スレッドセーフでないと何が問題になるのでしょうか。
ここでは、Javaのグローバル変数が複数ののスレッドから同時アクセスされることにより起きる問題を具体的なサンプルコードと共に紹介します
次のサンプルコードは、10個のThreadクラスを用いて、変数countをスレッドの中で1ずつインクリメントしていき、1〜10までの合計値を並列で求めるコードです。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class Main implements Runnable {
public static int count = 0; //インクリメント値を持つグローバル変数
public static int sum = 0; //合計値を持つグローバル変数;
public static Object lock = new Object();
@Override
public void run() {
Main.count += 1;
Main.sum += Main.count;
//その他の処理
}
public static void main(String[] args) {
final int MAX_THREADS = 10;
// スレッドを実行するためのExecutorServiceを生成
ExecutorService executor = Executors.newFixedThreadPool(MAX_THREADS);
// スレッドを10本立てて並列で1〜10の合計値を求める
for (int i = 0; i < 10; i++) {
// スレッドを起動する。
executor.submit(new Main());
}
// ExecutorServiceを閉じる。
executor.shutdown();
try {
// すべてのスレッドの終了を待機
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
}
System.out.println("合計値=" + String.valueOf(Main.sum));
}
}
このサンプルコードを実行すると次の結果になりました。
※ 同時実行されるタイミングなどが違いから、環境や実行するたびに結果が異なる場合があります。
合計値=56
1〜10までの合計値の答えは当然55であるが、並列でスレッドの処理が同時に動き、countをインクリメントしたことにより、countの数値が他スレッドから同時にインクリメントされてしまい合計値がおかしくなってしまいました。
このように、複数のスレッドから同時にアクセスされると、想定した結果と異なる結果になることがあり、そのような変数などの共有リソースを扱う場合は、排他制御を用います。
Javaでは、スレッド間の排他制御にはsynchronizedキーワードを使用します。
synchronizedキーワードでスレッド間の処理を排他制御することで、並列処理であっても共有リソースにアクセスする処理の時だは、シーケンシャルに処理を行い、共有リソースへの同時アクセスを防げます。
先程のソースを、synchronizedキーワードを使って排他制御すると、次のようなコードになります。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class Main implements Runnable {
public static int count = 0; //インクリメント値を持つグローバル変数
public static int sum = 0; //合計値を持つグローバル変数;
public static Object lock = new Object();
@Override
public void run() {
synchronized(Main.lock) {
Main.count += 1;
Main.sum += Main.count;
}
}
public static void main(String[] args) {
final int MAX_THREADS = 10;
// スレッドを実行するためのExecutorServiceを生成
ExecutorService executor = Executors.newFixedThreadPool(MAX_THREADS);
// スレッドを10本立てて並列で1〜10の合計値を求める
for (int i = 0; i < 10; i++) {
// スレッドを起動する。
executor.submit(new Main());
}
// ExecutorServiceを閉じる。
executor.shutdown();
try {
// すべてのスレッドの終了を待機
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
}
System.out.println("合計値=" + String.valueOf(Main.sum));
}
}
まとめ
Javaでグローバル変数を宣言する方法や、それに関わる注意点などについて解説してきました。
グローバル変数は便利ですか、使いすぎると管理しきれなくなり想定外のバグを生む可能性があるため、注意して使って行きましょう。
グローバル変数は、処理に全く関係のないクラス、メソッドからでも直に参照して値を変更することが可能です。特に必要がない限りは、変数はカプセル化して、処理内容に応じたメソッドなどを通じて変数を参照するようにしましょう。