Heim > Java > javaLernprogramm > Hauptteil

Thread-Status, Thread-Sicherheitsprobleme und die Verwendung des synchronisierten Schlüsselworts in Java

WBOY
Freigeben: 2023-05-07 14:46:08
nach vorne
1152 Leute haben es durchsucht

    Thread-Status in Java

    Auf Betriebssystemebene hat ein Thread zwei Zustände: bereit und blockiert

    Aber in Java, um schnell den Grund zu erkennen, warum ein Thread blockiert ist , Der Blockierungsstatus wurde weiter verfeinert

    Thread-Status, Thread-Sicherheitsprobleme und die Verwendung des synchronisierten Schlüsselworts in Java

    • NEU: Das Thread-Objekt wurde erstellt, aber der Thread auf Systemebene wurde nicht erstellt, oder das Thread-Objekt wurde nicht start() aufgerufen

    • BEENDET: Der Thread im System wurde zerstört, aber das Thread-Objekt im Code ist immer noch vorhanden. Das heißt, nachdem die Ausführung von run() beendet wurde, ist das Thread-Objekt immer noch vorhanden.

    • RUNNABLE: Der Thread befindet sich im Bereit-Warteschlange und kann jederzeit von der CPU geplant und ausgeführt werden

    • BLOCKIERT: Nachdem ein Thread ein Objekt gesperrt (synchronisiert) hat, fällt es in den Status BLOCKIERT, wenn ein anderer Thread dieses Objekt ebenfalls sperren möchte. Erst wenn der erste Thread das Sperrobjekt entsperrt, kann der zweite Thread das Objekt sperren.

    • WARTEN: Wird mit der Funktion „synchronisiert“ verwendet. Sobald ein Thread „wait()“ aufruft, wird das Objekt zuerst entsperrt. Nachdem ein anderer Thread „notify()“ ausgeführt hat, wird der Thread in der Warteschleife natürlich aktiviert Legen Sie auch einen Wert in wait() fest. Die maximale Wartezeit, um tote Wartezeiten zu verhindern Ein Thread-Sicherheitsproblem: Erstens besteht die Ursache für Sicherheitsprobleme darin, dass es bei der gleichzeitigen Ausführung mehrerer Threads zu einem Phänomen der präventiven Ausführung kommt. Wann wird eine Codefolge als Thread bezeichnet? Sicherheitsproblem? Wie viele Threads werden gleichzeitig ausgeführt? Unabhängig davon, wie viele Threads ihren Code präventiv ausführen, hat dies keinen Einfluss auf das Endergebnis, das als Thread-Sicherheit bezeichnet wird Threaded. Sicherheitsproblem, Fehler!

    • Typischer Fall: Verwenden Sie zwei Threads, um die automatische Inkrementierung 100.000 Mal durchzuführen:

    • 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
      Nach dem Login kopieren
    Natürlich ist das erwartete Ergebnis 100.000, aber die Berechnung ist mehr als 6.000.000. Dies ist, was passiert ist Thread-Sicherheitsprobleme

    Analyse der Gründe

    :
    1. Führen Sie nur einen automatischen Inkrementierungsvorgang für die Heap-Anzahl jedes Threads durch: Zunächst müssen Sie verstehen, dass es drei Schritte für eine Maschinenanweisung gibt So führen Sie eine automatische Erhöhung durch: Holen Sie sich den Zählwert aus dem Hauptspeicher in das CPU-Register –>erhöhen Sie den Zählwert im Register um 1 –>aktualisieren Sie den Zählwert im Register im Hauptspeicher drei Schritte: Laden->Hinzufügen->Speichern

      Wir gehen davon aus, dass zwei Befehlssätze gleichzeitig auf einer CPU ausgeführt werden (es ist besser, zwei CPUs zu zeichnen) (es erfolgt kein gleichzeitiges Laden):
    2. Wie die Situation im Bild oben:

    Die Beobachtung ergab, dass beide Threads count++ einmal ausführten, die Ergebnisse von zwei ++ jedoch nicht zufriedenstellend waren, was nur einer automatischen Inkrementierung entspricht. Das Obige ist eine Thread-Sicherheit Problem.

    Und wir können vorhersagen, dass der Ergebnisbereich des obigen Codes zwischen 5 und 10 W liegt. Warum? Die beiden Bilder oben zeigen die Situation, in der zwei Additionen gleichzeitig verwendet werden Wenn sich zwei Threads in diesem Zustand befinden (auch der schlechteste Zustand), aber das Berechnungsergebnis 5w ist, führt der andere Thread dann einen solchen Vorgang aus, wenn ein Thread vollständig geladen wurde. dann wird es seriell ausgeführt, was nicht 10 W ist.

    3 Wie kann der oben genannte Fall gelöst werden? , dann hat Java eine solche Funktion, die wir verwenden können, nämlich die Verwendung des synchronisierten Schlüsselworts

    Thread-Status, Thread-Sicherheitsprobleme und die Verwendung des synchronisierten Schlüsselworts in Java

    Das heißt: cpu1 sperrt das Sperrobjekt, bevor es geladen wird, und entsperrt es erst nach dem Speichern Dann kann cpu2 das Objekt sperren und eine Reihe von Vorgängen ausführen. Zu diesem Zeitpunkt ist die Atomizität des Ladens, Hinzufügens und Speicherns sichergestellt, sodass diese drei Schritte entweder nicht ausgeführt werden und die Ausführung auf einmal abgeschlossen wird.

    Dann fragen Sie sich vielleicht: Was ist der Unterschied zwischen der Verwendung von nur einem Hauptthread zur 100.000-fachen Berechnung des Selbstinkrements?

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

    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
    Nach dem Login kopieren

    内存可见性问题

    首先说明:这是有编译器优化导致的,其次要知道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("我已经在主线程中修改了标志位");
        }
    }
    Nach dem Login kopieren

    运行上述代码之后,程序并不会终止,而是一直在那打印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
    Nach dem Login kopieren

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

    指令重排序问题

    生活案例:买菜

    Thread-Status, Thread-Sicherheitsprobleme und die Verwendung des synchronisierten Schlüsselworts in Java

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

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

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

    针对这样的问题,如果是针对一个变量,我们可以使用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;
        }
    }
    Nach dem Login kopieren
    class Counter{
        private static int count;
        public static void increase(){
            synchronized(Counter.class){//注意这里锁的是类对象哦
                count++;
            }
        }
        public int getCount(){
            return this.count;
        }
    }
    Nach dem Login kopieren
    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;
        }
    }
    Nach dem Login kopieren

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

    Conclusion

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

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

    • 罪恶之源是抢占式执行

    • Wenn mehrere Threads dieselbe Variable ändern, gibt es kein Thread-Sicherheitsproblem, wenn mehrere Threads nur eine Variable lesen

    • Änderung Der Vorgang ist nicht atomar.

    Das obige ist der detaillierte Inhalt vonThread-Status, Thread-Sicherheitsprobleme und die Verwendung des synchronisierten Schlüsselworts in Java. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

    Verwandte Etiketten:
    Quelle:yisu.com
    Erklärung dieser Website
    Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
    Beliebte Tutorials
    Mehr>
    Neueste Downloads
    Mehr>
    Web-Effekte
    Quellcode der Website
    Website-Materialien
    Frontend-Vorlage