2.3多線程(java學習筆記)synchronized關鍵字

来源:https://www.cnblogs.com/huang-changfan/archive/2018/08/12/9460073.html
-Advertisement-
Play Games

一、為什麼要用synchronized關鍵字 首先多線程中多個線程運行面臨共用數據同步的問題。 多線程正常使用共用數據時需要經過以下步驟: 1.線程A從共用數據區中複製出數據副本,然後處理。 2.線程A將處理好的數據副本寫入共用數據區。 3.線程B從共用數據區中複製出數據副本。 如此迴圈,直到線程結 ...


一、為什麼要用synchronized關鍵字

首先多線程中多個線程運行面臨共用數據同步的問題。

多線程正常使用共用數據時需要經過以下步驟:

1.線程A從共用數據區中複製出數據副本,然後處理。

2.線程A將處理好的數據副本寫入共用數據區。

3.線程B從共用數據區中複製出數據副本。

如此迴圈,直到線程結束。

 

 假如線程A從共用數據區中複製出數據副本然後處理,在還沒有將更新的數據放入主記憶體時,線程B來到主記憶體讀取了未更新的數據,這樣就出問題了。

這就是所謂的臟讀,這類問題稱為多線程的併發問題。

 

舉個具體的例子:

 1 public class TestThread {
 2      public static void main(String[] args){
 3          TestSynchronized s = new TestSynchronized();
 4          new Thread(s,"t1").start();    //兩個線程訪問一個對象
 5          new Thread(s,"t2").start();
 6      }
 7 }
 8 
 9 class TestSynchronized implements Runnable{
10     private int ticket = 5;
11     
12     public void run(){    
13             for(int p = 0; p < 10; p++){
14                 try {
15                     Thread.sleep(500);
16                 } catch (InterruptedException e) {
17                     // TODO Auto-generated catch block
18                     e.printStackTrace();
19                 }
20                 if(ticket >= 0){
21                     System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
22                 }
23             }
24     }
25 }
運行結果:
t2 ticket:4
t1 ticket:5
t1 ticket:2
t2 ticket:3
t1 ticket:1
t2 ticket:1
t2 ticket:0

可以看到1號票同時給了t1和t2,當t1讀入1執行了ticket--後,數據還沒有來得及寫入主記憶體就被t2從主記憶體中讀走了1,就造成了這種現象。

要想避免這種現象就需要使用synchronized關鍵字,synchronized英譯為同步,我們認為暫且把他看做鎖定更好理解。

 

接下來我們看看synchronized如何使用。

 

二、synchronized的用法

1. synchronized修飾方法(也稱同步方法)

 

(1) java中每個對象都有一個鎖(lock),或者叫做監視器,當前線程訪問某個對象中synchronized修飾的方法(同步塊)時,線程需要獲取到該對象的鎖,獲取對象鎖後才能訪問該對象中synchronized方法(同步塊),且一個對象中只有一個鎖。

(2) 沒有獲得該對象的鎖的其他線程,無法訪問該對象中synchronized修飾的方法(同步塊)。

(3) 其他線程要想訪問該對象中synchronized修飾的方法需要獲取該對象的鎖。

(4) 對象鎖只有將synchronized方法(同步塊)中的內容運行完畢或遇到異常才會釋放鎖。

 例一:

 1 public class TestThread {
 2      public static void main(String[] args){
 3          TestSynchronized s = new TestSynchronized();
 4          new Thread(s,"t1").start();     //兩個線程訪問一個對象
 5          new Thread(s,"t2").start();
 6      }
 7 }
 8 
 9 class TestSynchronized implements Runnable{
10     private int ticket = 5;
11     
12     synchronized public void run(){    
13             for(int p = 0; p < 10; p++){
14                 try {
15                     Thread.sleep(500);
16                 } catch (InterruptedException e) {
17                     // TODO Auto-generated catch block
18                     e.printStackTrace();
19                 }
20                 if(ticket >= 0){
21                     System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
22                 }                    
23             }
24         }
25 }
運行結果:
t1 ticket:5
t1 ticket:4
t1 ticket:3
t1 ticket:2
t1 ticket:1
t1 ticket:0

