首頁  >  文章  >  Java  >  Java中執行緒狀態、執行緒安全性問題和synchronized關鍵字的使用

Java中執行緒狀態、執行緒安全性問題和synchronized關鍵字的使用

WBOY
WBOY轉載
2023-05-07 14:46:081154瀏覽

    java中的執行緒狀態

    在作業系統層面,一個執行緒就兩個狀態:就緒和阻塞狀態.

    但是java中為了在線程阻塞時能夠更快速的知曉一個線程阻塞的原因,又將阻塞的狀態進行了細化.

    Java中執行緒狀態、執行緒安全性問題和synchronized關鍵字的使用

    • ##NEW :線程物件已經創建好了,但是係統層面的線程還沒創建好,或者說線程對象還沒調用start()

    • #TERMINATED:系統中的線程已經銷毀,但是程式碼中的執行緒物件還在,也就是run()跑完了,Thread物件還在

    • RUNNABLE:執行緒位於就緒佇列,隨時都有可能被cpu調度執行

    • TIMED_WAITING:執行緒執行過程中,執行緒物件呼叫了sleep(),進入阻塞,休眠時間到了,就會回到就緒佇列

    • BLOCKED :有一個線程將一個對像上鎖(synchronized)之後,另一個線程也想給這個對像上鎖,就會陷入BLOCKED狀態,只有第一個線程將鎖對象解鎖了,後一個線程才有可能給這個物件進行上鎖.

    • WAITING:搭配synchronized進行使用wait(),一旦一個執行緒呼叫了wait(),會先將所物件解鎖,等到另一個執行緒進行notify( ),之後wait中的執行緒才會被喚醒,當然也可以在wait()中設定一個最長等待時間,防止出現死等.

    執行緒安全問題案例分析

    多執行緒對同一變數進行寫入操作

    1. 概念:一串程式碼什麼時候叫作有執行緒安全性問題呢?首先執行緒安全問題的罪惡之源是,多執行緒並發執行的時候,會有搶佔式執行的現象,這裡的搶佔式執行,執行的是機器指令!那一串代碼什麼時候叫作有線程安全問題呢?多線程並發時,不管若干個線程怎麼去搶佔式執行他們的程式碼,都不會影響最終結果,就叫作線程安全,但是由於搶佔式執行,出現了和預期不一樣的結果,就叫作有線程安全問題,出bug了!

    2. 典型案例:使用兩個執行緒對同一個數進行自增操作10w次:

    3. 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
    顯然預期結果是10w,但算出來就是6w多,這就是出現了線程安全問題.

    分析原因:

    僅針對每個線程的堆count進行自增的操作:首先要明白,進行一次自增的機器指令有三步:從主內存中把count值拿到cpu寄存器中->把寄存器中的count值進行自增1->把寄存器中的count值刷新到主內存中,我們姑且把這三步驟叫作:load->add->save

    我們假設就是在一個cpu上(畫兩個cpu好表示)並發執行兩組指令(就不會出現同時load這樣的情況了):

    Java中執行緒狀態、執行緒安全性問題和synchronized關鍵字的使用

    如出現上圖的情況:

    Java中執行緒狀態、執行緒安全性問題和synchronized關鍵字的使用

    觀察發現:兩個執行緒都是執行了一次count ,但是兩次的結果卻不如意,相當於只進行了一次自增,上述就是出現了線程安全問題了.

    並且我們可以預測出上述程式碼的結果範圍:5w-10w之間!,為什麼呢?

    上面兩張圖表示的是出現線程安全問題的情況,表現的結果就是兩次加加當一次去用了,如果兩個線程一直處於這樣的狀態(也是最壞的狀態了),可不就是計算結果就是5w咯,那如果兩個線程一直是一個線程完整的執行完load-add-save之後,另一個線程再去執行這樣的操作,那就串行式執行了,可不就是10w咯.

    3.針對上述案例如何去解決呢?

    案例最後也提到了,只要能夠實現串行式執行,就能保證結果的正確性,那java確實有這樣的功能供我們使用,即synchronized關鍵字的使用.

    Java中執行緒狀態、執行緒安全性問題和synchronized關鍵字的使用

    也就是說:cpu1執行load之前先給鎖對象加鎖,save之後再進行解鎖,cpu2此時才能去給那個對象進行上鎖,並進行一系列的操作.此時也就是保證了load-add-save的原子性,使得這三個步驟要麼就別執行,執行就一口氣執行完.

    那你可能會提問,那這樣和只用一個main線程去計算自增10w次有什麼區別,創建多線程還有什麼意義呢?

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

    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.线程安全问题

    • 罪恶之源是抢占式执行

    • 多執行緒對同一個變數進行修改,多執行緒只讀一個變數是沒有執行緒安全性問題的

    • 修改運算是非原子性的

    • 記憶體可見性所造成的執行緒安全性問題

    • 指令重新排序所引起的執行緒安全性問題

    3.synchronized的本質和用法

    以上是Java中執行緒狀態、執行緒安全性問題和synchronized關鍵字的使用的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    陳述:
    本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除