Java自學-多線程 同步synchronized

来源:https://www.cnblogs.com/jeddzd/archive/2020/02/27/12374699.html
-Advertisement-
Play Games

Java 多線程同步 synchronized 多線程的同步問題指的是多個線程同時修改一個數據的時候,可能導致的問題 多線程的問題,又叫 Concurrency 問題 步驟 1 : 演示同步問題 假設蓋倫有10000滴血,並且在基地里,同時又被對方多個英雄攻擊 就是有 多個線程在減少蓋倫的hp 同時 ...


Java 多線程同步 synchronized

多線程的同步問題指的是多個線程同時修改一個數據的時候,可能導致的問題

多線程的問題,又叫Concurrency 問題

步驟 1 : 演示同步問題

假設蓋倫有10000滴血,並且在基地里,同時又被對方多個英雄攻擊
就是有多個線程在減少蓋倫的hp
同時又有多個線程在恢覆蓋倫的hp
假設線程的數量是一樣的,並且每次改變的值都是1,那麼所有線程結束後,蓋倫應該還是10000滴血。
但是。。。

註意: 不是每一次運行都會看到錯誤的數據產生,多運行幾次,或者增加運行的次數

演示同步問題

package charactor;
  
public class Hero{
    public String name;
    public float hp;
     
    public int damage;
     
    //回血
    public void recover(){
        hp=hp+1;
    }
     
    //掉血
    public void hurt(){
        hp=hp-1;
    }
     
    public void attackHero(Hero h) {
        h.hp-=damage;
        System.out.format("%s 正在攻擊 %s, %s的血變成了 %.0f%n",name,h.name,h.name,h.hp);
        if(h.isDead())
            System.out.println(h.name +"死了!");
    }
  
    public boolean isDead() {
        return 0>=hp?true:false;
    }
  
}

.

package multiplethread;
    
import charactor.Hero;
    
public class TestThread {
    
    public static void main(String[] args) {
            
        final Hero gareen = new Hero();
        gareen.name = "蓋倫";
        gareen.hp = 10000;
           
        System.out.printf("蓋倫的初始血量是 %.0f%n", gareen.hp);
           
        //多線程同步問題指的是多個線程同時修改一個數據的時候,導致的問題
           
        //假設蓋倫有10000滴血,並且在基地里,同時又被對方多個英雄攻擊
           
        //用JAVA代碼來表示,就是有多個線程在減少蓋倫的hp
        //同時又有多個線程在恢覆蓋倫的hp
           
        //n個線程增加蓋倫的hp
           
        int n = 10000;
   
        Thread[] addThreads = new Thread[n];
        Thread[] reduceThreads = new Thread[n];
           
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                    gareen.recover();
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            addThreads[i] = t;
               
        }
           
        //n個線程減少蓋倫的hp
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                    gareen.hurt();
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            reduceThreads[i] = t;
        }
           
        //等待所有增加線程結束
        for (Thread t : addThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        //等待所有減少線程結束
        for (Thread t : reduceThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
           
        //代碼執行到這裡,所有增加和減少線程都結束了
           
        //增加和減少線程的數量是一樣的,每次都增加,減少1.
        //那麼所有線程都結束後,蓋倫的hp應該還是初始值
           
        //但是事實上觀察到的是:
                   
        System.out.printf("%d個增加線程和%d個減少線程結束後%n蓋倫的血量變成了 %.0f%n", n,n,gareen.hp);
           
    }
        
}

步驟 2 : 分析同步問題產生的原因

  1. 假設增加線程先進入,得到的hp是10000
  2. 進行增加運算
  3. 正在做增加運算的時候,還沒有來得及修改hp的值,減少線程來了
  4. 減少線程得到的hp的值也是10000
  5. 減少線程進行減少運算
  6. 增加線程運算結束,得到值10001,並把這個值賦予hp
  7. 減少線程也運算結束,得到值9999,並把這個值賦予hp
    hp,最後的值就是9999
    雖然經歷了兩個線程各自增減了一次,本來期望還是原值10000,但是卻得到了一個9999
    這個時候的值9999是一個錯誤的值,在業務上又叫做臟數據

分析同步問題產生的原因
步驟 3 : 解決思路

總體解決思路是: 在增加線程訪問hp期間,其他線程不可以訪問hp

  1. 增加線程獲取到hp的值,併進行運算
  2. 在運算期間,減少線程試圖來獲取hp的值,但是不被允許
  3. 增加線程運算結束,併成功修改hp的值為10001
  4. 減少線程,在增加線程做完後,才能訪問hp的值,即10001
  5. 減少線程運算,並得到新的值10000

解決思路
步驟 4 : synchronized 同步對象概念

解決上述問題之前,先理解
synchronized關鍵字的意義
如下代碼:

Object someObject =new Object();
synchronized (someObject){
  //此處的代碼只有占有了someObject後才可以執行
}

synchronized表示當前線程,獨占 對象 someObject
當前線程獨占 了對象someObject,如果有其他線程試圖占有對象someObject,就會等待,直到當前線程釋放對someObject的占用。
someObject 又叫同步對象,所有的對象,都可以作為同步對象
為了達到同步的效果,必須使用同一個同步對象

