欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

5個步驟讓你明白多線程和線程安全

 更新時間:2021年12月12日 15:06:59   作者:CSDN資訊  
本文詳細(xì)講解了多線程和線程安全的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下

什么是進(jìn)程?

電腦中時會有很多單獨(dú)運(yùn)行的程序,每個程序有一個獨(dú)立的進(jìn)程,而進(jìn)程之間是相互獨(dú)立存在的。比如下圖中的QQ、酷狗播放器、電腦管家等等。

什么是線程?

進(jìn)程想要執(zhí)行任務(wù)就需要依賴線程。換句話說,就是進(jìn)程中的最小執(zhí)行單位就是線程,并且一個進(jìn)程中至少有一個線程。

那什么是多線程?提到多線程這里要說兩個概念,就是串行和并行,搞清楚這個,我們才能更好地理解多線程。

所謂串行,其實(shí)是相對于單條線程來執(zhí)行多個任務(wù)來說的,我們就拿下載文件來舉個例子:當(dāng)我們下載多個文件時,在串行中它是按照一定的順序去進(jìn)行下載的,也就是說,必須等下載完A之后才能開始下載B,它們在時間上是不可能發(fā)生重疊的。

并行:下載多個文件,開啟多條線程,多個文件同時進(jìn)行下載,這里是嚴(yán)格意義上的,在同一時刻發(fā)生的,并行在時間上是重疊的。

了解了這兩個概念之后,我們再來說說什么是多線程。舉個例子,我們打開騰訊管家,騰訊管家本身就是一個程序,也就是說它就是一個進(jìn)程,它里面有很多的功能,我們可以看下圖,能查殺病毒、清理垃圾、電腦加速等眾多功能。

按照單線程來說,無論你想要清理垃圾、還是要病毒查殺,那么你必須先做完其中的一件事,才能做下一件事,這里面是有一個執(zhí)行順序的。

如果是多線程的話,我們其實(shí)在清理垃圾的時候,還可以進(jìn)行查殺病毒、電腦加速等等其他的操作,這個是嚴(yán)格意義上的同一時刻發(fā)生的,沒有執(zhí)行上的先后順序。

以上就是,一個進(jìn)程運(yùn)行時產(chǎn)生了多個線程。

在了解完這個問題后,我們又需要去了解一個使用多線程不得不考慮的問題——線程安全。

今天我們不說如何保證一個線程的安全,我們聊聊什么是線程安全?因?yàn)槲抑懊嬖嚤粏柕搅耍f真的,我之前真的不是特別了解這個問題,我們好像只學(xué)了如何確保一個線程安全,卻不知道所謂的安全到底是什么!

什么是線程安全?

既然是線程安全問題,那么毫無疑問,所有的隱患都是在多個線程訪問的情況下產(chǎn)生的,也就是我們要確保在多條線程訪問的時候,我們的程序還能按照我們預(yù)期的行為去執(zhí)行,我們看一下下面的代碼。

Integer count = 0;
public void getCount() {
       count ++;
       System.out.println(count);
 }

很簡單的一段代碼,下面我們就來統(tǒng)計一下這個方法的訪問次數(shù),多個線程同時訪問會不會出現(xiàn)什么問題,我開啟的3條線程,每個線程循環(huán)10次,得到以下結(jié)果:

我們可以看到,這里出現(xiàn)了兩個26,出現(xiàn)這種情況顯然表明這個方法根本就不是線程安全的,出現(xiàn)這種問題的原因有很多。

最常見的一種,就是我們A線程在進(jìn)入方法后,拿到了count的值,剛把這個值讀取出來,還沒有改變count的值的時候,結(jié)果線程B也進(jìn)來的,那么導(dǎo)致線程A和線程B拿到的count值是一樣的。

那么由此我們可以了解到,這確實(shí)不是一個線程安全的類,因?yàn)樗麄兌夹枰僮鬟@個共享的變量。其實(shí)要對線程安全問題給出一個明確的定義,還是蠻復(fù)雜的,我們根據(jù)我們這個程序來總結(jié)下什么是線程安全。

當(dāng)多個線程訪問某個方法時,不管你通過怎樣的調(diào)用方式、或者說這些線程如何交替地執(zhí)行,我們在主程序中不需要去做任何的同步,這個類的結(jié)果行為都是我們設(shè)想的正確行為,那么我們就可以說這個類是線程安全的。??

