LoginSignup
2
3

More than 3 years have passed since last update.

Flyweight パターンとConcurrentHashMap を Java で学ぶ

Posted at

Java の知識をアップデートしようとして、Essential Java の第三刷を読んでいるのだが、Flyweight パターンが出てきた。そういえばこのパターンはちゃんとやったことがないので、理解して試してみることにした。

Flyweight パターン

Flyweight パターンは、GoFのデザインパターンの一つで、次のような問題を解くためのパターンである。

  • 多くのオブジェクトが効率的にサポートされる
  • 多くのオブジェクトの生成を避けたい

定義

  • 本質的なステートがシェアできる
  • (本質的なステートを区別できる)ステートを投入するインターフェイスがある。

何かこれだけだとよくわからない。

image.png

シーケンスを見ると、どうやらキャッシュするパターンのようだ。歴史の項目を見てみると、ドキュメントエディタで使われたようで、フォントの情報とか、例えばアルファベットが26文字だとすると、その都度文字のインスタンスを生成していたら、インスタンスが大量に生成されてしまう。だから、26個だけインスタンスを生成するようにして、それをキャッシュして返す仕組みのようだ。

ギターフレットの音のサンプル

サンプルを作って理解してみよう。要はファクトリーのキャッシュしたものであるので、簡単だ。今回のお題は若干強引だが、ギターの弦の番号と、フレットの番号を入力すると、その場所の音を返すアプリケーションだ。音は12個しかないので、インスタンスは、12個でよいはず。デザインパターンのダイアグラムでいう Flyweight のオブジェクトとして、Sound を使ってみる。

Sound.java : Flyweight

package com.company;

public interface Sound {
    void Play();
}

SoundImpl : Flywieght1

package com.company;

public class SoundImpl implements Sound{
    private String note;
    public SoundImpl(String note) {
        this.note = note;
    }
    @Override
    public void Play() {
        System.out.println(note + "- ♪");
    }
    public String getNote() {
        return note;
    }
}

SoundFactory : FlywieghtFactory

package com.company;

import java.util.HashMap;

public class SoundFactory {
    private static Map<String, Sound> sounds = new HashMap<String, Sound>();
    public static Sound getSound(String note) {
        if (sounds.containsKey(note)){
            return sounds.get(note);
        } else {
            Sound sound = new SoundImpl(note);
            sounds.put(note, sound);
            return sound;
        }
    }
}

Main が膨れ上がってかっこ悪いが、ともかく、動作するものが出来た。単純でポイントは、Factoryに Map を持たせて、キャッシュしておけば良い。簡単だ。

Main.java

package com.company;

import java.util.HashMap;

public class Main {
    private static HashMap<Integer, Integer> stringMap = new HashMap<Integer, Integer>();
    private static HashMap<Integer, String> notes = new HashMap<Integer, String>();
    private static void setup() {
        stringMap.put(1, 4);
        stringMap.put(2, 11);
        stringMap.put(3, 7);
        stringMap.put(4, 2);
        stringMap.put(5, 9);
        stringMap.put(6, 4);

        notes.put(0, "C");
        notes.put(1, "C#");
        notes.put(2, "D");
        notes.put(3, "D#");
        notes.put(4, "E");
        notes.put(5, "F");
        notes.put(6, "F#");
        notes.put(7, "G");
        notes.put(8, "G#");
        notes.put(9, "A");
        notes.put(10, "A#");
        notes.put(11, "B");

    }
    public static void main(String[] args) {
        setup();
        while(true) {
            java.io.Console con = System.console();
            if (con != null) {
                String input = con.readLine("string:fret:");
                if (input.contains("exit")) {
                    System.out.println("Closing ...");
                    System.exit(0);
                } else {
                    String[] stringFret = input.split(":");
                    int openNote = stringMap.get(Integer.parseInt(stringFret[0]));
                    int fret = Integer.parseInt(stringFret[1]);
                    int note = openNote + fret;
                    if (note >= 12) {
                        note = note - 12;
                    }
                    String noteString =  notes.get(new Integer(note));
                    Sound sound = SoundFactory.getSound(noteString);
                    sound.Play();
                }
            }

        }
    }

}

Concurrent だとどうなるのか?

本編が簡単だったので、Javaにあまり慣れていないので、もうステップやってみることにした。コンカレントな状態だと、どうすればいいのだろう?HashMap は、Thread Safe ではないらしい。Thread Safe にしたかったら HashTable もしくは、ConcurrentHashMap を使うと良いらしい。いったい何が違うのだろうか?

HashTable と ConcurrentHashMap

  • HashMapConcurrentHashMap の違いは、スレッドセーフか否か。つまりコンカレントの環境で使えるか否か。HashTable が提供するレベルの同期レベルを提供できるではないが、実用では十分なれべる
  • HashMap は、Collections.synchronizedMap でラップするとHashTableとほぼ同等のコレクションが返される。それは、すべての更新イベントで、Map 全体がロックされる。ConcurrentHashMap の場合は、スレッドセーフを全体のMapをパーティション分割して、一部だけロックするような仕組みになっている。
  • ConcurrentHashMap は、よりスケーラブルで、パフォーマンスもSynchronized HashMap より良い。シングルスレッドの環境下では、HashMap の方が、ConcurrentHashMap よりパフォーマンスが良い

これらの比較を見てみると、明らかに ConcurrentHashMap を使うのが妥当なケースが多そうだ。じゃあ実装してみよう。

Micronaut を使って実装する

簡単そうだ。

Micronaut install