我們來分析上面程式,首先線程t1進去run方法獲得對象s的鎖,然後執行完run方法釋放鎖,run運行忘了也就沒有t2的事了。

因為只有將synchronized修飾的方法執行完才會釋放鎖,故列印五個t1.。

 

還有一點,如果一個對象裡面有多個synchronized方法,某一時刻只能有一個線程進入其中一個synchronized修飾的方法,則這時其他任何線程無法進入該對象中任何一個synchronized修飾的方法。

補充片段:

 1 public class TestThread {
 2     public static void main(String[] args){
 3         Test m1 = new Test();  //兩個線程共訪問一個對象。
 4 
 5         TestSynchronized_1 s1 = new TestSynchronized_1(m1);
 6         TestSynchronized_2 s2 = new TestSynchronized_2(m1);
 7         new Thread(s1,"t1").start();
 8         new Thread(s2,"t2").start();
 9     }
10 }
11 
12 class Test{
13     synchronized public void test1(){
14                for(int p = 0; p < 5; p++){
15                    System.out.println("s1.run.TestSynchronized_test 1");
16                }   
17     }
18     
19     synchronized public void test2(){
20         for(int p = 0; p < 5; p++){
21             System.out.println("s2.run.TestSynchronized_test 2");
22         }   
23 }
24 }
25 
26 class TestSynchronized_1 implements Runnable{
27    
28    private Test m;
29    public TestSynchronized_1(Test m){
30       this.m = m;
31    }
32    
33    public void run(){
34        m.test1();
35    }
36 }
37 
38 class TestSynchronized_2 implements Runnable{
39        
40        private Test m;
41        public TestSynchronized_2(Test m){
42           this.m = m;
43        }
44        
45        public void run(){
46            m.test2();
47        }
48 }

 

運行結果:
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s1.run.TestSynchronized_test 1
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2
s2.run.TestSynchronized_test 2

當線程t1運行synchronized修飾的test1方法時,線程t2是無法運行test2方法。結合之前說的,一個對象鎖只有一把,而這裡是兩個線程共用對象(m1),當線程t1獲得鎖時,線程t2就只能等待。歸根結底把握幾個要點:

1.鎖的唯一性(一個對象只有一把鎖,但不同對象就有不同的鎖)

2.沒鎖不能進去入synchronized修飾內容中運行。

3.只有運行完synchronized修飾的內容或遇到異常才釋放鎖。

 

 

我們來看下麵這個代碼:

例二:

 1 public class TestThread {
 2      public static void main(String[] args){
 3          Mouth m1 = new Mouth();  
 4          Mouth m2 = new Mouth();  
 5          TestSynchronized s1 = new TestSynchronized(m1);//兩個線程訪問兩個對象。
 6          TestSynchronized s2 = new TestSynchronized(m2);
 7          new Thread(s1,"t1").start();  //線程t1
 8          new Thread(s2,"t2").start();   //線程t2
 9      }
10 }
11 
12 class Mouth{  //資源及方法 
13     synchronized public void test(){
14         int ticket = 5;
15         for(int p = 0; p < 10; p++){
16             try {
17                 Thread.sleep(500);
18             } catch (InterruptedException e) {
19                 // TODO Auto-generated catch block
20                 e.printStackTrace();
21             }
22             if(ticket >= 0){
23                 System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
24             }                    
25         }
26     }
27 
28 }
29 
30 class TestSynchronized implements Runnable{
31     private Mouth m = new Mouth();
32     
33     public TestSynchronized(Mouth m){
34         this.m = m;
35     }
36     
37     synchronized public void run(){
38         m.test();
39     }
40 }
運行結果:
t1 ticket:5
t2 ticket:5
t2 ticket:4
t1 ticket:4
t1 ticket:3
t2 ticket:3
t2 ticket:2
t1 ticket:2
t1 ticket:1
t2 ticket:1
t2 ticket:0
t1 ticket:0

 