搞清楚了什么是線程安全,接下來我們看看Java中確保線程安全最常用的兩種方式。先來看段代碼。

public void threadMethod(int j) {

    int i = 1;

    j = j + i;
}

大家覺得這段代碼是線程安全的嗎?

毫無疑問,它絕對是線程安全的,我們來分析一下,為什么它是線程安全的?

我們可以看到這段代碼是沒有任何狀態(tài)的,就是說我們這段代碼,不包含任何的作用域,也沒有去引用其他類中的域進(jìn)行引用,它所執(zhí)行的作用范圍與執(zhí)行結(jié)果只存在它這條線程的局部變量中,并且只能由正在執(zhí)行的線程進(jìn)行訪問。當(dāng)前線程的訪問,不會對另一個訪問同一個方法的線程造成任何的影響。

兩個線程同時訪問這個方法,因?yàn)闆]有共享的數(shù)據(jù),所以他們之間的行為,并不會影響其他線程的操作和結(jié)果,所以說無狀態(tài)的對象,也是線程安全的。

添加一個狀態(tài)呢?

如果我們給這段代碼添加一個狀態(tài),添加一個count,來記錄這個方法并命中的次數(shù),每請求一次count+1,那么這個時候這個線程還是安全的嗎?

public class ThreadDemo {

   int count = 0; // 記錄方法的命中次數(shù)

   public void threadMethod(int j) {

       count++ ;

       int i = 1;

       j = j + i;
   }
}

很明顯已經(jīng)不是了,單線程運(yùn)行起來確實(shí)是沒有任何問題的,但是當(dāng)出現(xiàn)多條線程并發(fā)訪問這個方法的時候,問題就出現(xiàn)了,我們先來分析下count+1這個操作。

進(jìn)入這個方法之后首先要讀取count的值,然后修改count的值,最后才把這把值賦值給count,總共包含了三步過程:“讀取”一>“修改”一>“賦值”,既然這個過程是分步的,那么我們先來看下面這張圖,看看你能不能看出問題:

可以發(fā)現(xiàn),count的值并不是正確的結(jié)果,當(dāng)線程A讀取到count的值,但是還沒有進(jìn)行修改的時候,線程B已經(jīng)進(jìn)來了,然后線程B讀取到的還是count為1的值,正因?yàn)槿绱怂晕覀兊腸ount值已經(jīng)出現(xiàn)了偏差,那么這樣的程序放在我們的代碼中,是存在很多的隱患的。

如何確保線程安全?

既然存在線程安全的問題,那么肯定得想辦法解決這個問題,怎么解決?我們說說常見的幾種方式。

1、synchronized

synchronized關(guān)鍵字,就是用來控制線程同步的,保證我們的線程在多線程環(huán)境下,不被多個線程同時執(zhí)行,確保我們數(shù)據(jù)的完整性,使用方法一般是加在方法上。

public class ThreadDemo {

   int count = 0; // 記錄方法的命中次數(shù)

   public synchronized void threadMethod(int j) {

       count++ ;

       int i = 1;

       j = j + i;
   }
}

這樣就可以確保我們的線程同步了,同時這里需要注意一個大家平時忽略的問題,首先synchronized鎖的是括號里的對象,而不是代碼,其次,對于非靜態(tài)的synchronized方法,鎖的是對象本身也就是this。

當(dāng)synchronized鎖住一個對象之后,別的線程如果想要獲取鎖對象,那么就必須等這個線程執(zhí)行完釋放鎖對象之后才可以,否則一直處于等待狀態(tài)。

注意點(diǎn):雖然加synchronized關(guān)鍵字,可以讓我們的線程變得安全,但是我們在用的時候,也要注意縮小synchronized的使用范圍,如果隨意使用時很影響程序的性能,別的對象想拿到鎖,結(jié)果你沒用鎖還一直把鎖占用,這樣就有點(diǎn)浪費(fèi)資源。

2、Lock

先來說說它跟synchronized有什么區(qū)別吧,Lock是在Java1.6被引入進(jìn)來的,Lock的引入讓鎖有了可操作性,什么意思?就是我們在需要的時候去手動的獲取鎖和釋放鎖,甚至我們還可以中斷獲取以及超時獲取的同步特性,但是從使用上說Lock明顯沒有synchronized使用起來方便快捷。我們先來看下一般是如何使用的:

private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子類

   private void method(Thread thread){
       lock.lock(); // 獲取鎖對象
       try {
           System.out.println("線程名:"+thread.getName() + "獲得了鎖");
           // Thread.sleep(2000);
       }catch(Exception e){
           e.printStackTrace();
       } finally {
           System.out.println("線程名:"+thread.getName() + "釋放了鎖");
           lock.unlock(); // 釋放鎖對象
       }
   }

進(jìn)入方法我們首先要獲取到鎖,然后去執(zhí)行我們業(yè)務(wù)代碼,這里跟synchronized不同的是,Lock獲取的所對象需要我們親自去進(jìn)行釋放,為了防止我們代碼出現(xiàn)異常,所以我們的釋放鎖操作放在finally中,因?yàn)閒inally中的代碼無論如何都是會執(zhí)行的。

寫個主方法,開啟兩個線程測試一下我們的程序是否正常:

public static void main(String[] args) {
       LockTest lockTest = new LockTest();

       // 線程1
       Thread t1 = new Thread(new Runnable() {

           @Override
           public void run() {
               // Thread.currentThread()  返回當(dāng)前線程的引用
               lockTest.method(Thread.currentThread());
           }
       }, "t1");

       // 線程2
       Thread t2 = new Thread(new Runnable() {

           @Override
           public void run() {
               lockTest.method(Thread.currentThread());
           }
       }, "t2");

       t1.start();
       t2.start();
   }

結(jié)果:

可以看出我們的執(zhí)行,是沒有任何問題的。

其實(shí)在Lock還有幾種獲取鎖的方式,我們這里再說一種,就是tryLock()這個方法跟Lock()是有區(qū)別的,Lock在獲取鎖的時候,如果拿不到鎖,就一直處于等待狀態(tài),直到拿到鎖,但是tryLock()卻不是這樣的,tryLock是有一個Boolean的返回值的,如果沒有拿到鎖,直接返回false,停止等待,它不會像Lock()那樣去一直等待獲取鎖。

我們來看下代碼:

private void method(Thread thread){
       // lock.lock(); // 獲取鎖對象
       if (lock.tryLock()) {
           try {
               System.out.println("線程名:"+thread.getName() + "獲得了鎖");
               // Thread.sleep(2000);
           }catch(Exception e){
               e.printStackTrace();
           } finally {
               System.out.println("線程名:"+thread.getName() + "釋放了鎖");
               lock.unlock(); // 釋放鎖對象
           }
       }
   }

結(jié)果:我們繼續(xù)使用剛才的兩個線程進(jìn)行測試可以發(fā)現(xiàn),在線程t1獲取到鎖之后,線程t2立馬進(jìn)來,然后發(fā)現(xiàn)鎖已經(jīng)被占用,那么這個時候它也不在繼續(xù)等待。

似乎這種方法,感覺不是很完美,如果我第一個線程,拿到鎖的時間,比第二個線程進(jìn)來的時間還要長,是不是也拿不到鎖對象?

那我能不能,用一中方式來控制一下,讓后面等待的線程,可以等待5秒,如果5秒之后,還獲取不到鎖,那么就停止等,其實(shí)tryLock()是可以進(jìn)行設(shè)置等待的相應(yīng)時間的。

private void method(Thread thread) throws InterruptedException {
       // lock.lock(); // 獲取鎖對象

       // 如果2秒內(nèi)獲取不到鎖對象,那就不再等待
       if (lock.tryLock(2,TimeUnit.SECONDS)) {
           try {
               System.out.println("線程名:"+thread.getName() + "獲得了鎖");

               // 這里睡眠3秒
               Thread.sleep(3000);
           }catch(Exception e){
               e.printStackTrace();
           } finally {
               System.out.println("線程名:"+thread.getName() + "釋放了鎖");
               lock.unlock(); // 釋放鎖對象
           }
       }
   }

結(jié)果:看上面的代碼,我們可以發(fā)現(xiàn),雖然我們獲取鎖對象的時候,可以等待2秒,但是我們線程t1在獲取鎖對象之后,執(zhí)行任務(wù)缺花費(fèi)了3秒,那么這個時候線程t2是不在等待的。

