talosのプログラミング教室

Java Gold合格への道 ~並行処理・CyclicBarrier~

こんにちは。たろすです。

今回は並行処理におけるCyclicBarrierについて説明します。

CyclicBarrierとは

CyclicBarrierは並行処理において各スレッドの足並みをそろえるときに使うクラスです。

例えば以下のようなコードを実行すると、

public class Main {

	public static void main(String[] args) {

		Thread t0 = new Thread(new Runnable() {
			public void run() {
				String threadName = Thread.currentThread().getName();
				System.out.println(threadName + ":start");
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(threadName + ":end");
			}
		});

		Thread t1 = new Thread(new Runnable() {
			public void run() {
				String threadName = Thread.currentThread().getName();
				System.out.println(threadName + ":start");
				try {
					Thread.sleep(5000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(threadName + ":end");
			}
		});

		Thread t2 = new Thread(new Runnable() {
			public void run() {
				String threadName = Thread.currentThread().getName();
				System.out.println(threadName + ":start");
				try {
					Thread.sleep(10000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(threadName + ":end");
			}
		});

		t0.start();
		t1.start();
		t2.start();
	}
}
Thread-0:start
Thread-1:start
Thread-2:start
Thread-0:end
Thread-1:end
Thread-2:end

「Thread-0:end」と表示された後約4秒後に「Thread-1:end」と表示され、さらに約5秒後に「Thread-2:end」と表示されます。

一方で、CyclicBarrierを使うと、

public class Main {

	public static void main(String[] args) {

		CyclicBarrier barrier = new CyclicBarrier(2);

		Thread t0 = new Thread(new Runnable() {
			public void run() {
				String threadName = Thread.currentThread().getName();
				System.out.println(threadName + ":start");
				try {
					Thread.sleep(1000);
					barrier.await();
				} catch (InterruptedException | BrokenBarrierException e) {
					e.printStackTrace();
				}
				System.out.println(threadName + ":end");
			}
		});

		Thread t1 = new Thread(new Runnable() {
			public void run() {
				String threadName = Thread.currentThread().getName();
				System.out.println(threadName + ":start");
				try {
					Thread.sleep(5000);
					barrier.await();
				} catch (InterruptedException | BrokenBarrierException e) {
					e.printStackTrace();
				}
				System.out.println(threadName + ":end");
			}
		});

		Thread t2 = new Thread(new Runnable() {
			public void run() {
				String threadName = Thread.currentThread().getName();
				System.out.println(threadName + ":start");
				try {
					Thread.sleep(10000);
					barrier.await();
				} catch (InterruptedException | BrokenBarrierException e) {
					e.printStackTrace();
				}
				System.out.println(threadName + ":end");
			}
		});

		t0.start();
		t1.start();
		t2.start();
	}
}
Thread-1:start
Thread-0:start
Thread-2:start
Thread-1:end
Thread-0:end

実行から約10秒経ってから「Thread-0:end」と「Thread-1:end」が同時に表示されます。

ちなみに「Thread-2:end」が表示されないのはコンストラクタの引数(足並みをそろえるスレッドの数)を2に設定しているからです。

先に到着した二つのスレッドは先に行ってしまい、残りのスレッドは一つになってしまったので最後のスレッドは永遠に待機状態のままです。

おわりに

今回は並行処理におけるCyclicBarrierについて説明しました。

Java Gold合格への道 ~並行処理・並行コレクション~

こんにちは。たろすです。

今回は並行処理における並行コレクションについて説明します。

コレクションと並行コレクションの違い

コレクションと言えばArrayListやHashMapなどがあります。

これらはスレッド・セーフではなく、並行処理においてConcurrentModificationExceptionが発生する可能性があります。

一方で並行コレクションはスレッド・セーフのためこの例外は発生しません。

並行コレクションにはCopyOnWriteArrayListやConcurrentMapなどがあります。

試しにCopyOnWriteArrayListを使ってその違いを確認してみましょう。

ArrayListでの実装

public class Main {
	public static void main(String[] args) {

		List<Integer> list = new ArrayList<>();

		for (int i = 0; i < 1000; i++) {
			list.add(i);
		}

		Thread t1 = new Thread(new Runnable() {
			public void run() {
				for (int i = 0; i < 1000; i++) {
					list.remove(0);
				}
			}
		});

		Thread t2 = new Thread(new Runnable() {
			public void run() {
				for (Integer num : list) {
					System.out.println(num);
				}
			}
		});

		t1.start();
		t2.start();
	}
}
Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at collection.Main$2.run(Main.java:27)
	at java.lang.Thread.run(Thread.java:748)

上記のコードは0~999までの数値をArrayListに格納し、先頭から削除すると同時に走査しています。

当然走査している最中にArrayListが変更されてしまうので、ConcurrentModificationExceptionが発生します。

CopyOnWriteArrayListでの実装

これをConcurrentModificationExceptionが発生しないようにCopyOnWriteArrayListで実装するとどうなるかというと、

public class Main {
	public static void main(String[] args) {

		List<Integer> list = new CopyOnWriteArrayList<>();

		for (int i = 0; i < 1000; i++) {
			list.add(i);
		}

		Thread t1 = new Thread(new Runnable() {
			public void run() {
				for (int i = 0; i < 1000; i++) {
					list.remove(0);
				}
			}
		});

		Thread t2 = new Thread(new Runnable() {
			public void run() {
				for (Integer num : list) {
					System.out.println(num);
				}
			}
		});

		t1.start();
		t2.start();
	}
}
76
77

省略

998
999

Listの定義をArrayListからCopyOnWriteArrayListに変えただけです。

CopyOnWriteArrayListを使うことによりConcurrentModificationExceptionが発生しなくなりました。

おわりに

今回は並行処理における並行コレクションについて説明しました。

Java Gold合格への道 ~並行処理・アトミック変数~

こんにちは。たろすです。

今回は並行処理におけるアトミック変数について説明します。

アトミック変数とは

アトミックとは「原子の」という意味を持ちます。

並行処理におけるアトミックとは複数の処理を一つの操作として扱い、途中から介入できない性質を言います。

アトミック変数はスレッド・セーフなプログラミングを可能にします。

上記の説明ではわかりにくいと思います。

例を出してみましょう。

public class ThreadSample extends Thread {
	static private int num;

	public void run() {
		for (int i = 0; i < 100000; i++) {
			System.out.println(++num);
		}
	}
}
public class Main {

	public static void main(String[] args) {
		ThreadSample thread1 = new ThreadSample();
		ThreadSample thread2 = new ThreadSample();
		ThreadSample thread3 = new ThreadSample();

		thread1.start();
		thread2.start();
		thread3.start();
	}
}

上記のMainを実行したときの最終的なnumの値はいくつになるでしょうか。

3つのスレッドで100000回ずつインクリメントしているから300000?

そうとは限りません。

threa1とthread2が同時にnumにアクセスしてそれぞれインクリメントし、numに上書きした場合、本来2回インクリメントされているから+2されるべきところが同じ値に対してインクリメントしたため+1しかされません。


一方で、アトミック変数を使った場合どうなるか。

public class ThreadSample extends Thread {
	static private AtomicInteger num = new AtomicInteger(0);

	public void run() {
		for (int i = 0; i < 100000; i++) {
			System.out.println(num.incrementAndGet());
		}
	}
}

同じmainを実行すると必ず結果は300000になります。

これはスレッド・セーフであるから、つまりあるスレッドが変数にアクセスしているときに他のスレッドは変数にアクセスできないようになっているからです。

おわりに

今回は並行処理におけるアトミック変数について説明しました。

Java Gold合格への道 ~Java I/O・ファイルツリーのトラバース~

こんにちは。たろすです。

今回はJava I/Oのファイルツリーのトラバースについて説明します。

方法

ファイルのトラバースはwalkFileTreeメソッドやwalkメソッドで行います。

walkFileTree

try {
	Files.walkFileTree(Paths.get("dir0"), new SimpleFileVisitor<Path>() {
		public FileVisitResult preVisitDirectory(Path file, BasicFileAttributes attrs) throws IOException {
			System.out.println(file);
			return FileVisitResult.CONTINUE;
		}
	});
} catch (IOException e) {
	e.printStackTrace();
}
dir0
dir0\dir1
dir0\dir2
dir0\dir2\dir3
dir0\dir2\dir4
dir0\dir2\dir4\dir5

第一引数は始点となるパスで、第二引数はFileVisitorインタフェースです。

SimpleFileVisitorクラスはFileVisitorクラスの簡易実装として四つのメソッドの実装を提供しています。

今回はディレクトリ内のエントリをたどる前に呼び出されるpreVisitDirectoryのみオーバーライドしました。

その他に以下のメソッドがあります。

postVisitDirectory:ディレクトリ内のエントリおよびそれらのサブディレクトリをたどった後に呼び出される。

visitFile:ディレクトリ内のファイルをたどった際に呼び出される。

visitFileFailed:たどれなかったファイルに対して呼び出される。

walk

try {
	Files.walk(Paths.get("dir0")).forEach(System.out::println);
} catch (IOException e) {
	e.printStackTrace();
}
dir0
dir0\dir1
dir0\dir2
dir0\dir2\dir3
dir0\dir2\dir4
dir0\dir2\dir4\dir5

walkメソッドも第一引数は始点となるパスです。

戻り値はStream<Path>オブジェクトです。

おわりに

今回はJava I/Oのファイルツリーのトラバースについて説明しました。

Java Gold合格への道 ~Java I/O・Filesクラス~

こんにちは。たろすです。

今回はJava I/OのFilesクラスについて説明します。

FileクラスとFilesクラスの違い

FileクラスではFileオブジェクトを生成し、メソッドを呼ぶことでファイル操作をしていました。

一方でFilesクラスはstaticメソッドのみで、引数にPathオブジェクトをとることでファイル操作します。

Filesクラスの主なメソッド

try {
	// ファイルのコピー
	System.out.println("filesディレクトリの中身(コピー前):");
	Files.list(Paths.get("files")).forEach(System.out::println);
	System.out.println();
	Files.copy(Paths.get("files/text.txt"), Paths.get("files/text2.txt"));
	System.out.println("filesディレクトリの中身(コピー後):");
	Files.list(Paths.get("files")).forEach(System.out::println);
	System.out.println();

	// ファイルの移動
	System.out.println("tmpディレクトリの中身(移動前):");
	Files.list(Paths.get("files/tmp")).forEach(System.out::println);
	System.out.println();
	Files.move(Paths.get("files/text2.txt"), Paths.get("files/tmp/text2.txt"));
	System.out.println("filesディレクトリの中身(移動後):");
	Files.list(Paths.get("files")).forEach(System.out::println);
	System.out.println();
	System.out.println("tmpディレクトリの中身(移動後):");
	Files.list(Paths.get("files/tmp")).forEach(System.out::println);
	System.out.println();

	// 属性の確認
	System.out.println("最終更新日時:" + Files.getAttribute(Paths.get("files/text.txt"), "lastModifiedTime"));
	System.out.println("最終アクセス日時:" + Files.getAttribute(Paths.get("files/text.txt"), "lastAccessTime"));
	System.out.println("作成日時:" + Files.getAttribute(Paths.get("files/text.txt"), "creationTime"));
	System.out.println("サイズ:" + Files.getAttribute(Paths.get("files/text.txt"), "size"));
} catch (IOException e) {
	e.printStackTrace();
}
filesディレクトリの中身(コピー前):
files\text.txt
files\tmp

filesディレクトリの中身(コピー後):
files\text.txt
files\text2.txt
files\tmp

tmpディレクトリの中身(移動前):

filesディレクトリの中身(移動後):
files\text.txt
files\tmp

tmpディレクトリの中身(移動後):
files\tmp\text2.txt

最終更新日時:2022-02-01T09:12:38.929466Z
最終アクセス日時:2022-02-19T09:47:20.39287Z
作成日時:2022-02-01T08:01:59.585525Z
サイズ:11

list

ディレクトリ内のファイル、ディレクトリの一覧をStream<Path>オブジェクトとして返します。

copy

ファイルやディレクトリをコピーします。

第一引数はコピー元のパス、第二引数はコピー先のパスです。

第三引数は以下を設定することができます。

LinkeOption.NOFOLLOW_LINKS:シンボリック・リンクをだとらない。

StandardCopyOption.COPY_ATTRIBUTES:ファイル属性をコピーする。

StandardCopyOption.REPLACE_EXISTING:コピー先にコピー元が存在する場合は置換する。

ディレクトリをコピーする場合は中身はコピーされないので注意しましょう。

move

ファイルやディレクトリを移動します。

第一引数はコピー元のパスで、第二引数はコピー先のパスです。

第三引数はcopyと同じ列挙定数を指定できます。

getAttribute

ファイルやディレクトリの属性を取得します。

第一引数にファイルのパス、第二引数に取得する属性の名前(文字列)を指定します。

第二引数に指定できる属性名はコード中ででてきたものの他にisRegularFile、isDirectory、isSymbolickLink、isOther、fileKeyがあります。

第三引数には以下の列挙定数を指定することができます。

LinkeOption.NOFOLLOW_LINKS:シンボリック・リンクをだとらない。

おわりに

今回はJava I/OのFilesクラスについて説明しました。

Java Gold合格への道 ~Java I/O・Pathインタフェース~

こんにちは。たろすです。

今回はJava I/OのPathインタフェースについて説明します。

Pathインタフェースとは

PathインタフェースはJava SE 7から追加されたファイルやディレクトリのパスを表すクラスです。

Java SE 6以前はFileクラスが用いられて気ましたが、様々な扱いにくさがありPathインタフェースが追加されました。

Pathインタフェースの主なメソッド

Path path = Paths.get("C:/aaa/bbb/ccc");

System.out.println("ルート:" + path.getRoot());
System.out.println("最下層のパス名:" + path.getFileName());
System.out.println("ルートを除くパス階層数:" + path.getNameCount());
System.out.println("パスの抜き出し:" + path.subpath(1, 2));
System.out.println("解決(絶対パス):" + path.resolve(Paths.get("D:/ddd")));
System.out.println("解決(相対パス):" + path.resolve(Paths.get("eee")));
System.out.println("親パスに対して解決(絶対パス):" + path.resolveSibling(Paths.get("D:/ddd")));
System.out.println("親パスに対して解決(相対パス):" + path.resolveSibling(Paths.get("eee")));

Path p0 = Paths.get("C:/aaa/./bbb/../ccc");
System.out.println("冗長なパスを正規化:" + p0.normalize());

Path p1 = Paths.get("C:/ddd/eee");
System.out.println("相対パスとして解決:" + path.relativize(p1));
ルート:C:\
最下層のパス名:ccc
ルートを除くパス階層数:3
パスの抜き出し:bbb
解決(絶対パス):D:\ddd
解決(相対パス):C:\aaa\bbb\ccc\eee
親パスに対して解決(絶対パス):D:\ddd
親パスに対して解決(相対パス):C:\aaa\bbb\eee
冗長なパスを正規化:C:\aaa\ccc
相対パスとして解決:..\..\..\ddd\eee

getRoot

パスのルートを返します。

getFileName

最下層のパス名を返します。

最下層がファイルではない場合も最下層のディレクトリ名を返すので注意しましょう。

パスがルートだけの場合はnullを返します。

getNameCount

ルートを除いたパス階層の数を返します。

パスがルートだけの場合は0を返します。

subpath

パスの一部を抜き出して返します。

resolve

指定されたパスを呼び出し元のパスに対して解決します。

具体的には、引数が絶対パスの場合は引数のパスがそのまま返されます。

相対パスの場合には呼び出し元のパスに引数のパスを結合したものを返します。

resolveSibling

引数のパスを呼び出し元の親パスに対して解決します。

具体的には、引数が絶対パスの場合は引数のパスがそのまま返されます。

相対パスの場合には呼び出し元の親パスに引数のパスを結合したものを返します。

normalize

パスに含まれる冗長な表現を正規化したパスを返します。

「.」はカレントディレクトリを表すため省略され、「..」は親ディレクトリを表すためひとつ上のディレクトリに戻ります。

relativize

呼び出し元のパスから引数のパスへの相対パスを返します。

おわりに

今回はJava I/OのPathインタフェースについて説明しました。

Java Gold合格への道 ~Java I/O・オブジェクトの直列化と復元~

こんにちは。たろすです。

今回はJava I/Oのオブジェクトの直列化と復元について説明します。

直列化はなんのために行う?

そもそも直列化とはなにかというと、オブジェクトをバイト列として一列に並べることです。

そうすることによって、アプリケーションが終了してもオブジェクトを保存しておいて、再度アプリケーションが起動したときにオブジェクトを復元できます。

また、直列化されたオブジェクトはネットワーク上で送信することもできます。

直列化

直列化はwriteObjectメソッドによって行います。

直列化したいオブジェクトにはSerializableインタフェースを実装します。

public class Data implements Serializable {

	private int id;
	private int num;

	public Data(int id, int num) {
		this.id = id;
		this.num = num;
	}

	public String toString() {
		return id + ":" + num;
	}

        // setter & getter
}
Data outData = new Data(1, 5);

try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("sample.ser"))) {
	oos.writeObject(outData);
} catch (FileNotFoundException e) {
	e.printStackTrace();
} catch (IOException e) {
	e.printStackTrace();
}

復元

復元はreadObjectメソッドを使って行います。

戻り値はオブジェクト型なので元の型にキャストします。

try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("sample.ser"))) {
	Data inData = (Data) ois.readObject();
	System.out.println(inData);
} catch (FileNotFoundException e) {
	e.printStackTrace();
} catch (IOException e) {
	e.printStackTrace();
} catch (ClassNotFoundException e) {
	e.printStackTrace();
}
1:5

おわりに

今回はJava I/Oのオブジェクトの直列化と復元について説明しました。