42

以下のコード (Java Concurrency in Practice リスト 16.3) は、明らかな理由でスレッド セーフではありません。

public class UnsafeLazyInitialization {
    private static Resource resource;

    public static Resource getInstance() {
        if (resource == null)
            resource = new Resource();  // unsafe publication
        return resource;
    }
}

ただし、数ページ後のセクション 16.3 では、次のように述べています。

UnsafeLazyInitializationが不変の場合、 実際には安全です。Resource

私はその声明を理解していません:

  • が不変の場合、変数をResource監視するすべてのスレッドは、resource変数が null または完全に構築されていることを認識します (Java メモリ モデルによって提供される final フィールドの強力な保証のおかげです)。
  • ただし、命令の並べ替えを妨げるものはありません。特に、 の 2 つの読み取りをresource並べ替えることができます ( に 1 つ、 に 1 つの読み取りがありますif) returnresourceそのため、スレッドは条件で非 null を確認できますifが、null 参照 (*) を返します。

不変UnsafeLazyInitialization.getInstance()でもnullを返すことができると思います。Resourceそれは事実であり、なぜ(またはなぜそうでないのか)?


(*) 並べ替えに関する私の主張をよりよく理解するために、同時実行性に関する JLS の第 17 章の著者の 1 人である Jeremy Manson によるこのブログ投稿では、String のハッシュコードが良性のデータ競合を介して安全に公開される方法と、ローカル変数を使用すると、ハッシュコードが誤って 0 を返す可能性があります。これは、上記で説明したのと非常によく似た並べ替えが発生する可能性があるためです。

ここで行ったことは、追加の読み取りを追加することです。つまり、リターンの前にハッシュの 2 回目の読み取りを行います。奇妙に聞こえるかもしれませんが、起こりそうもないことですが、最初の読み取りで正しく計算されたハッシュ値が返され、2 回目の読み取りで 0 が返されることがあります。これは、モデルが操作の広範な並べ替えを許可するため、メモリ モデルの下で許可されます。2 番目の読み取りは、コード内で実際に移動できるため、プロセッサは最初の読み取りよりも先に読み取ります。

4

10 に答える 10

3

更新2月10日

コンパイル実行の2つのフェーズを分離する必要があると確信しています。

nullを返すことができるかどうかの決定要因は、バイトコードが何であるかだと思います。私は3つの例を作りました:

例1:

文字通りバイトコードに変換された元のソースコード:

if (resource == null)
    resource = new Resource();  // unsafe publication
return resource;

バイトコード:

public static Resource getInstance();
Code:
0:   getstatic       #20; //Field resource:LResource;
3:   ifnonnull       16
6:   new             #22; //class Resource
9:   dup
10:  invokespecial   #24; //Method Resource."<init>":()V
13:  putstatic       #20; //Field resource:LResource;
16:  getstatic       #20; //Field resource:LResource;
19:  areturn