我們再來改一下這個等待時間,改為5秒,再來看下結(jié)果:

private void method(Thread thread) throws InterruptedException {
       // lock.lock(); // 獲取鎖對象

       // 如果5秒內(nèi)獲取不到鎖對象,那就不再等待
       if (lock.tryLock(5,TimeUnit.SECONDS)) {
           try {
               System.out.println("線程名:"+thread.getName() + "獲得了鎖");
           }catch(Exception e){
               e.printStackTrace();
           } finally {
               System.out.println("線程名:"+thread.getName() + "釋放了鎖");
               lock.unlock(); // 釋放鎖對象
           }
       }
   }

結(jié)果:這個時候我們可以看到,線程t2等到5秒獲取到了鎖對象,執(zhí)行了任務(wù)代碼。

以上就是使用Lock,來保證我們線程安全的方式。

到此這篇關(guān)于5個步驟讓你明白多線程和線程安全的文章就介紹到這了。希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

相關(guān)文章

  • 詳解Spring Security中權(quán)限注解的使用

    詳解Spring Security中權(quán)限注解的使用

    這篇文章主要為大家詳細(xì)介紹一下Spring Security中權(quán)限注解的使用方法,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)或工作有一定參考價值,需要的可以參考一下
    2022-05-05
  • 解決spring boot hibernate 懶加載的問題

    解決spring boot hibernate 懶加載的問題

    這篇文章主要介紹了解決spring boot hibernate 懶加載的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-10-10
  • java+SQL server2008學(xué)生信息管理系統(tǒng)源碼

    java+SQL server2008學(xué)生信息管理系統(tǒng)源碼

    這篇文章主要為大家詳細(xì)介紹了java+SQL server2008學(xué)生信息管理系統(tǒng)源碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-01-01
  • SpringBoot使用Mybatis注解實(shí)現(xiàn)分頁動態(tài)sql開發(fā)教程

    SpringBoot使用Mybatis注解實(shí)現(xiàn)分頁動態(tài)sql開發(fā)教程

    這篇文章主要為大家介紹了SpringBoot使用Mybatis注解實(shí)現(xiàn)分頁及動態(tài)sql開發(fā)教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-03-03
  • java實(shí)現(xiàn)Base64加密解密算法

    java實(shí)現(xiàn)Base64加密解密算法

    Base64用來將非ASCII字符的數(shù)據(jù)轉(zhuǎn)換成ASCII字符的一種方法,這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)Base64加密解密算法,感興趣的小伙伴們可以參考一下
    2016-04-04
  • 詳解Java中的ThreadLocal

    詳解Java中的ThreadLocal

    ThreadLocal是JDK包提供的,它提供線程本地變量,如果創(chuàng)建一個ThreadLocal變量,那么訪問這個變量的每個線程都會有這個變量的一個副本,在實(shí)際多線程操作的時候,操作的是自己本地內(nèi)存中的變量,從而規(guī)避了線程安全問題
    2021-06-06
  • Java BufferWriter寫文件寫不進(jìn)去或缺失數(shù)據(jù)的解決

    Java BufferWriter寫文件寫不進(jìn)去或缺失數(shù)據(jù)的解決

    這篇文章主要介紹了Java BufferWriter寫文件寫不進(jìn)去或缺失數(shù)據(jù)的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-07-07
  • @PathVariable和@RequestParam傳參為空問題及解決

    @PathVariable和@RequestParam傳參為空問題及解決

    這篇文章主要介紹了@PathVariable和@RequestParam傳參為空問題及解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-11-11
  • 基于SpringBoot創(chuàng)建Web頁面并熱更新的操作步驟

    基于SpringBoot創(chuàng)建Web頁面并熱更新的操作步驟

    SpringBoot是一個用于快速開發(fā)單個微服務(wù)的框架,它基于 Spring 框架,簡化了Spring應(yīng)用的初始化過程和開發(fā)流程,本文給大家介紹了如何基于SpringBoot創(chuàng)建Web頁面并熱更新,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下
    2023-11-11
  • elasticsearch索引的創(chuàng)建過程index?create邏輯分析

    elasticsearch索引的創(chuàng)建過程index?create邏輯分析

    這篇文章主要介紹了elasticsearch索引核心index?create,索引的創(chuàng)建過程解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-04-04

最新評論