ThreadPoolExecutorのコアプールサイズと最大プールサイズ


回答:


130

このブログ投稿から:

この例を見てみましょう。開始スレッドプールサイズは1、コアプールサイズは5、最大プールサイズは10、キューは100です。

リクエストが届くと、最大5つのスレッドが作成され、100に達するまでタスクがキューに追加されます。キューがいっぱいになると、最大で新しいスレッドが作成されmaxPoolSizeます。すべてのスレッドが使用され、キューがいっぱいになると、タスクは拒否されます。キューが減少すると、アクティブなスレッドの数も減少します。


これは正しいです?maxPoolSizeに達するまで新しいスレッドが作成されると思いました。その後、新しいスレッドがキューに入れられます。私が間違っている場合は修正してください..
Glide

4
はい、これは正しいです。キューにタスクがある場合にのみ、スレッドはcorePoolSizeを超えて追加されます。これらの追加のスレッドは、キューがゼロに達すると「消えます」。
ルーク

3
allowCoreThreadTimeOut(boolean)与えられたアイドル時間の後にコアスレッドを強制終了できる興味深い方法があります。これをtrueに設定し、core threads= を設定max threadsすると、スレッドプールを0からの間でスケーリングできますmax threads
Jaroslaw Pawlak

4
あなたはちょうどここからそれをコピーしbigsoft.co.uk/blog/index.php/2009/11/27/...
クマールマニッシュを

1
拒否されたタスクはどうなりますか?
ワックス

53

IF 実行中のスレッド> corePoolSize&<maxPoolSize総タスクキューがいっぱいになると新しいものが到着している場合は、新しいスレッドを作成します。

フォームDOC:(複数ある場合corePoolSize未満maximumPoolSizeのスレッドが実行されているが、キューがいっぱいになった場合にのみ、新しいスレッドが作成されます。)

さて、簡単な例を挙げましょう。

ThreadPoolExecutor executorPool = new ThreadPoolExecutor(5, 10, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(50));

ここで、5はcorePoolSizeです。つまり、JVMは最初の5つのタスクの新しいタスク用に新しいスレッドを作成します。その他のタスクは、キューがいっぱいになるまでキューに追加されます(50タスク)。

10はmaxPoolSizeです-JVMは最大10のスレッドを作成できます。すでに5つのタスク/スレッドが実行中であり、キューが50の保留中のタスクでいっぱいであり、さらに1つの新しい要求/タスクがキューに到着している場合、JVMは最大10の新しいスレッドを作成します(合計スレッド=前の5 +新しい5) ;

新しいArrayBlockingQueue(50) =合計キューサイズ-50個のタスクをキューに入れることができます。

10個のスレッドがすべて実行されると、新しいタスクが到着すると、その新しいタスクは拒否されます。

SUNが内部でスレッドを作成するための規則:

  1. スレッド数がcorePoolSize未満の場合は、新しいスレッドを作成して新しいタスクを実行します。

  2. スレッドの数がcorePoolSizeと等しい(またはそれより大きい)場合は、タスクをキューに入れます。

  3. キューがいっぱいで、スレッドの数がmaxPoolSize未満の場合は、タスクを実行する新しいスレッドを作成します。

  4. キューがいっぱいで、スレッドの数がmaxPoolSize以上の場合、タスクを拒否します。

これはHelpFulです。私が間違っている場合は修正してください...


21

ドキュメントから:

メソッドexecute(java.lang.Runnable)で新しいタスクが送信され、実行中のcorePoolSizeスレッドより少ない場合、他のワーカースレッドがアイドル状態であっても、リクエストを処理するために新しいスレッドが作成されます。corePoolSizeを超えてmaximumPoolSize未満のスレッドが実行されている場合、キューがいっぱいの場合にのみ、新しいスレッドが作成されます。

さらに:

corePoolSizeとmaximumPoolSizeを同じに設定することで、固定サイズのスレッドプールを作成します。maximumPoolSizeをInteger.MAX_VALUEなどの本質的に無制限の値に設定することにより、プールが任意の数の並行タスクに対応できるようにします。最も一般的には、コアおよび最大プールサイズは構築時にのみ設定されますが、setCorePoolSize(int)およびsetMaximumPoolSize(int)を使用して動的に変更することもできます。


1)execute(java.lang.Runnable)メソッドで新しいタスクが送信され、実行中のcorePoolSizeスレッドより少ない場合、他のワーカースレッドがアイドル状態であっても、リクエストを処理するために新しいスレッドが作成されます。アイドルスレッドがある場合、リクエストを処理するために新しいスレッドを作成する必要があるのはなぜですか?
user2568266 2013