これは最も興味深いケースです。2read秒(Line#0とLine#16)があり、その間に1つwrite(Line#13)があるからです。再注文はできないとのことですが、以下で調べてみましょう。

例2

次のように文字通りJavaに再変換できる「コンパイラ最適化」コード。

Resource read = resource;
if (resource==null)
    read = resource = new Resource();
return read;

そのためのバイトコード(実際には、上記のコードスニペットをコンパイルしてこれを作成しました):

public static Resource getInstance();
Code:
0:   getstatic       #20; //Field resource:LResource;
3:   astore_0
4:   getstatic       #20; //Field resource:LResource;
7:   ifnonnull       22
10:  new     #22; //class Resource
13:  dup
14:  invokespecial   #24; //Method Resource."<init>":()V
17:  dup
18:  putstatic       #20; //Field resource:LResource;
21:  astore_0
22:  aload_0
23:  areturn

コンパイラが「最適化」し、上記のようなバイトコードが生成された場合、null読み取りが発生する可能性があることは明らかです(たとえば、Jeremy Mansonのブログを参照してください) 。

a = b = c新しいインスタンスへの参照(Line#14)が複製され(Line#17)、同じ参照が最初にb(resource、(Line#18))に格納され、次にa(読んでください、(行#21))。

例3

さらに少し変更を加えましょうresource。一度だけ読んでください。コンパイラが最適化を開始した場合(および他の人が述べたようにレジスタを使用した場合)、これは上記よりも優れた最適化です。これは、例2のより高価な「静的アクセス」ではなく「レジスタアクセス」であるためです。

Resource read = resource;
if (read == null)   // reading the local variable, not the static field
    read = resource = new Resource();
return read;

例3のバイトコード(上記を文字通りコンパイルして作成されたもの):

public static Resource getInstance();
Code:
0:   getstatic       #20; //Field resource:LResource;
3:   astore_0
4:   aload_0
5:   ifnonnull       20
8:   new     #22; //class Resource
11:  dup
12:  invokespecial   #24; //Method Resource."<init>":()V
15:  dup
16:  putstatic       #20; //Field resource:LResource;
19:  astore_0
20:  aload_0
21:  areturn

また、このバイトコードはと同じように構築されており、の静的変数の読み取りが1つしかないため、このバイトコードからnullを取得できないことも簡単にわかります。String.hashcode()resource

次に、例1を調べてみましょう。

0:   getstatic       #20; //Field resource:LResource;
3:   ifnonnull       16
6:   new             #22; //class Resource
9:   dup
10:  invokespecial   #24; //Method Resource."<init>":()V
13:  putstatic       #20; //Field resource:LResource;
16:  getstatic       #20; //Field resource:LResource;
19:  areturn

Line#16(variable#20return for return)はLine#13からの書き込み(コンストラクターからの割り当て)を最もよく観察していることがわかります。したがって、Line#13が実行される実行順序でvariable#20それを先に置くことは違法です。したがって、並べ替えはできません

JVMの場合、(特定の追加条件を使用して)Line#13書き込みをバイパスするブランチを構築(および利用)することができます。条件は、読み取り元variable#20 がnullであってはならないということです。

したがって、どちらの場合も、例1ではnullを返すことはできません。

結論:

上記の例を見ると、例1にあるバイトコードは生成されませんnull例2 のような最適化されたバイトコードは生成されますが、生成されnullないさらに優れた最適化の例3があります。null

すべてのコンパイラの可能なすべての最適化に備えることはできないため、可能な場合と不可能な場合return nullがあり、すべてバイトコードに依存していると言えます。また、両方の場合に少なくとも1つの例があることを示しました。


古い推論:Assyliasの例を参照してください:主な質問は次のとおりです:VMが11と14の読み取りを並べ替えて、14が11より前に発生することは有効ですか(すべての仕様、JMM、JLSに関して)?

それが発生する可能性がある場合、独立者Thread2は23でリソースを書き込むことができるため、14はを読み取ることができますnull。私はそれが不可能であると述べています。

実際には、13の書き込みが可能であるため、有効な実行順序ではありません。VMは実行順序を最適化して、実行されていないブランチ(2回の読み取りのみ、書き込みなし)を除外する場合がありますが、この決定を行うには、最初の読み取り(11)を実行し、null以外の読み取りを行う必要があります。したがって、14の読み取りを11の読み取りの前に行うことはできません。したがって、を返すことはできませんnull


不変性

不変性に関しては、このステートメントは正しくないと思います。

UnsafeLazyInitializationは、リソースが不変である場合、実際には安全です。

ただし、コンストラクターが予測できない場合は、興味深い結果が得られる可能性があります。次のようなコンストラクターを想像してみてください。

public class Resource {
    public final double foo;

    public Resource() {
        this.foo = Math.random();
    }
}

これがある場合Thread、2つのスレッドが異なる動作のオブジェクトを受け取る結果になる可能性があります。したがって、完全なステートメントは次のように聞こえるはずです。

UnsafeLazyInitializationは、Resourceが不変であり、その初期化が一貫している場合、実際には安全です。

一貫性があるということは、2回のコンストラクターを呼び出すと、まったく同じように動作する2つのオブジェクトを受け取ることを意味しますResource(両方で同じメソッドを同じ順序で呼び出すと、同じ結果が得られます)。

于 2013-01-31T15:36:53.963 に答える
3

ここであなたが持っていると思う混乱は、著者が安全な出版物によって意味したものです. 彼は null 以外のリソースの安全な公開について言及していましたが、あなたはそれを理解しているようです。

あなたの質問は興味深いです - リソースの null キャッシュ値を返すことは可能ですか?

はい。

コンパイラは、そのような操作を並べ替えることができます

public static Resource getInstance(){
   Resource reordered = resource;
   if(resource != null){
       return reordered;
   }
   return (resource = new Resource());
} 

これは順次整合性のルールに違反しませんが、null 値を返す可能性があります。

これが最良の実装であるかどうかは議論の余地がありますが、この種の並べ替えを防止するルールはありません。

于 2013-01-31T16:25:10.750 に答える
3

getInstanceこの例に JLS ルールを適用した後、確実に を返すことができるという結論に達しましたnull。特に、JLS 17.4 :

メモリ モデルは、プログラムの各ポイントで読み取ることができる値を決定します。分離された各スレッドのアクションは、そのスレッドのセマンティクスによって管理されるように動作する必要があります。ただし、各読み取りで表示される値はメモリ モデルによって決定されます

2 つの読み取りのそれぞれが何かを観察できるため、同期が存在しない場合null、メソッドの正当な結果であることは明らかです。


証拠

読み取りと書き込みの分解

プログラムは次のように分解できます (読み取りと書き込みを明確に確認するため)。

                              Some Thread
---------------------------------------------------------------------
 10: resource = null; //default value                                  //write
=====================================================================
           Thread 1               |          Thread 2                
----------------------------------+----------------------------------
 11: a = resource;                | 21: x = resource;                  //read
 12: if (a == null)               | 22: if (x == null)               
 13:   resource = new Resource(); | 23:   resource = new Resource();   //write
 14: b = resource;                | 24: y = resource;                  //read
 15: return b;                    | 25: return y;                    

JLS の説明

JLS 17.4.5は、読み取りが書き込みを観察できるようにするための規則を示しています。

変数 v の読み取り r が v への書き込み w を観察できると言います。

  • r は w の前に順序付けされていません (つまり、hb(r, w) の場合ではありません)。
  • v への書き込み w' は介在しません (つまり、hb(w, w') および hb(w', r) のような w' から v への書き込みはありません)。

規則の適用

この例では、スレッド 1 が null を認識し、適切に初期化すると仮定しますresource。スレッド 2 では、無効な実行は 21 が 23 を観察することです (プログラムの順序による) - しかし、他の書き込み (10 と 13) はいずれかの読み取りで観察できます。

  • 10 はすべてのアクションの前に発生するため、読み取りは 10 より前に命令されません
  • 21 と 24 は 13 と hb 関係がありません
  • 13 は 23 の前に発生しません (両者の間に hb 関係はありません)

したがって、21 と 24 (2 回の読み取り) の両方で、10 (null) または 13 (not null) のいずれかを観察できます。

null を返す実行パス

特に、スレッド 1 が 11 行目でヌルを検出し、resource13 行目で初期化すると仮定すると、スレッド 2 は次のように合法的に実行できます。

  • 24: y = null(読み書き 10)
  • 21: x = non null(読み書き 13)
  • 22: false
  • 25: return y

注: 明確にするために、これは T2 が非 null を認識し、その後 null を認識する(因果関係の要件に違反する) という意味ではありません。つまり、実行の観点からすると、2 つの読み取りが並べ替えられ、2 つ目の読み取りが最初の読み取りの前にコミットされたことを意味します。 1 つ - ただし、最初のプログラムの順序に基づいて、後の書き込みが前の書き込みの前に見られたように見えます。

2月10日更新

コードに戻ると、有効な並べ替えは次のようになります。

Resource tmp = resource; // null here
if (resource != null) { // resource not null here
    resource = tmp = new Resource();
}
return tmp; // returns null

また、そのコードは逐次一貫性があるため (単一のスレッドで実行された場合、常に元のコードと同じ動作をします)、因果関係の要件が満たされていることを示しています (結果を生成する有効な実行があります)。


同時実行関心リストに投稿した後、その並べ替えの合法性に関するいくつかのメッセージを受け取りました。これは、それnullが合法的な結果であることを確認しています。

  • シングルスレッドの実行では違いがわからないため、変換は間違いなく合法です。[注意してください] 変換は賢明ではないようです - コンパイラがそれを行う正当な理由はありません。ただし、周囲のコードが大量にある場合や、コンパイラの最適化の「バグ」が原因である場合は、この問題が発生する可能性があります。
  • スレッド内の順序とプログラムの順序に関する記述は、物事の妥当性に疑問を抱かせましたが、最終的には、JMM は実行されるバイトコードに関連しています。変換は javac コンパイラーによって行うことができ、その場合 null は完全に有効になります。また、javac が Java ソースから Java バイトコードに変換する方法についてのルールはありません...
于 2013-02-01T18:25:17.863 に答える
2

あなたが尋ねている本質的に2つの質問があります:

1.並べ替えによってgetInstance()メソッドが戻るnullことはありますか?

(これはあなたが本当に求めているものだと思うので、最初に答えようとします)

これを許可するようにJavaを設計することは完全に狂っていると思いますgetInstance()、 nullを返すことができるのは実際には正しいようです。

あなたのコード例:

if (resource == null)
    resource = new Resource();  // unsafe publication
return resource;

リンク先のブログ投稿の例と論理的に 100% 同一です。

if (hash == 0) {
    // calculate local variable h to be non-zero
    hash = h;
}
return hash;

Jeremy Manson は、並べ替えによってコードが 0 を返す可能性があると説明しています。最初は、次の「前に起こる」ロジックが成り立つはずだと思っていたので、信じられませんでした。

   "if (resource == null)" happens before "resource = new Resource();"
                                   and
     "resource = new Resource();" happens before "return resource;"
                                therefore
"if (resource == null)" happens before "return resource;", preventing null

しかし、Jeremy は彼のブログ投稿へのコメントで次の例を示しています。このコードがコンパイラによって有効に書き換えられる方法:

read = resource;
if (resource==null)
    read = resource = new Resource();
return read;

これは、シングルスレッド環境では元のコードとまったく同じように動作しますが、マルチスレッド環境では次の実行順序になる可能性があります。

Thread 1                        Thread 2
------------------------------- -------------------------------------------------
read = resource;    // null
                                read = resource;                      // null
                                if (resource==null)                   // true
                                    read = resource = new Resource(); // non-null
                                return read;                          // non-null
if (resource==null) // FALSE!!!
return read;        // NULL!!!

さて、最適化の観点から、これを行うことは私には意味がありません.代わりに生成if (read==null)して、問題を防ぎます。したがって、Jeremy が彼のブログで指摘しているように、おそらくこれが起こる可能性はほとんどありません。しかし、純粋に言語規則の観点から見ると、実際には許可されているようです。

この例は、実際には JLS でカバーされています。

http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4

r2r4、およびr5inの値の間で観察される効果は、上記の例の、、および でTable 17.4. Surprising results caused by forward substitution発生する可能性があるものと同等です。read = resourceif (resource==null)return resource

余談ですが、回答の最終的な情報源としてブログ投稿を参照するのはなぜですか? それを書いた人は、並行性に関する JLS の第 17 章を書いた人でもあるからです! だから、彼は正しいほうがいいです!:)

