ホームページ  >  記事  >  Java  >  スレッドのステータス、スレッドの安全性の問題、Java での synchronized キーワードの使用

スレッドのステータス、スレッドの安全性の問題、Java での synchronized キーワードの使用

WBOY
WBOY転載
2023-05-07 14:46:081146ブラウズ

    Java のスレッド ステータス

    オペレーティング システム レベルでは、スレッドには準備完了とブロックという 2 つの状態があります。

    ただし、順序は次のとおりです。スレッドがブロックされているときにスレッドがブロックされる理由をすぐに知るために、Java はブロック ステータスをさらに改良しました。

    スレッドのステータス、スレッドの安全性の問題、Java での synchronized キーワードの使用

    • ##NEW : スレッド オブジェクトには次のような特徴があります。作成されましたが、システム レベルのスレッドが作成されていないか、スレッド オブジェクトが start()を呼び出していません。

    • TERMINATED: システム内のスレッドは破棄されましたが、スレッドはコード内のオブジェクトはまだ存在します。つまり、run() の実行が終了した後も、Thread オブジェクトはまだ存在します。

    • RUNNABLE: スレッドは準備完了キューにあり、スケジュールされる可能性があります。いつでも CPU によって実行できるようにします

    • TIMED_WAITING: スレッドの実行中、スレッド オブジェクトは sleep() を呼び出し、ブロッキングに入ります。スリープ時間が経過すると、準備完了状態に戻ります。 queue

    • BLOCKED : 1 つのスレッドがオブジェクトをロック (同期) し、別のスレッドもそのオブジェクトをロックしようとすると、BLOCKED 状態になります。最初のスレッドがロックを解除した場合のみ、オブジェクトをロックすると、後者のスレッドがオブジェクトをロックできる可能性があります。オブジェクトはロックされています。

    • WAITING: wait() を同期して使用します。スレッドが wait() を呼び出すと、オブジェクトのロックが解除されます最初に別のスレッドが Notice() を実行するまで待機します), その後、待機中のスレッドが起動されます. もちろん、デッド待機を防ぐために wait() に最大待機時間を設定することもできます。

      スレッド セーフティ問題のケース分析

      ##同じ変数に書き込む複数のスレッド

    概念: コード文字列にスレッド セーフティの問題が発生するのはどのような場合ですか? まず第一に、 , スレッド セーフティの問題の悪の原因は、複数のスレッドが同時に実行されると、プリエンプティブ実行という現象が発生します。ここでのプリエンプティブ実行は、機械語命令を実行します! コード文字列にスレッド セーフティの問題が発生するのはいつですか? 複数のスレッドが実行されるときスレッドの数に関係なく、スレッドがどのようにコードをプリエンプティブ実行しても、最終的な結果には影響しません (これをスレッド セーフと呼びます)。スレッド セーフティの問題とバグ!

    1. 典型的なケース: 2 つのスレッドを使用して、同じ数値に対して自動インクリメント操作を 100,000 回実行します:

    2. public class Demo1 {
          private static int count=0;
          public static void main(String[] args) {
              Thread t1=new Thread(()->{
                  for(int i=0;i<50000;i++){
                      count++;
                  }
              });
              t1.start();
              Thread t2=new Thread(()->{
              t2.start();
              try {
                  t1.join();
                  t2.join();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(count);
          }
      }
      //打印结果:68994
    3. 明らかに予想される結果は 100,000 ですが、計算では 6w を超えることが判明し、これはスレッドの安全性の問題です。

      #理由の分析
    4. :

    各スレッドのヒープ カウントの自動インクリメント操作のみを実行します。まず、自動インクリメント機械命令を実行するには 3 つのステップがあることを理解する必要があります。メイン メモリから CPU レジスタにカウント値を取得 -> インクリメントレジスタ内のカウント値を 1 ずつ更新する -> レジスタ内のカウント値をメイン メモリに更新する、これら 3 つのステップを呼び出しましょう:load->add->save

    2 つの命令セットがあると仮定します。 1 つの CPU で同時に実行されます (2 つの CPU を描画して表します) (同時にロードするような状況は発生しません):

    上の図が表示された場合:

    スレッドのステータス、スレッドの安全性の問題、Java での synchronized キーワードの使用

    検出結果: 2 実行された各スレッドのカウントは 1 回ですが、結果が 2 回満足のいくものではありませんでした。これは、自動インクリメントが 1 回だけであることに相当します。上記はスレッドの安全性の問題です。 .

    そして、上記のコード範囲: 5w-10w の結果を予測できます!、なぜですか?

    スレッドのステータス、スレッドの安全性の問題、Java での synchronized キーワードの使用上の 2 つの図は、スレッド セーフティの問題が発生する状況を示しています。 2 つの加算を 1 つとして使用します 2 つのスレッドの場合 この状態 (これも最悪の状態) になっていますが、計算結果は 5w です。すると、2 つのスレッドが 1 つのスレッドで完全にロード-追加-保存を実行した場合、もう一方のスレッドはこのような操作を実行すると、シリアルに実行されますが、10w ではありません。

    3. 上記のケースを解決するにはどうすればよいですか?

    このケースも、最後に記載されています。シリアル実行を実現でき、結果の正確性を保証できます。Java には、そのような機能、つまり synchronized キーワードを使用できる機能があります。 #つまり、cpu1 が load を実行する前に、まずロック オブジェクトをロックし、保存後にロックを解除します。このときのみ、cpu2 はオブジェクトをロックして一連の操作を実行できます。このとき、load-add-save のアトミック性は次のようになります。

    次に、これと 1 つのメインスレッドだけを使用して自己計算を行うこととの違いは何なのかと尋ねるかもしれません。 -increment 100,000 回? 複数のスレッドを作成する方法 スレッドの意味は何ですか?

    意义很大,因为我们创建的线程很多时候不仅仅只是一个操作,光针对自增我们可以通过加锁防止出现线程安全问题,但是各线程的其他操作要是不涉及线程安全问题那就可以并发了呀,那此时不就大大提升了执行效率咯.

    4.具体如何加锁呢?

    此处先只说一种加锁方式,先把上述案例的问题给解决了再说.

    使用关键字synchronized,此处使用的是给普通方法加synchronized修饰的方法(除此之外,synchronized还可以修饰代码块和静态方法)

    class Counter{
        private int count;
        synchronized public void increase(){
            this.count++;
        }
        public int getCount(){
            return this.count;
        }
    }
    public class Demo2 {
        private static int num=50000;
        public static void main(String[] args) {
            Counter counter=new Counter();//此时对象中的count值默认就是0
            Thread t1=new Thread(()->{
                for (int i = 0; i < num; i++) {
                    counter.increase();
                }
            });
            t1.start();
    
            Thread t2=new Thread(()->{
                for (int i = 0; i < num; i++) {
                    counter.increase();
                }
            });
            t2.start();
    
            try {
                t1.join();
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println(counter.getCount());
        }
    }//打印10W

    内存可见性问题

    首先说明:这是有编译器优化导致的,其次要知道cpu读取变量时:先从主内存将变量的值存至缓存或者寄存器中,cpu计算时再在寄存器中读取这个值.

    当某线程频繁的从内存中读取一个不变的变量时,编译器将会把从内存获取变量的值直接优化成从寄存器直接获取.之所以这样优化,是因为,cpu从主内存中读取一个变量比在缓存或者寄存器中读取一个变量的值慢成千上万倍,如果每每在内存中读到的都是同一个值,既然缓存里头已经有这个值了,干嘛还大费周折再去主内存中进行获取呢,直接从缓存中直接读取就可以了,可提升效率.

    但是:一旦一个线程被优化成上述的情况,那如果有另一个线程把内存中的值修改了,我被优化的线程还傻乎乎的手里拿着修改之前的值呢,或者内存中的变量值被修改了,被优化的线程此时已经感应不到了.

    具体而言:

    public class Demo3 {
        private static boolean flag=false;
        public static void main(String[] args) {
            Thread t1=new Thread(()->{
                while(!flag){
                    System.out.println("我是优化完之后直接读取寄存器中的变量值才打印的哦!");
                }
            });
            t1.start();
    
            flag=true;
            System.out.println("我已经在主线程中修改了标志位");
        }
    }

    运行上述代码之后,程序并不会终止,而是一直在那打印t1线程中的打印语句.

    如何解决上述问题:

    引入关键字volatile:防止内存可见性问题,修饰一个变量,那某线程想获取该变量的值的时候,只能去主内存中获取,其次它还可以防止指令重排序,指令重排问题会在线程安全的单例模式(懒汉)进行介绍.具体:

    public class Demo3 {
        private static volatile boolean flag=false;
        public static void main(String[] args) {
            Thread t1=new Thread(()->{
                while(!flag){
                    System.out.println("我是优化完之后直接读取寄存器中的变量值才打印的哦!");
                }
            });
            t1.start();
    
            try {
                Thread.sleep(1);//主线程给t1留有充足的时间先跑起来
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag=true;
            System.out.println("我已经在主线程中修改了标志位");
        }
    }
    //打印若干t1中的打印语句之后,主线程main中修改标志位之后,可以终止t1

    注意:上述优化现象只会出现在频繁读的情况,如果不是频繁读,就不会出现那样的优化.

    指令重排序问题

    生活案例:买菜

    スレッドのステータス、スレッドの安全性の問題、Java での synchronized キーワードの使用

    如果是傻乎乎的按照菜单从上到下的去买菜,从路线图可以看出,不必要的路是真的没少走.

    如果执行代码时,编译器认为某些个代码调整一下顺序并不会影响结果,那代码的执行顺序就会被调整,就比如可以把上面买菜的顺序调整成:黄瓜->萝卜->青菜->茄子

    单线程这样的指令重排一般不会出现问题,但是多线程并发时,还这样优化,就容易出现问题

    针对这样的问题,如果是针对一个变量,我们可以使用volatile修饰,如果是针对代码块,我们可以使用synchronized.

    synchronized的用法

    • synchronized起作用的本质

    • 修饰普通方法

    • 修饰静态方法

    • 修饰代码块

    synchronized起作用的本质

    因为我们知道java中所有类都继承了Object,所以所有类都包含了Object的部分,我们可以称这继承的部分是"对象头",使用synchronized进行对象头中的标志位的修改,就可以做到一个对象的锁一个时刻只能被一个线程所持有,其他线程此时不可抢占.这样的设置,就好像把一个对象给锁住了一样.

    修饰普通方法

    如前述两个线程给同一个count进行自增的案例.不再赘述.此时的所对象就是Counter对象

    修饰静态方法⚡️

    与普通方法类似.只不过这个方法可以类名直接调用.

    修饰代码块

    首先修饰代码块需要执行锁对象是谁,所以这里可以分为三类,一个是修饰普通方法的方法体这个代码块的写法,其次是修饰静态方法方法体的写法,最后可以单独写一个Object的对象,来对这个Object对象进行上锁.

    class Counter{
        private int count;
        public void increase(){
            synchronized(this){
                count++;
            }
        }
        public int getCount(){
            return this.count;
        }
    }
    class Counter{
        private static int count;
        public static void increase(){
            synchronized(Counter.class){//注意这里锁的是类对象哦
                count++;
            }
        }
        public int getCount(){
            return this.count;
        }
    }
    class Counter{
        private static int count;
        private static Object locker=new Object();
        public static void increase(){
            synchronized(locker){
                count++;
            }
        }
        public int getCount(){
            return this.count;
        }
    }

    注意:java中这种随手拿一个对象就能上锁的用法,是java中一种很有特色的用法,在别的语言中,都是有专门的锁对象的.

    Conclusion

    java中的线程状态,以及如何区分线程安全问题 罪恶之源是抢占式执行多线程对同一个变量进行修改,多线程只读一个变量是没有线程安全问题的修改操作是非原子性的内存可见性引起的线程安全问题指令重排序引起的线程安全问题 synchronized的本质和用法

    1.java中的线程状态,以及如何区分
    2.线程安全问题

    • 罪恶之源是抢占式执行

    • 複数のスレッドが同じ変数を変更する場合、複数のスレッドが 1 つの変数を読み取るだけであれば、スレッド セーフティの問題はありません

    • 変更操作は非アトミックです

    • メモリの可視性によって引き起こされるスレッドの安全性の問題

    • 命令の並べ替えによって引き起こされるスレッドの安全性の問題

    3 .synchronized

    の本質と使い方

    以上がスレッドのステータス、スレッドの安全性の問題、Java での synchronized キーワードの使用の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

    声明:
    この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。