1
2)corePoolSizeを超えてmaximumPoolSize未満のスレッドが実行されている場合、キューがいっぱいの場合にのみ、新しいスレッドが作成されます。ここでは、corePoolSizeとmaximumPoolSizeの違いがわかりません。次に、スレッドがmaximumPoolSize未満の場合、キューはどのようにいっぱいになるのでしょうか。キューがいっぱいになるのは、スレッドがmaximumPoolSizeと等しい場合のみです。だよね?
user2568266 2013

9

ファクトリクラスThreadPoolExecutorを使用する代わりに手動で作成する場合Executorsは、コンストラクタの1つを使用して作成および構成する必要があります。このクラスの最も広範なコンストラクターは次のとおりです。

public ThreadPoolExecutor(
    int corePoolSize,
    int maxPoolSize,
    long keepAlive,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    RejectedExecutionHandler handler
);

ご覧のとおり、以下を構成できます。

  • コアプールサイズ(スレッドプールが固執しようとするサイズ)。
  • 最大プールサイズ。
  • キープアライブ時間。アイドルスレッドが破棄されるまでの時間です。
  • 実行待ちのタスクを保持するワークキュー。
  • タスクの送信が拒否されたときに適用するポリシー。

キューに入れるタスクの数を制限する

実行中の並行タスクの数を制限し、スレッドプールのサイズを変更することは、予測可能性と安定性の点で、アプリケーションとその実行環境に大きなメリットをもたらします。無制限のスレッドの作成は、最終的にランタイムリソースを使い果たし、結果としてアプリケーションが経験する可能性があります。 、アプリケーションの不安定性につながる可能性のある深刻なパフォーマンスの問題。

これは、問題の一部の解決策です。実行中のタスクの数に上限を設定していますが、後で実行するためにサブミットしてキューに入れることができるジョブの数に上限はありません。アプリケーションのリソース不足は後で発生しますが、最終的には、送信率が実行率を常に上回った場合に発生します。

この問題の解決策は、次のとおりです。実行待ちのタスクを保持するための実行キューにブロッキングキューを提供します。キューがいっぱいになると、送信されたタスクは「拒否」されます。RejectedExecutionHandlerタスクの提出が却下されたときに呼び出され、拒否された動詞が前の項目に引用された理由のこと。独自の拒否ポリシーを実装することも、フレームワークが提供する組み込みポリシーの1つを使用することもできます。

デフォルトの拒否ポリシーでは、executorがをスローしRejectedExecutionExceptionます。ただし、他の組み込みポリシーを使用すると、次のことができます。

  • 黙ってジョブを破棄します。
  • 最も古いジョブを破棄して、最後のジョブを再実行依頼してください。
  • 拒否されたタスクを呼び出し元のスレッドで実行します。

5

ソース

ThreadPoolExecutorプールサイズのルール

ThreadPoolExecutor'sプールのサイズに関するルールは、一般に誤解されています。それは、プールが意図したとおりに、または希望どおりに機能しないためです。

この例を見てみましょう。開始スレッドプールサイズは1、コアプールサイズは5、最大プールサイズは10、キューは100です。

Sunの方法:リクエストがスレッドに到着すると最大5つまで作成され、タスクは100に達するまでキューに追加されます。キューがいっぱいになると、新しいスレッドが最大で作成されmaxPoolSizeます。すべてのスレッドが使用され、キューがいっぱいになると、タスクは拒否されます。キューが減少すると、アクティブなスレッドの数も減少します。

ユーザーが予想した方法:リクエストはスレッドで受信されるため、最大10個作成され、タスクが100に達するまでキューに追加されます。この時点で、タスクは拒否されます。キューが空になるまで、スレッドの数は最大で名前が変更されます。キューが空になると、スレッドがなくなるまでスレッドは停止しcorePoolSizeます。

違いは、ユーザーはプールサイズをより早く増やし始め、キューを小さくしたいということですが、Sunの方法ではプールサイズを小さく保ち、負荷が大きくなったときにのみそれを増やしたいと考えています。