2.Resource不変にすると、getInstance()メソッドはスレッドセーフになりますか?

ミュータブルかどうかに関係なく発生する可能性のある潜在的なnull結果を考えると、Resourceこの質問に対する即時の簡単な答えは次のとおりです。いいえ(厳密ではありません)

ただし、この非常にありそうもないが可能性のあるシナリオを無視すると、答えは次のようになります

コードの明らかなスレッドの問題は、次の実行順序につながる可能性があることです (並べ替えの必要はありません)。

Thread 1                                 Thread 2
---------------------------------------- ----------------------------------------
if (resource==null) // true;  
                                         if (resource==null)          // true
                                             resource=new Resource(); // object 1
                                         return resource;             // object 1
    resource=new Resource(); // object 2
return resource;             // object 2

したがって、非スレッド セーフは、関数から 2 つの異なるオブジェクトが返される可能性があるという事実から来ています (ただし、順序を変更しなければどちらもnull.

さて、この本がおそらく言おうとしていたことは、次のことです。

String や Integers などの Java 不変オブジェクトは、同じコンテンツに対して複数のオブジェクトを作成しないようにします。したがって、"hello"ある場所と"hello"別の場所にある場合、Java はまったく同じオブジェクト参照を提供します。同様に、new Integer(5)ある場所とnew Integer(5)別の場所にある場合。これがnew Resource()同様に当てはまる場合、同じ参照が返されobject 1object 2上記の例ではまったく同じオブジェクトになります。これは実際、効果的にスレッドセーフな関数につながります (並べ替えの問題は無視されます)。

しかし、Resource自分で実装する場合、コンストラクターが新しいオブジェクトを作成するのではなく、以前に作成されたオブジェクトへの参照を返すようにする方法さえないと思います。object 1したがって、object 2まったく同じオブジェクトを作成することはできません。しかし、同じ引数 (どちらの場合もなし) でコンストラクターを呼び出していることを考えると、作成されたオブジェクトがまったく同じオブジェクトでなくても、すべての意図と目的のために、次のように動作する可能性があります。もしそうなら、コードを効果的にスレッドセーフにすることもできます。

ただし、必ずしもそうである必要はありません。Dateたとえば、の不変バージョンを想像してみてください。デフォルトのコンストラクターDate()は、現在のシステム時刻を日付の値として使用します。したがって、オブジェクトが不変であり、コンストラクターが同じ引数で呼び出されたとしても、それを 2 回呼び出しても、おそらく同等のオブジェクトにはなりません。したがって、このgetInstance()メソッドはスレッドセーフではありません。

したがって、一般的な声明として、あなたが本から引用した行はまったく間違っていると思います(少なくともここでは文脈から切り離されているため)。

ADDITION Re: 並べ替え

このresource==new Resource()例は少し単純すぎて、Java によるこのような並べ替えを許可することが理にかなっている理由を理解するのに役立ちません。それでは、これが実際に最適化に役立つ何かを考え出すことができるかどうか見てみましょう:

System.out.println("Found contact:");
System.out.println(firstname + " " + lastname);
if (firstname==null) firstname = "";
if (lastname ==null) lastname  = "";
return firstname + " " + lastname;

ifsここで、両方がyieldになる可能性が最も高いケースでfalseは、高価な String 連結firstname + " " + lastnameを 2 回 (デバッグ メッセージ用に 1 回、リターン用に 1 回) 行うのは最適ではありません。したがって、代わりに次のことを行うようにコードを並べ替えることは、ここでは理にかなっています。

System.out.println("Found contact:");
String contact = firstname + " " + lastname;
System.out.println(contact);
if ((firstname==null) || (lastname==null)) {
    if (firstname==null) firstname = "";
    if (lastname ==null) lastname  = "";
    contact = firstname + " " + lastname;
}
return contact;

例がより複雑になり、コンパイラーが使用するプロセッサーレジスターに既にロード/計算されているものを追跡し、既存の結果の再計算をインテリジェントにスキップすることについて考え始めると、この効果は実際にはますます可能性が高くなります。起こる。ですから、昨夜寝たときにこれを言うとは思っていませんでしたが、もっと考えてみると、コードの最適化が最大限に機能するようにするために、これは必要な/良い決定だったのではないかと実際に信じています。印象的な魔法。しかし、多くの人がこれに気づいていないと思うので、それはまだ非常に危険だと思います。

この並べ替えを許可しないと、一連のプロセス ステップの中間結果のキャッシュと再利用が違法になり、可能な限り最も強力なコンパイラ最適化の 1 つがなくなります。

于 2013-02-10T04:30:54.633 に答える
0

null非の場合、参照を設定するものはありませんnullnull別のスレッドがスレッドを非に設定した後、スレッドがそれを確認nullすることは可能ですが、その逆がどのように可能かはわかりません。

ここでは、命令の並べ替えが要因かどうかはわかりませんが、2つのスレッドによる命令のインターリーブは重要です。ブランチの状態が評価される前に、ifブランチを並べ替えて実行することはできません。

于 2013-01-31T11:19:43.037 に答える
0

間違っていたら申し訳ありませんが (私は英語を母国語としないため)、次のように思われます。

Resource が不変の場合、UnsafeLazyInitialization は実際には安全です。

文脈から切り離されています。このステートメントは、初期化の安全性を使用することに関するものです。

初期化の安全性の保証により、適切に構築された不変オブジェクトを同期なしでスレッド間で安全に共有できます

...

初期化の安全性により、適切に構築されたオブジェクトの場合、コンストラクターによって設定された最終フィールドの正しい値がすべてのスレッドに表示されることが保証されます。

于 2013-01-31T11:47:20.877 に答える
-1

確かに安全ですUnsafeLazyInitialization.resource。不変です。つまり、フィールドはfinalとして宣言されます。

private static final Resource resource = new Resource();

Resourceクラス自体が不変であり、使用しているインスタンスに関係がない場合は、スレッドセーフと見なされることもあります。その場合、2つの呼び出しは、同時にResource呼び出すスレッドの数に応じてメモリ消費量が増加することを除けば、問題なくの異なるインスタンスを返す可能性があります。getInstance()

しかし、それはとてつもないようで、タイプミスがあると思います。実際の文は

UnsafeLazyInitializationは、* r *esourceが不変である場合に実際に安全です。

于 2013-01-31T11:25:53.040 に答える
-1

UnsafeLazyInitialization.getInstance() null を返すことはできません

@assylias のテーブルを使用します。

                              Some Thread
---------------------------------------------------------------------
 10: resource = null; //default value                                  //write
=====================================================================
           Thread 1               |          Thread 2                
----------------------------------+----------------------------------
 11: a = resource;                | 21: x = resource;                  //read
 12: if (a == null)               | 22: if (x == null)               
 13:   resource = new Resource(); | 23:   resource = new Resource();   //write
 14: b = resource;                | 24: y = resource;                  //read
 15: return b;                    | 25: return y;    

スレッド 1 の行番号を使用します。スレッド 1 は、11 での読み取りの前に 10 での書き込みを確認し、14 での読み取りの前に 11 行での読み取りを確認します。これらはスレッド内発生前の関係であり、スレッド 2 に関するすべて。14 行目の読み取りは、JMM によって定義された値を返します。タイミングによっては、13 行目で作成されたリソースである場合もあれば、スレッド 2 によって書き込まれた任意の値である場合もあります。ただし、その書き込みは、11 行目の読み取りの後に発生する必要があります。 23 行目。10 行目の null への書き込みは、スレッドの順序付けにより 11 行目の前に発生したため、範囲外です。

Resource不変かどうかは関係ありません。これまでの議論のほとんどは、不変性が関連するスレッド間アクションに焦点を当ててきましたが、このメソッドが null を返すことを可能にする並べ替えは、スレッドルールによって禁止されています。仕様の関連セクションはJLS 17.4.7です。

各スレッド t に対して、A 内の t によって実行されるアクションは、そのスレッドによって分離されたプログラム順で生成されるアクションと同じであり、各書き込み w は値 V(w) を書き込みます。各読み取り r が値 V を見る場合(W(r))。各読み取りで表示される値は、メモリ モデルによって決まります。指定されたプログラムの順序は、P のスレッド内セマンティクスに従ってアクションが実行されるプログラムの順序を反映する必要があります。

これは基本的に、読み取りと書き込みが並べ替えられる可能性がある一方で、読み取りと書き込みを実行するスレッドに対して、同じ変数への読み取りと書き込みが発生したように見える必要があることを意味します。

null の書き込みは 1 回だけです (10 行目)。どちらのスレッドも、リソースの独自のコピーまたは他のスレッドのコピーを見ることができますが、いずれかのリソースを読み取った後、以前の null への書き込みを確認することはできません。

ちなみに、null への初期化は別のスレッドで行われます。JCIP の安全な出版物に関するセクションには、次のように記載されています。

静的初期化子は、クラスの初期化時に JVM によって実行されます。JVM の内部同期のため、このメカニズムは、この方法で初期化されたすべてのオブジェクトを安全に公開することが保証されています [JLS 12.4.2]

UnsafeLazyInitialization.getInstance()null を返すようになり、null を返すように提案された同等の書き換えのいくつかを取得するテストを作成してみる価値があるかもしれません。それらが真に同等ではないことがわかります。

編集

わかりやすくするために、読み取りと書き込みを分離した例を次に示します。public static 変数オブジェクトがあるとしましょう。

public static Object object = new Integer(0);

スレッド 1 はそのオブジェクトに次のように書き込みます。

object = new Integer(1);
object = new Integer(2);
object = new Integer(3);

スレッド 2 はそのオブジェクトを読み取ります。

System.out.println(object);
System.out.println(object);
System.out.println(object);

スレッド間発生前の関係を提供する同期の形式がなければ、スレッド 2 はさまざまなものを出力できます。

1, 2, 3
0, 0, 0
3, 3, 3
1, 1, 3
etc.

しかし、3、2、1 のような減少するシーケンスを出力することはできません。3 回使用する代わりに、object3 つの個別の静的変数を使用するように例を変更すると、並べ替えに関する制限がないため、より多くの出力が可能になります。

于 2013-02-05T17:05:36.077 に答える