可以發現好像用synchronized修飾的test方法沒有起作用,怎麼是t1,t2怎麼是交替運行的?

我們回顧下之前說的對象鎖,線程獲得對象鎖後可以訪問該對象裡面synchronized修飾的方法,其他線程無法訪問。

我們上面的代碼裡面對象有兩個,一個是m1、一個是m2。

t1獲得了對象m1的鎖,然後訪問m1中的test方法;t2獲得了對象m2的鎖,然後訪問s2中的test方法。

線程t1和線程t2訪問的是不同的資源(m1,m2),並不相互干擾所以沒有影響。例一中是因為兩個線程訪問同一個資源(s1)所以synchronized的起了限製作用。

synchronized修飾方法時只能對多個線程訪問同一資源(對象)時起限製作用。

 

 

可能大家會說了,那我們有沒有辦法也限制下這種情況呢,答案當然是可以的。

這就是下麵要說的:

 

2.synchronized修飾靜態方法

 當修飾靜態方法時鎖定的是,而不是對象,我們先把例二修改下看下結果。

例三:

 1 public class TestThread {
 2      public static void main(String[] args){
 3          Mouth m1 = new Mouth();
 4          Mouth m2 = new Mouth();
 5          TestSynchronized s1 = new TestSynchronized(m1);
 6          TestSynchronized s2 = new TestSynchronized(m2); //兩個線程訪問兩個對象
 7          new Thread(s1,"t1").start();
 8          new Thread(s2,"t2").start();
 9      }
10 }
11 
12 class Mouth{
13     synchronized public static void test(){ //改為靜態方法,鎖定的是類。
14         int ticket = 5;
15         for(int p = 0; p < 10; p++){
16             try {
17                 Thread.sleep(500);
18             } catch (InterruptedException e) {
19                 // TODO Auto-generated catch block
20                 e.printStackTrace();
21             }
22             if(ticket >= 0){
23                 System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
24             }                    
25         }
26     }
27 
28 }
29 
30 class TestSynchronized implements Runnable{
31     private Mouth m = new Mouth();
32     
33     public TestSynchronized(Mouth m){
34         this.m = m;
35     }
36     
37     synchronized public void run(){
38         m.test();
39     }
40 }
運行結果:
t1 ticket:5
t1 ticket:4
t1 ticket:3
t1 ticket:2
t1 ticket:1
t1 ticket:0
t2 ticket:5
t2 ticket:4
t2 ticket:3
t2 ticket:2
t2 ticket:1
t2 ticket:0

當synchronized修飾靜態方法時,線程需要獲得類(Mouth)鎖才能運行,沒有獲得類鎖的線程無法運行,且獲得類鎖的線程會將synchronized修飾的靜態方法會運行完畢才釋放類鎖。

例如例三中的代碼,t1先獲得類(Mouth)鎖運行Mouth類中的test方法,而t2沒有類(Mouth)鎖就無法運行。一個類只有一個類鎖,卻可以有多個對象(t1,t2等...)都是一個類(Mouth)中的對象,只要一個線程獲取了類(Mouth)鎖,其他線程就要等到類鎖被釋放,然後獲得類(Mouth)鎖之後才能運行類(Mouth)中synchronized修飾的靜態方法。所以即使是兩個線程(t1,t2)訪問兩個不同的資源(m1,m2)也會受到限制,因為m1,m2都屬於一個類(Mouth),而鎖住類(Mouth)後每次只能有一個線程訪問該類(Mouth)中的sychronized修飾的靜態方法。

當t1訪問m1中的test時,首先獲得類(Mouth)鎖,這時如果t2訪問m2中的test方法時也需要獲得類鎖,可是這時類鎖已經被線程t1獲得,故t2無法訪問m2中的方法。只有等t1運行完方法中的內容或異常釋放鎖後t2才有機會獲得鎖,獲得鎖後才能運行。