簡単な用語でのスレッド作成に関するSunのルールは次のとおりです。

  1. スレッド数が未満の場合はcorePoolSize、新しいスレッドを作成して新しいタスクを実行します。
  2. スレッドの数がと等しい(またはそれより大きい)corePoolSize場合は、タスクをキューに入れます。
  3. キューがいっぱいで、スレッドの数が未満の場合はmaxPoolSize、タスクを実行する新しいスレッドを作成します。
  4. キューがいっぱいで、スレッド数が以上の場合maxPoolSize、タスクを拒否します。長い点と短い点は、キューがいっぱいになったときにのみ新しいスレッドが作成されるため、無制限のキューを使用している場合、スレッドの数はを超えませんcorePoolSize

より完全な説明については、馬の口から入手してください:ThreadPoolExecutorAPIドキュメント。

ThreadPoolExecutorコード例を処理する方法について説明する、非常に優れたフォーラム投稿があります。http//forums.sun.com/thread.jspa?threadID = 5401400&tstart = 0

詳細:http : //forums.sun.com/thread.jspa?threadID=5224557&tstart=450


3

corepoolsizeおよびmaxpoolsizeという用語の定義は、javadocにあります。http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/ThreadPoolExecutor.html

上記のリンクはあなたの質問への答えを持っています。ただし、明確にするためです。アプリケーションは、corePoolSizeに達するまでスレッドを作成し続けます。ここでの考え方は、これらの多くのスレッドはタスクの流入を処理するのに十分でなければならないということです。corePoolSizeスレッドが作成された後に新しいタスクが来ると、タスクはキューに入れられます。キューがいっぱいになると、executorは新しいスレッドの作成を開始します。それは一種のバランスです。それが本質的に意味することは、タスクの流入が処理能力以上であることです。したがって、Executorはスレッドの最大数に達するまで、新しいスレッドの作成を再開します。ここでも、キューがいっぱいの場合にのみ、新しいスレッドが作成されます。


3

このブログの良い説明:

public class ThreadPoolExecutorExample {

    public static void main (String[] args) {
        createAndRunPoolForQueue(new ArrayBlockingQueue<Runnable>(3), "Bounded");
        createAndRunPoolForQueue(new LinkedBlockingDeque<>(), "Unbounded");
        createAndRunPoolForQueue(new SynchronousQueue<Runnable>(), "Direct hand-off");
    }

    private static void createAndRunPoolForQueue (BlockingQueue<Runnable> queue,
                                                                      String msg) {
        System.out.println("---- " + msg + " queue instance = " +
                                                  queue.getClass()+ " -------------");

        ThreadPoolExecutor e = new ThreadPoolExecutor(2, 5, Long.MAX_VALUE,
                                 TimeUnit.NANOSECONDS, queue);

        for (int i = 0; i < 10; i++) {
            try {
                e.execute(new Task());
            } catch (RejectedExecutionException ex) {
                System.out.println("Task rejected = " + (i + 1));
            }
            printStatus(i + 1, e);
        }

        e.shutdownNow();

        System.out.println("--------------------\n");
    }

    private static void printStatus (int taskSubmitted, ThreadPoolExecutor e) {
        StringBuilder s = new StringBuilder();
        s.append("poolSize = ")
         .append(e.getPoolSize())
         .append(", corePoolSize = ")
         .append(e.getCorePoolSize())
         .append(", queueSize = ")
         .append(e.getQueue()
                  .size())
         .append(", queueRemainingCapacity = ")
         .append(e.getQueue()
                  .remainingCapacity())
         .append(", maximumPoolSize = ")
         .append(e.getMaximumPoolSize())
         .append(", totalTasksSubmitted = ")
         .append(taskSubmitted);

        System.out.println(s.toString());
    }

    private static class Task implements Runnable {