私の環境では、chocolatey のソースが書き換わっていたので、デフォルトに戻す。 Administrator 権限で PowerShellを起動するとインストールできる。

$ choco isntall micronaut -s https://chocolatey.org/api/v2/

Generate project via template

次の感じで Maven のプロジェクトを生成できる。

$ mn create-app flyweight-server --build maven

まずはこれで、サーバーは上がるようになる。自分がハマったポイントは、自分は、IntelliJ を使ってコードを書くのだが、Terminal の JAVA_HOME が設定されていなくて、Caused by: java.lang.IllegalArgumentException: invalid target release: 11 というエラーメッセージが出た。もともとは、Java8 の JDK だけパスにあったので、それが問題なのだが、Java の基本は、JAVA_HOME の設定と、%JAVA_HOME%/bin をパスに通すことだと改めて思いだした。そうでないと、ちゃんと maven が動作しない。

$ cd flywieght-server
$ mvn clean package
$ java -jar .\target\flyweight-server-0.1.jar

 コントローラの追加

コントローラを追記してみる。Micronautは、どちらかというと、Spring の軽量版みたいな感じで様々な機能をサポートしている様子。私は今回 Http Server が欲しいだけだったので、マニュアルを見てコードを書いてみる。コントローラーとルーディングをするのは、単にコントローラを書けばよいみたい。とても簡単でござる。

*The HTTP Server

FlyweightController.java

package flyweight.server;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

@Controller("/flyweight")
public class FlyweightController {
    @Get(value = "/{string}/{fret}", produces = MediaType.TEXT_PLAIN)
    public String index(Integer string, Integer fret){
        return "String: " + string + " Fret: "+ fret;
    }
}

これで動いたので、コントローラーの知りたい挙動は理解できた。次に Flyweight のアプリを改造してみよう。これは、マルチスレッドで動作すると思うので、先ほどの ConcurrentHashMap を使ってみよう。

なんと先ほどのより短くなってしまったではないか。computeIfAbsent メソッドが、関数を受け取るようになってきて、アトミックな動作を保証してくれる。これは、C# の ConcurrentDictionary と同じ雰囲気だ。だから、もし、なかったら新たに作って返すし、そうでなければ、既存のものを返してくれる。最高!

SoundFactory.java

package flyweight.server;

import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class SoundFactory {
    private static ConcurrentMap<String, Sound> sounds = new ConcurrentHashMap<String, Sound>();
    public static Sound getSound(String note) {
        return sounds.computeIfAbsent(note, n -> new SoundImpl(n));
    }
}

全体像

FlyweightController.java

package flyweight.server;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

@Controller("/flyweight")
public class FlyweightController {
    @Get(value = "/{string}/{fret}", produces = MediaType.TEXT_PLAIN)
    public String index(Integer string, Integer fret){
        Integer openNote = Constants.stringMap.get(string.intValue());
        int note = openNote.intValue() + fret.intValue();
        if (note >= 12) {
            note = note - 12;
        }
        Sound sound = SoundFactory.getSound(Constants.notes.get(note));
        return "String: " + string + " Fret: "+ fret + " Sound: " + sound.Play();
    }
}

Constants.java

package flyweight.server;

import java.util.HashMap;

public class Constants {
    static HashMap<Integer, Integer> stringMap = new HashMap<Integer, Integer>();
    static HashMap<Integer, String> notes = new HashMap<Integer, String>();
    static void setup() {
        stringMap.put(1, 4);
        stringMap.put(2, 11);
        stringMap.put(3, 7);
        stringMap.put(4, 2);
        stringMap.put(5, 9);
        stringMap.put(6, 4);

        notes.put(0, "C");
        notes.put(1, "C#");
        notes.put(2, "D");
        notes.put(3, "D#");
        notes.put(4, "E");
        notes.put(5, "F");
        notes.put(6, "F#");
        notes.put(7, "G");
        notes.put(8, "G#");
        notes.put(9, "A");
        notes.put(10, "A#");
        notes.put(11, "B");

    }
}

Sound.java

package flyweight.server;

public interface Sound {
    String Play();
}

SoundImpl.java

package flyweight.server;

public class SoundImpl implements Sound {
    private String note;

    public SoundImpl(String note) {
        this.note = note;
    }

    public String Play() {
        return this.note + "- note";
    }

    public String getNote() {
        return this.note;
    }
}

Application.java

package flyweight.server;

import io.micronaut.runtime.Micronaut;

public class Application {

    public static void main(String[] args) {
        Constants.setup();
        Micronaut.run(Application.class, args);
    }
}

実行結果

[INFO] Replacing original artifact with shaded artifact.
[INFO] Replacing C:\Users\tsushi\Code\java\spike\micronaut\flyweight-server\target\flyweight-server-0.1.jar with C:\Users\tsushi\Code\java\spike\micronaut\flyweight-server\target\flyweight-server-0.1-shaded.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  7.444 s
[INFO] Finished at: 2020-10-26T14:16:48-07:00
[INFO] ------------------------------------------------------------------------
PS C:\Users\tsushi\Code\java\spike\micronaut\flyweight-server> java -jar .\target\flyweight-server-0.1.jar
[36m14:17:24.022[0;39m [1;30m[main][0;39m [34mINFO [0;39m [35mio.micronaut.runtime.Micronaut[0;39m - Startup completed in 1154ms. Server Running: http://localhost:8080

image.png

今日は軽く Flyweight パターンと、ConcurrentHashMap を学べたので良しとしましょう。

Resource

2
3
0

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
3