釋放同步對象的方式: synchronized 塊自然結束,或者有異常拋出

synchronized 同步對象概念

package multiplethread;
  
import java.text.SimpleDateFormat;
import java.util.Date;
   
public class TestThread {
     
    public static String now(){
        return new SimpleDateFormat("HH:mm:ss").format(new Date());
    }
     
    public static void main(String[] args) {
        final Object someObject = new Object();
          
        Thread t1 = new Thread(){
            public void run(){
                try {
                    System.out.println( now()+" t1 線程已經運行");
                    System.out.println( now()+this.getName()+ " 試圖占有對象:someObject");
                    synchronized (someObject) {
                          
                        System.out.println( now()+this.getName()+ " 占有對象:someObject");
                        Thread.sleep(5000);
                        System.out.println( now()+this.getName()+ " 釋放對象:someObject");
                    }
                    System.out.println(now()+" t1 線程結束");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        };
        t1.setName(" t1");
        t1.start();
        Thread t2 = new Thread(){
  
            public void run(){
                try {
                    System.out.println( now()+" t2 線程已經運行");
                    System.out.println( now()+this.getName()+ " 試圖占有對象:someObject");
                    synchronized (someObject) {
                        System.out.println( now()+this.getName()+ " 占有對象:someObject");
                        Thread.sleep(5000);
                        System.out.println( now()+this.getName()+ " 釋放對象:someObject");
                    }
                    System.out.println(now()+" t2 線程結束");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        };
        t2.setName(" t2");
        t2.start();
    }
       
}

步驟 5 : 使用synchronized 解決同步問題

所有需要修改hp的地方,有要建立在占有someObject的基礎上
而對象 someObject在同一時間,只能被一個線程占有。 間接地,導致同一時間,hp只能被一個線程修改

使用synchronized 解決同步問題

package multiplethread;
   
import java.awt.GradientPaint;
 
import charactor.Hero;
   
public class TestThread {
   
    public static void main(String[] args) {
 
        final Object someObject = new Object();
         
        final Hero gareen = new Hero();
        gareen.name = "蓋倫";
        gareen.hp = 10000;
          
        int n = 10000;
  
        Thread[] addThreads = new Thread[n];
        Thread[] reduceThreads = new Thread[n];
          
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                     
                    //任何線程要修改hp的值,必須先占用someObject
                    synchronized (someObject) {
                        gareen.recover();
                    }
                     
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            addThreads[i] = t;
              
        }
          
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                    //任何線程要修改hp的值,必須先占用someObject
                    synchronized (someObject) {
                        gareen.hurt();
                    }
                     
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            reduceThreads[i] = t;
        }
          
        for (Thread t : addThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        for (Thread t : reduceThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
          
        System.out.printf("%d個增加線程和%d個減少線程結束後%n蓋倫的血量是 %.0f%n", n,n,gareen.hp);
          
    }
       
}

步驟 6 : 使用hero對象作為同步對象

既然任意對象都可以用來作為同步對象,而所有的線程訪問的都是同一個hero對象,索性就使用gareen來作為同步對象
進一步的,對於Hero的hurt方法,加上:

synchronized (this) {
}

表示當前對象為同步對象,即也是gareen為同步對象

package multiplethread;
   
import java.awt.GradientPaint;
 
import charactor.Hero;
   
public class TestThread {
   
    public static void main(String[] args) {
 
        final Hero gareen = new Hero();
        gareen.name = "蓋倫";
        gareen.hp = 10000;
          
        int n = 10000;
  
        Thread[] addThreads = new Thread[n];
        Thread[] reduceThreads = new Thread[n];
          
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                     
                    //使用gareen作為synchronized
                    synchronized (gareen) {
                        gareen.recover();
                    }
                     
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            addThreads[i] = t;
              
        }
          
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                    //使用gareen作為synchronized
                    //在方法hurt中有synchronized(this)
                    gareen.hurt();
                     
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            reduceThreads[i] = t;
        }
          
        for (Thread t : addThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        for (Thread t : reduceThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
          
        System.out.printf("%d個增加線程和%d個減少線程結束後%n蓋倫的血量是 %.0f%n", n,n,gareen.hp);
          
    }
       
}

.

package charactor;
  
public class Hero{
    public String name;
    public float hp;
     
    public int damage;
     
    //回血
    public void recover(){
        hp=hp+1;
    }
     
    //掉血
    public void hurt(){
        //使用this作為同步對象
        synchronized (this) {
            hp=hp-1;   
        }
    }
     
    public void attackHero(Hero h) {
        h.hp-=damage;
        System.out.format("%s 正在攻擊 %s, %s的血變成了 %.0f%n",name,h.name,h.name,h.hp);
        if(h.isDead())
            System.out.println(h.name +"死了!");
    }
  
    public boolean isDead() {
        return 0>=hp?true:false;
    }
  
}

步驟 7 : 在方法前,加上修飾符synchronized

在recover前,直接加上synchronized ,其所對應的同步對象,就是this
和hurt方法達到的效果是一樣
外部線程訪問gareen的方法,就不需要額外使用synchronized 了

package charactor;
  
public class Hero{
    public String name;
    public float hp;
     
    public int damage;
     
    //回血
    //直接在方法前加上修飾符synchronized
    //其所對應的同步對象,就是this
    //和hurt方法達到的效果一樣
    public synchronized void recover(){
        hp=hp+1;
    }
     
    //掉血
    public void hurt(){
        //使用this作為同步對象
        synchronized (this) {
            hp=hp-1;   
        }
    }
     
    public void attackHero(Hero h) {
        h.hp-=damage;
        System.out.format("%s 正在攻擊 %s, %s的血變成了 %.0f%n",name,h.name,h.name,h.hp);
        if(h.isDead())
            System.out.println(h.name +"死了!");
    }
  
    public boolean isDead() {
        return 0>=hp?true:false;
    }
  
}

.

package multiplethread;
       
import java.awt.GradientPaint;
 
import charactor.Hero;
   
public class TestThread {
   
    public static void main(String[] args) {
 
        final Hero gareen = new Hero();
        gareen.name = "蓋倫";
        gareen.hp = 10000;
          
        int n = 10000;
  
        Thread[] addThreads = new Thread[n];
        Thread[] reduceThreads = new Thread[n];
          
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                     
                    //recover自帶synchronized
                    gareen.recover();
                     
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            addThreads[i] = t;
              
        }
          
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                    //hurt自帶synchronized
                    gareen.hurt();
                     
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            reduceThreads[i] = t;
        }
          
        for (Thread t : addThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        for (Thread t : reduceThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
          
        System.out.printf("%d個增加線程和%d個減少線程結束後%n蓋倫的血量是 %.0f%n", n,n,gareen.hp);
          
    }
       
}

步驟 8 : 線程安全的類

如果一個類,其方法都是有synchronized修飾的,那麼該類就叫做線程安全的類

同一時間,只有一個線程能夠進入 這種類的一個實例 的去修改數據,進而保證了這個實例中的數據的安全(不會同時被多線程修改而變成臟數據)

比如StringBuffer和StringBuilder的區別
StringBuffer的方法都是有synchronized修飾的,StringBuffer就叫做線程安全的類
而StringBuilder就不是線程安全的類

線程安全的類


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 2000年,博客剛進入中國,卻並不被看好,用戶寥寥無幾。 直到2005年,隨著新浪、搜狐等門戶網站的佈局,博客逐漸在國內興起。 但幾年後,因微博、公眾號等媒介的發展,博客的生存空間受到擠壓,開始走向沒落。 然而,迄今為止,依舊有一批熱衷於創作的人在堅持經營著個人博客。 不少技術大牛和程式員,也更願意 ...
  • 題目描述:給定一個二叉樹,返回它的中序 遍歷。示例: 輸入: [1,null,2,3] 1 \ 2 / 3輸出: [1,3,2] 解法一:遞歸(較簡單) /** * Definition for a binary tree node. * public class TreeNode { * int  ...
  • Java synchronized 關鍵字詳解 前置技能點 進程和線程的概念 線程創建方式 線程的狀態狀態轉換 線程安全的概念 synchronized 關鍵字的幾種用法 1. 修飾非靜態成員方法 2. 修飾靜態成員方法 3. 類鎖代碼塊 4. 對象鎖代碼塊 synchronized 修飾非靜態方法 ...
  • 前言 在我們平時自己寫線程的測試demo時,一般都是用new Thread的方式來創建線程。但是,我們知道創建線程對象,就會在記憶體中開闢空間,而線程中的任務執行完畢之後,就會銷毀。 單個線程的話還好,如果線程的併發數量上來之後,就會頻繁的創建和銷毀對象。這樣,勢必會消耗大量的系統資源,進而影響執行效 ...
  • 這篇文章主要介紹 ElasticSearch 的基本概念,學習文檔、索引、集群、節點、分片等概念,同時會將 ElasticSearch 和關係型資料庫做簡單的類比,還會簡單介紹 REST API 的使用用法。 ElasticSearch 術語 索引和文檔是偏向於邏輯上的概念,節點和分片更偏向於物理上 ...
  • 基於JSP+Servlet+bootstrap開發電影院購票系統:開發環境: Windows操作系統開發工具: MyEclipse+Jdk+Tomcat+Mysql資料庫 程式要求:電影院訂票系統 用戶信息管理:用戶註冊後,可修改個人信息、登錄密碼等 影片分類:對影片進行分類,按類型、國家區域分類, ...
  • 開發環境: Windows操作系統開發工具: Eclipse+Jdk+Tomcat+MYSQL資料庫運行效果圖 源碼及原文鏈接:https://javadao.xyz/forum.php?mod=viewthread&tid=53 ...
  • 關註公眾號:CoderBuff,回覆“redis”獲取《Redis5.x入門教程》完整版PDF。 《Redis5.x入門教程》目錄 "第一章 · 準備工作" "第二章 · 數據類型" "第三章 · ​命令" "第四章 ​· 配置" "第五章 · Java客戶端(上)" "第六章 · 事務" "第七章 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...