        @Override
        public void run () {
            while (true) {
                try {
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {
                    break;
                }
            }
        }
    }
}

出力:

---- Bounded queue instance = class java.util.concurrent.ArrayBlockingQueue -------------
poolSize = 1, corePoolSize = 2, queueSize = 0, queueRemainingCapacity = 3, maximumPoolSize = 5, totalTasksSubmitted = 1
poolSize = 2, corePoolSize = 2, queueSize = 0, queueRemainingCapacity = 3, maximumPoolSize = 5, totalTasksSubmitted = 2
poolSize = 2, corePoolSize = 2, queueSize = 1, queueRemainingCapacity = 2, maximumPoolSize = 5, totalTasksSubmitted = 3
poolSize = 2, corePoolSize = 2, queueSize = 2, queueCapacity = 1, maximumPoolSize = 5, totalTasksSubmitted = 4
poolSize = 2, corePoolSize = 2, queueSize = 3, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 5
poolSize = 3, corePoolSize = 2, queueSize = 3, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 6
poolSize = 4, corePoolSize = 2, queueSize = 3, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 7
poolSize = 5, corePoolSize = 2, queueSize = 3, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 8
Task rejected = 9
poolSize = 5, corePoolSize = 2, queueSize = 3, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 9
Task rejected = 10
poolSize = 5, corePoolSize = 2, queueSize = 3, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 10
--------------------

---- Unbounded queue instance = class java.util.concurrent.LinkedBlockingDeque -------------
poolSize = 1, corePoolSize = 2, queueSize = 0, queueRemainingCapacity = 2147483647, maximumPoolSize = 5, totalTasksSubmitted = 1
poolSize = 2, corePoolSize = 2, queueSize = 0, queueRemainingCapacity = 2147483647, maximumPoolSize = 5, totalTasksSubmitted = 2
poolSize = 2, corePoolSize = 2, queueSize = 1, queueRemainingCapacity = 2147483646, maximumPoolSize = 5, totalTasksSubmitted = 3
poolSize = 2, corePoolSize = 2, queueSize = 2, queueRemainingCapacity = 2147483645, maximumPoolSize = 5, totalTasksSubmitted = 4
poolSize = 2, corePoolSize = 2, queueSize = 3, queueRemainingCapacity = 2147483644, maximumPoolSize = 5, totalTasksSubmitted = 5
poolSize = 2, corePoolSize = 2, queueSize = 4, queueRemainingCapacity = 2147483643, maximumPoolSize = 5, totalTasksSubmitted = 6
poolSize = 2, corePoolSize = 2, queueSize = 5, queueRemainingCapacity = 2147483642, maximumPoolSize = 5, totalTasksSubmitted = 7
poolSize = 2, corePoolSize = 2, queueSize = 6, queueRemainingCapacity = 2147483641, maximumPoolSize = 5, totalTasksSubmitted = 8
poolSize = 2, corePoolSize = 2, queueSize = 7, queueRemainingCapacity = 2147483640, maximumPoolSize = 5, totalTasksSubmitted = 9
poolSize = 2, corePoolSize = 2, queueSize = 8, queueRemainingCapacity = 2147483639, maximumPoolSize = 5, totalTasksSubmitted = 10
--------------------

---- Direct hand-off queue instance = class java.util.concurrent.SynchronousQueue -------------
poolSize = 1, corePoolSize = 2, queueSize = 0, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 1
poolSize = 2, corePoolSize = 2, queueSize = 0, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 2
poolSize = 3, corePoolSize = 2, queueSize = 0, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 3
poolSize = 4, corePoolSize = 2, queueSize = 0, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 4
poolSize = 5, corePoolSize = 2, queueSize = 0, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 5
Task rejected = 6
poolSize = 5, corePoolSize = 2, queueSize = 0, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 6
Task rejected = 7
poolSize = 5, corePoolSize = 2, queueSize = 0, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 7
Task rejected = 8
poolSize = 5, corePoolSize = 2, queueSize = 0, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 8
Task rejected = 9
poolSize = 5, corePoolSize = 2, queueSize = 0, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 9
Task rejected = 10
poolSize = 5, corePoolSize = 2, queueSize = 0, queueRemainingCapacity = 0, maximumPoolSize = 5, totalTasksSubmitted = 10
--------------------


Process finished with exit code 0

1

Java同時実行の基本的な本から:

CorePoolSize:ThreadPoolExecutorには属性corePoolSizeがあり、キューがいっぱいのときにのみ新しいスレッドが開始されるまでに開始するスレッドの数を決定します

MaximumPoolSize:この属性は、最大で開始されるスレッドの数を決定します。これをIntegerに設定できます。上限を持たないためのMAX_VALUE


0

java.util.concurrent.ThreadPoolExecutor

  public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

0

ThreadPoolExecutor新しいタスクが送信されたときの内部動作を理解することで、方法corePoolSizemaximumPoolSize違いを理解するのに役立ちました。

みましょう:

  • Nは、プール内のスレッドの数ですgetPoolSize()。アクティブスレッド+アイドルスレッド。
  • T エグゼキューター/プールに送信されたタスクの量になります。
  • CコアプールサイズであるgetCorePoolSize()新しいタスクがキューに移動する前に、着信タスクのプールごとに最大でいくつのスレッドを作成できるか。
  • M、最大プールサイズですgetMaximumPoolSize()。プールが割り当てることができるスレッドの最大量。

ThreadPoolExecutor新しいタスクが送信されたときのJava での動作:

