基于線程、并發(fā)的基本概念(詳解)
什么是線程?
提到“線程”總免不了要和“進(jìn)程”做比較,而我認(rèn)為在Java并發(fā)編程中混淆的不是“線程”和“進(jìn)程”的區(qū)別,而是“任務(wù)(Task)”。進(jìn)程是表示資源分配的基本單位。而線程則是進(jìn)程中執(zhí)行運(yùn)算的最小單位,即執(zhí)行處理機(jī)調(diào)度的基本單位。關(guān)于“線程”和“進(jìn)程”的區(qū)別耳熟能詳,說來說去就一句話:通常來講一個(gè)程序有一個(gè)進(jìn)程,而一個(gè)進(jìn)程可以有多個(gè)線程。
但是“任務(wù)”是很容易忽略的一個(gè)概念。我們?cè)趯?shí)際編碼中通常會(huì)看到這么一個(gè)包叫做xxx.xxx.task,包下是XxxTask等等以Task后綴名結(jié)尾的類。而XxxTask類通常都是實(shí)現(xiàn)Runnable接口或者Thread類。嚴(yán)格來說,“任務(wù)”和并發(fā)編程沒多大關(guān)系,就算是單線程結(jié)構(gòu)化順序編程中,我們也可以定義一個(gè)Task類,在類中執(zhí)行我們想要完成的一系列操作?!叭蝿?wù)”我認(rèn)為是我們?nèi)藶槎x的一個(gè)概念,既抽象又具體,抽象在它指由軟件完成的一個(gè)活動(dòng),它可以是一個(gè)線程,也可以是多個(gè)線程共同達(dá)到某一目的的操作,具體在于它是我們認(rèn)為指定實(shí)實(shí)在在的操作,例如:定時(shí)獲取天氣任務(wù)(定時(shí)任務(wù)),下線任務(wù)……關(guān)鍵就在于不要認(rèn)為一個(gè)任務(wù)對(duì)應(yīng)的就是一個(gè)線程,也許它是多個(gè)線程,甚至在這個(gè)任務(wù)中是一個(gè)線程池,這個(gè)線程池處理這個(gè)我們定義的操作。
我產(chǎn)生“線程”和“任務(wù)”的疑惑就是在《Thinking in Java》這本書的“并發(fā)”章節(jié)中它將線程直接定義為一個(gè)任務(wù),在開篇標(biāo)題就取名為“定義任務(wù)”,并且提到定義任務(wù)只需實(shí)現(xiàn)Runnable接口.而這個(gè)任務(wù)則是通過調(diào)用start來創(chuàng)建一改新的線程來執(zhí)行.說來說去有點(diǎn)繞,其實(shí)也不必糾結(jié)于在書中時(shí)而提到線程,時(shí)而提到人任務(wù).我認(rèn)為就記住:任務(wù)是我們?cè)诰幊虝r(shí)所賦這段代碼的實(shí)際意義,而線程就關(guān)注它是否安全,是否需要安全,這就是后面要提到的線程安全問題.在像我一樣產(chǎn)生疑惑時(shí),不用在意它兩者間的關(guān)系和提法。
什么是并發(fā)?
提到了并發(fā),那又不得不和并行作比較。并發(fā)是指在一段時(shí)間內(nèi)同時(shí)做多個(gè)事情,比如在1點(diǎn)-2點(diǎn)洗碗、洗衣服等。而并行是指在同一時(shí)刻做多個(gè)事情,比如1點(diǎn)我左手畫圓右手畫方。兩個(gè)很重要的區(qū)別就是“一段時(shí)間”和“同一時(shí)刻”.在操作系統(tǒng)中就是:
1) 并發(fā)就是在單核處理中同時(shí)處理多個(gè)任務(wù)。(這里的同時(shí)指的是邏輯上的同時(shí))
2) 并行就是在多核處理器中同時(shí)處理多個(gè)任務(wù)。(這里的同時(shí)指的就是物理上的同時(shí))
初學(xué)編程基本上都是單線程結(jié)構(gòu)化編程,或者說是根本就接觸不到線程這個(gè)概念,反正程序照著自己實(shí)現(xiàn)的邏輯,程序一步一步按照我們的邏輯去實(shí)現(xiàn)并且得到希望輸出的結(jié)果。但隨著編程能力的提高,以及應(yīng)用場(chǎng)景的復(fù)雜多變,我們不得不要面臨多線程并發(fā)編程。而初學(xué)多線程并發(fā)編程時(shí),常常出現(xiàn)一些預(yù)料之外的結(jié)果,這就是涉及到“線程安全”問題。
什么線程安全?
這是在多線程并發(fā)編程中需要引起足夠重視的問題,如果你的線程不足夠“安全”,程序就可能出現(xiàn)難以預(yù)料,以及難以復(fù)現(xiàn)的結(jié)果?!禞ava并發(fā)編程實(shí)戰(zhàn)》提到對(duì)線程安全不好做一個(gè)定義,我的簡(jiǎn)單理解就是:線程安全就是指程序按照你的代碼邏輯執(zhí)行,并始終輸出預(yù)定的結(jié)果。書中的給的定義:當(dāng)多個(gè)線程訪問某個(gè)類時(shí),這個(gè)類始終都能表現(xiàn)出正確的行為,那么就稱這個(gè)類是線程安全的。具體有關(guān)線程安全的問題,例如原子性、可見性等等不在這里做詳細(xì)闡述,適當(dāng)?shù)臅r(shí)候會(huì)進(jìn)行詳細(xì)介紹,簡(jiǎn)單說一點(diǎn),想要這個(gè)線程安全,得在訪問的時(shí)候給它上個(gè)鎖,不讓其他線程訪問,當(dāng)然這種說法不嚴(yán)謹(jǐn),不過可以暫時(shí)這么理解。
以上是從基本概念理論出發(fā)來大致了解需要知道的一些概念,下面就針對(duì)JDK中有關(guān)線程的API來對(duì)多線程并發(fā)編程做一個(gè)了解。
java.lang.Object -public void notify()//喚醒這個(gè)對(duì)象監(jiān)視器正在等待獲取鎖的一個(gè)線程 -public void notifyAll()//喚醒這個(gè)對(duì)象監(jiān)視器上正在等待獲取鎖的所有線程 -public void wait()//導(dǎo)致當(dāng)前線程等待另一個(gè)線程調(diào)用notify()或notifyAll() -public void wait(long timeout)// 導(dǎo)致當(dāng)前線程等待另一個(gè)線程調(diào)用notify()或notifyAll(),或者達(dá)到timeout時(shí)間 -public void wait(long timeout, int nanos)//與上個(gè)方法相同,只是將時(shí)間控制到了納秒nanos
我們先用一個(gè)經(jīng)典的例子——生產(chǎn)者消費(fèi)者問題來說明上面的API是如何使用的。生產(chǎn)者消費(fèi)者問題指的的是,生產(chǎn)者生產(chǎn)產(chǎn)品到倉(cāng)庫(kù)里,消費(fèi)者從倉(cāng)庫(kù)中拿,倉(cāng)庫(kù)滿時(shí)生產(chǎn)者不能繼續(xù)生產(chǎn),倉(cāng)庫(kù)為空時(shí)消費(fèi)者不能繼續(xù)消費(fèi)。轉(zhuǎn)化成程序語言也就是生產(chǎn)者是一個(gè)線程
1,消費(fèi)者是線程
2,倉(cāng)庫(kù)是一個(gè)隊(duì)列,線程1往隊(duì)尾中新增,線程2從隊(duì)首中移除,隊(duì)列滿時(shí)線程1不能再新增,隊(duì)列空時(shí)線程2不能再移除。
package com.producerconsumer; import java.util.Queue; /** * 生產(chǎn)者 * Created by yulinfeng on 2017/5/11. */ public class Producer implements Runnable{ private final Queue<String> queue; private final int maxSize; public Producer(Queue<String> queue, int maxSize) { this.queue = queue; this.maxSize = maxSize; } public void run() { produce(); } /** * 生產(chǎn) */ private void produce() { try { while (true) { synchronized (queue) { if (queue.size() == maxSize) { System.out.println("生產(chǎn)者:倉(cāng)庫(kù)滿了,等待消費(fèi)者消費(fèi)"); queue.wait(); } System.out.println("生產(chǎn)者:" + queue.add("product")); queue.notifyAll(); } } } catch (InterruptedException e) { e.printStackTrace(); } } } package com.producerconsumer; import java.util.Queue; /** * 消費(fèi)者 * Created by yulinfeng on 2017/5/11. */ public class Consumer implements Runnable { private final Queue<String> queue; public Consumer(Queue<String> queue) { this.queue = queue; } public void run() { consume(); } /** * 消費(fèi) */ private void consume() { synchronized (queue) { try { while (true) { if (queue.isEmpty()) { System.out.println("消費(fèi)者:倉(cāng)庫(kù)空了,等待生產(chǎn)者生產(chǎn)"); queue.wait(); } System.out.println("消費(fèi)者:" + queue.remove()); queue.notifyAll(); } } catch (InterruptedException e) { e.printStackTrace(); } } } } package com.producerconsumer; import java.util.LinkedList; import java.util.Queue; /** * Created by yulinfeng on 2017/5/11. */ public class Main { public static void main(String[] args) { Queue<String> queue = new LinkedList<String>(); int maxSize = 100; Thread producer = new Thread(new Producer(queue, maxSize)); Thread consumer = new Thread(new Consumer(queue)); producer.start(); consumer.start(); } }
這個(gè)生產(chǎn)者消費(fèi)者問題的實(shí)現(xiàn),我采用線程不安全的LinkedList,使用內(nèi)置鎖synchronized來保證線程安全,在這里我們不討論synchronized,主要談notify()、notifyAll()和wait()。
在這里例子中,作為生產(chǎn)者,當(dāng)隊(duì)列滿時(shí)調(diào)用了隊(duì)列的wait()方法,表示等待,并且此時(shí)釋放了鎖。作為消費(fèi)者此時(shí)獲取到鎖并且移除隊(duì)首元素時(shí)調(diào)用了notifyAll()方法,此時(shí)生產(chǎn)者由wait等待狀態(tài)轉(zhuǎn)換為喚醒狀態(tài),但注意!此時(shí)僅僅是線程被喚醒了,有了爭(zhēng)奪CPU資源的資格,并不代表下一步就一定是生產(chǎn)者生產(chǎn),還有可能消費(fèi)者繼續(xù)爭(zhēng)奪了CPU資源。一定記住是被喚醒了,有資格爭(zhēng)奪CPU資源。notifyAll()表示的是喚醒所有等待的線程,所有等待的線程被喚醒過后,都有了爭(zhēng)奪CPU資源的權(quán)利,至于是誰會(huì)獲得這個(gè)鎖,那不一定。而如果是使用notify(),那就代表喚醒所有等待線程中的一個(gè),只是一個(gè)被喚醒具有了爭(zhēng)奪CPU的權(quán)力,其他沒被喚醒的線程繼續(xù)等待。如果等待線程就只有一個(gè)那么notify()和notifyAll()就沒區(qū)別,不止一個(gè)那區(qū)別就大了,一個(gè)是只喚醒其中一個(gè),一個(gè)是喚醒所有。喚醒不是代表這個(gè)線程就一定獲得CPU資源一定獲得鎖,而是有了爭(zhēng)奪的權(quán)利。
java.lang.Thread -public void join() -public void sleep() -public static void yield() -……
針對(duì)Thread線程類,我們只說常見的幾個(gè)不容易理解的方法,其余方法不在這里做詳細(xì)闡述。
關(guān)于sleep()方法,可能很容易拿它和Object的wait方法作比較。兩個(gè)方法很重要的一點(diǎn)就是sleep不會(huì)釋放鎖,而wait會(huì)釋放鎖。在上面的生產(chǎn)者消費(fèi)者的生產(chǎn)或消費(fèi)過程中添加一行Thread.sleep(5000),你將會(huì)發(fā)現(xiàn)執(zhí)行到此處時(shí),這個(gè)跟程序都會(huì)暫停執(zhí)行5秒,不會(huì)有任何其他線程執(zhí)行,因?yàn)樗粫?huì)釋放鎖。
關(guān)于join()方法,JDK7的解釋是等待線程結(jié)束(Waits for this thread to die)似乎還是不好理解,我們?cè)趍ain函數(shù)中啟動(dòng)兩個(gè)線程,在啟動(dòng)完這兩個(gè)線程后main函數(shù)再執(zhí)行其他操作,但如果不加以限制,有可能main函數(shù)率先執(zhí)行完需要的操作,但如果在main函數(shù)中加入join方法,則表示阻塞等待這兩個(gè)線程執(zhí)行結(jié)束后再執(zhí)行main函數(shù)后的操作,例如:
package com.join; public class Main { public static void main(String[] args) throws Exception{ Thread t1 = new Thread(new Task(0)); Thread t2 = new Thread(new Task(0)); t1.start(); t2.start(); t1.join(); t2.join(); System.out.print("main結(jié)束"); } }
上面?zhèn)€例子如果沒有join方法,那么“main”結(jié)束這條輸出語句可能就會(huì)先于t1、t2,加上在啟動(dòng)線程的調(diào)用方使用了線程的join方法,則調(diào)用方則會(huì)阻塞線程執(zhí)行結(jié)束過后再執(zhí)行剩余的方法。
關(guān)于Thread.yield()方法,本來這個(gè)線程處于執(zhí)行狀態(tài),其他線程也想爭(zhēng)奪這個(gè)資源,突然,這個(gè)線程不想執(zhí)行了想和大家一起來重新奪取CPU資源。所以Thread.yield也稱讓步。從下一章開始就正式開始了解java.util.concurrent。
以上這篇基于線程、并發(fā)的基本概念(詳解)就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
使用Java的Lucene搜索工具對(duì)檢索結(jié)果進(jìn)行分組和分頁
這篇文章主要介紹了使用Java的搜索工具Lucene對(duì)檢索結(jié)果進(jìn)行分組和分頁的方法,Luence是Java環(huán)境中的一個(gè)全文檢索引擎工具包,需要的朋友可以參考下2016-03-03SpringBoot+Redis Bitmap實(shí)現(xiàn)活躍用戶統(tǒng)計(jì)
Redis的Bitmap數(shù)據(jù)結(jié)構(gòu)是一種緊湊的位圖,它可以用于實(shí)現(xiàn)各種場(chǎng)景,其中統(tǒng)計(jì)活躍用戶是一種經(jīng)典的業(yè)務(wù)場(chǎng)景,下面我們就來學(xué)習(xí)一下SpringBoot如何利用Redis中的Bitmap實(shí)現(xiàn)活躍用戶統(tǒng)計(jì)吧2023-11-11MyBatis使用自定義TypeHandler轉(zhuǎn)換類型的實(shí)現(xiàn)方法
這篇文章主要介紹了MyBatis使用自定義TypeHandler轉(zhuǎn)換類型的實(shí)現(xiàn)方法,本文介紹使用TypeHandler 實(shí)現(xiàn)日期類型的轉(zhuǎn)換,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-10-10全排列算法-遞歸與字典序的實(shí)現(xiàn)方法(Java)
下面小編就為大家?guī)硪黄帕兴惴?遞歸與字典序的實(shí)現(xiàn)方法(Java) 。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-04-04基于Spring Boot DevTools實(shí)現(xiàn)開發(fā)過程優(yōu)化
這篇文章主要介紹了基于Spring Boot DevTools實(shí)現(xiàn)開發(fā)過程優(yōu)化,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09解決java讀取EXCEL數(shù)據(jù)變成科學(xué)計(jì)數(shù)法的問題
這篇文章主要介紹了解決java讀取EXCEL數(shù)據(jù)變成科學(xué)計(jì)數(shù)法的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-04-04Java?Chassis3負(fù)載均衡選擇器技術(shù)解密
這篇文章主要為大家介紹了Java?Chassis3負(fù)載均衡選擇器技術(shù)解密,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01Java中String的split切割字符串方法實(shí)例及擴(kuò)展
最近在項(xiàng)目中遇到一個(gè)小問題,一個(gè)字符串分割成一個(gè)數(shù)組,下面這篇文章主要給大家介紹了關(guān)于Java中String的split切割字符串方法的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06Mybatis order by 動(dòng)態(tài)傳參出現(xiàn)的問題及解決方法
今天,我正在愉快地CRUD,突然發(fā)現(xiàn)出現(xiàn)一個(gè)Bug,我們來看看是怎么回事吧!接下來通過本文給大家介紹Mybatis order by 動(dòng)態(tài)傳參出現(xiàn)的一個(gè)小bug,需要的朋友可以參考下2021-07-07