而之前例一中t1,t2鎖的是對象,需要結合這幾段代碼理解下。

 

3.synchronized塊(也稱同步塊)

如果每次都鎖定的範圍都是一個方法,每次只能有一個線程進去勢必會導致效率的低下,這主要是鎖定範圍過多引起的。

這時可以根據實際情況鎖定合適的區域,這就要用到同步塊了

synchronized(需要鎖住的對象或類){
       
       鎖定的部分,需要鎖才能運行。
}

 ()中可以確定鎖定的是對象還是類,鎖定對象的話可以用this,對類上鎖類名加class,例如要鎖定Mounth類(Moutn.class)。

我們首先看個沒有任何同步的例子:

 1 public class TestThread {
 2      public static void main(String[] args){
 3          TestSynchronized s1 = new TestSynchronized();
 4     
 5          new Thread(s1,"t1").start();  //兩個線程訪問一個對象
 6          new Thread(s1,"t2").start();
 7      }
 8 }
 9 
10 class TestSynchronized implements Runnable{
11     private int ticket = 5;
12     
13            public void run(){    
14             for(int p = 0; p < 10; p++){
15                 try {
16                     Thread.sleep(1000);
17                 } catch (InterruptedException e) {
18                     // TODO Auto-generated catch block
19                     e.printStackTrace();
20                 }
21                 if(ticket >= 0){
22                     System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
23                 }                    
24             }
25         }
26 }
運行結果:
t1 ticket:5
t2 ticket:4
t2 ticket:3
t1 ticket:2
t2 ticket:1
t1 ticket:1
t2 ticket:0
t1 ticket:-1

其中出現了-1,我們在其中一塊區域加上同步形成同步塊。

 1 public class TestThread {
 2      public static void main(String[] args){
 3          TestSynchronized s1 = new TestSynchronized(); //兩個線程訪問一個對象
 4     
 5          new Thread(s1,"t1").start();
 6          new Thread(s1,"t2").start();
 7      }
 8 }
 9 
10 class TestSynchronized implements Runnable{
11     private int ticket = 5;
12     
13            public void run(){    
14             for(int p = 0; p < 10; p++){
15                 try {
16                     Thread.sleep(1000);
17                 } catch (InterruptedException e) {
18                     // TODO Auto-generated catch block
19                     e.printStackTrace();
20                 }
21                 synchronized(this){  //此次加上同步塊,這部分內容一次只有一個線程可以進入,其他內容不受約束。
22                     if(ticket >= 0){ //這裡鎖的是對象,這裡面的內容需要對象鎖才能運行。
23                         System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
24                     }
25                 }                    
26             }
27         }
28 }
運行結果:
t2 ticket:5
t1 ticket:4
t1 ticket:3
t2 ticket:2
t1 ticket:1
t2 ticket:0

一次只能有一個線程進入同步塊中,就不會出現線程讀了未更新的數據或者多減一次的情況。未加synchronized修飾的其他區域不受影響,故兩個線程的順序不定。

 

下麵我們來看一個同步塊鎖定的例子,效果和例三一樣,只不過例三使用靜態方法鎖定了類,而下麵這個是使用同步塊鎖定了類。

 1 public class TestThread {
 2     public static void main(String[] args){
 3         TestSynchronized s1 = new TestSynchronized();
 4         TestSynchronized s2 = new TestSynchronized(); //兩個線程訪問兩個對象
 5         new Thread(s1,"t1").start();
 6         new Thread(s2,"t2").start();
 7     }
 8 }
 9 