  • の場合N <= C、アイドルスレッドには新しい着信タスクが割り当てられず、代わりに新しいスレッドが作成されます。
  • 以下のためN > Cとあれば、新しいタスクがそこに割り当てられているアイドル状態のスレッドがあります。
  • N > Cとアイドルスレッドを存在しない場合は、新しいタスクがキューに入れられます。ここに新しいスレッドは作成されません。
  • キューがいっぱいになると、最大で新しいスレッドが作成されMます。M到達した場合、タスクを拒否します。ここで重要でないのは、キューがいっぱいになるまで新しいスレッドを作成しないことです。

出典:

キュー容量がの場合corePoolSize = 0との例。maximumPoolSize = 1050

これにより、キューに50のアイテムが含まれるまで、プール内に1つのアクティブスレッドが生成されます。

executor.execute(task #1):

before task #1 submitted to executor: java.util.concurrent.ThreadPoolExecutor@c52dafe[Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]

after task #1 submitted to executor: java.util.concurrent.ThreadPoolExecutor@c52dafe[Running, pool size = 1, active threads = 1, queued tasks = 1, completed tasks = 0]

[task #1 immediately queued and kicked in b/c the very first thread is created when `workerCountOf(recheck) == 0`]

execute(task #2):

before task #2 submitted to executor: java.util.concurrent.ThreadPoolExecutor@c52dafe[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]

after task #2 submitted to executor: java.util.concurrent.ThreadPoolExecutor@c52dafe[Running, pool size = 1, active threads = 1, queued tasks = 1, completed tasks = 0]

[task #2 not starting before #1 is done]

... executed a few tasks...

execute(task #19)

before task #19 submitted to executor: java.util.concurrent.ThreadPoolExecutor@735afe38[Running, pool size = 1, active threads = 1, queued tasks = 17, completed tasks = 0]

after task #19 submitted to executor: java.util.concurrent.ThreadPoolExecutor@735afe38[Running, pool size = 1, active threads = 1, queued tasks = 18, completed tasks = 0]

...

execute(task #51)

before task submitted to executor: java.util.concurrent.ThreadPoolExecutor@735afe38[Running, pool size = 1, active threads = 1, queued tasks = 50, completed tasks = 0]

after task submitted to executor: java.util.concurrent.ThreadPoolExecutor@735afe38[Running, pool size = 2, active threads = 2, queued tasks = 50, completed tasks = 0]

Queue is full.
A new thread was created as the queue was full.

キュー容量がの場合corePoolSize = 10との例。maximumPoolSize = 1050

これにより、プール内でアクティブなスレッドが10個になります。キューに50のアイテムがある場合、タスクは拒否されます。

execute(task #1)

before task #1 submitted to executor: java.util.concurrent.ThreadPoolExecutor@32d9e072[Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]

after task #1 submitted to executor: java.util.concurrent.ThreadPoolExecutor@32d9e072[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]

execute(task #2)

before task #2 submitted to executor: java.util.concurrent.ThreadPoolExecutor@32d9e072[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]

after task #2 submitted to executor: java.util.concurrent.ThreadPoolExecutor@32d9e072[Running, pool size = 2, active threads = 2, queued tasks = 0, completed tasks = 0]

execute(task #3)

before task #3 submitted to executor: java.util.concurrent.ThreadPoolExecutor@32d9e072[Running, pool size = 2, active threads = 2, queued tasks = 0, completed tasks = 0]

after task #3 submitted to executor: java.util.concurrent.ThreadPoolExecutor@32d9e072[Running, pool size = 3, active threads = 3, queued tasks = 0, completed tasks = 0]

... executed a few tasks...

execute(task #11)

before task #11 submitted to executor: java.util.concurrent.ThreadPoolExecutor@32d9e072[Running, pool size = 10, active threads = 10, queued tasks = 0, completed tasks = 0]

after task #11 submitted to executor: java.util.concurrent.ThreadPoolExecutor@32d9e072[Running, pool size = 10, active threads = 10, queued tasks = 1, completed tasks = 0]

... executed a few tasks...

execute(task #51)
before task #51 submitted to executor: java.util.concurrent.ThreadPoolExecutor@32d9e072[Running, pool size = 10, active threads = 10, queued tasks = 50, completed tasks = 0]

Task was rejected as we have reached `maximumPoolSize`. 
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.