10 class TestSynchronized implements Runnable{
11    private int ticket = 5;
12    
13    synchronized public void run(){
14        synchronized(TestSynchronized.class){  //將synchronized修飾的靜態方法改成了同步塊。
15             
16            for(int p = 0; p < 10; p++){
17                try {
18                    Thread.sleep(500);
19                } catch (InterruptedException e) {
20                    // TODO Auto-generated catch block
21                    e.printStackTrace();
22                }
23                if(ticket >= 0){
24                    System.out.println(Thread.currentThread().getName() + " ticket:" + ticket--);
25                }                    
26            }
27        }
28    }
29 }
運行結果:
t1 ticket:5
t1 ticket:4
t1 ticket:3
t1 ticket:2
t1 ticket:1
t1 ticket:0
t2 ticket:5
t2 ticket:4
t2 ticket:3
t2 ticket:2
t2 ticket:1
t2 ticket:0

上述代碼和例三功能一樣,只是鎖定方法不同,這裡只是做下演示。

 

synchronized修飾方法是一種粗顆粒的併發控制,某一時刻只有一個線程執行方法內的內容效率較低下。

synchronized同步塊是一種細顆粒的併發控制,可以自行根據需求確定區域較為靈活,可以平衡下效率和安全,同時也能因選擇區域不恰當而造成問題。

只要不在synchronized方法(同步塊)內的其他部分都不受限制。

 

普通方法鎖定的對象,需要獲得對象鎖

靜態方法鎖定的是類,需要獲得類鎖。

同步塊可以確定是鎖對象(this )還是鎖類(xxx.class),同時也可以自行確定區域。 


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

-Advertisement-
Play Games
更多相關文章
  • 前言 基礎面向對象 靜態,單例模式,繼承詳情知識點。 關鍵字, 變數,靜態代碼塊,代碼塊(不加靜態),對象創建過程,單例模式,繼承。 靜態 static關鍵字 在以上的代碼中,我們會遇到這種情況,創建了一個對象,但是調用功能時,沒有調用到對象中的數據,那麼為啥還要調用呢?創建對象不就是為了封裝數據的 ...
  • get是通過URL傳參,容易暴露,容易被sql註入,防止sql註入的方式之一是:加intval($_GET['id']); ...
  • 最終效果:瀏覽器地址欄輸入www.baidu.com訪問時,會顯示自己的網頁 1、創建文件 任意盤新建一個www.baidu.com文件,在該文件夾下新建WEB-INF文件、自己寫的一個html文件,一張圖片,然後在WEB-INF下新建一個classes文件、lib文件以及一個web.xml文件,在 ...
  • 給定兩個有序整數數組 nums1 和 nums2,將 nums2 合併到 nums1 中,使得 num1 成為一個有序數組。 說明: 初始化 nums1 和 nums2 的元素數量分別為 m 和 n。 你可以假設 nums1 有足夠的空間(空間大小大於或等於 m + n)來保存 nums2 中的元素 ...
  • 又有時間寫東西了,最近深感世事並不以人的美好願望而改變,還是要以積極地心態來適應新變化,多多關心身邊的人。 圖釘畫中一個圖釘代表一個像素,所以關鍵在於像素渣化,降低解析度,圖釘的色彩有限,還需要降低圖片的色彩數量,統計各種色彩的數量及位置。 以上都可以用Pillow完成,Pillow是Python中 ...
  • 網路上兩台主機的交互 ①根據IP找到對方主機 ②數據發送到對方指定的應用程式上,為了表示這些應用程式,引入了埠的概念。 常用埠: wed埠80 MySQL埠3306 有效埠 0~65535 ③定義通信規則,稱為協議。國際組織定義了通用協議TCP/IP協議 本地迴環地址:127.0.0.1( ...
  • 恢復內容開始 python爬蟲學習從0開始 第一次學習了python語法,迫不及待的來開始python的項目。首先接觸了爬蟲,是一個簡單爬蟲。個人感覺python非常簡潔,相比起java或其他面向對象的編程語言,動態語言不需要聲明函數或變數類型。python有20年的發展歷史,以簡潔高效聞名,pyt ...
  • 說概率前複習下歷史函數create_rand_list() #創建一個含有指定數量元素的listsum_fun() #累加len_fun() #統計個數multiply_fun() #累乘sum_mean_fun() #算數平均數sum_mean_rate() #算數平均數計算回報median_fu ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...