一文精通Java 多線程之全方位解讀
并行和并發(fā)
并行:多個(gè)CPU實(shí)例或是多臺(tái)機(jī)器同時(shí)執(zhí)行一段處理邏輯,是真正的同時(shí)。
并發(fā):一個(gè)CPU或一臺(tái)機(jī)器,通過CPU調(diào)度算法,讓用戶看上去同時(shí)去執(zhí)行,實(shí)際上從CPU操作層面并不是真正的同時(shí)。并發(fā)往往需要公共的資源,對(duì)公共資源的處理和線程之間的協(xié)調(diào)是并發(fā)的難點(diǎn)。
線程基礎(chǔ)概念
線程和進(jìn)程
進(jìn)程就是程序,有獨(dú)立的運(yùn)行內(nèi)存空間,比如應(yīng)用和后臺(tái)服務(wù),windows是一個(gè)支持多進(jìn)程的操作系統(tǒng)。內(nèi)存越大能同時(shí)運(yùn)行的程序越多,在Java里一個(gè)進(jìn)程指的是一個(gè)運(yùn)行在獨(dú)立JVM的程序。
線程:一個(gè)程序里運(yùn)行的多個(gè)任務(wù),每個(gè)任務(wù)就是一個(gè)線程,線程是共享內(nèi)存的在QQ、微信、釘釘?shù)溶浖忻總€(gè)聊天窗口都是一個(gè)線程,可以同時(shí)接收消息和發(fā)送消息,但只有一個(gè)內(nèi)存占用。
多線程的好處
◆ 同時(shí)運(yùn)行多個(gè)任務(wù),提升CPU的使用效率
◆ 共享內(nèi)存,占用資源更少,線程間可以通信
◆ 異步調(diào)用,避免阻塞
◆ 用戶體驗(yàn)感更好
線程的狀態(tài)
線程包括5種狀態(tài):
1、新建(New):線程對(duì)象被創(chuàng)建時(shí),它只會(huì)短暫地處于這種狀態(tài)。此時(shí)它已經(jīng)分配了必須的系統(tǒng)資源,并執(zhí)行了初始化。例如,Thread thread = new Thread()。
2、就緒(Runnable):稱為“可執(zhí)行狀態(tài)”。線程對(duì)象被創(chuàng)建后,其它線程調(diào)用了該對(duì)象的start()方法,從而來啟動(dòng)該線程。例如,thread.start()。處于就緒狀態(tài)的線程,隨時(shí)可能被CPU調(diào)度執(zhí)行。
3、運(yùn)行(Running):線程獲取CPU權(quán)限進(jìn)行執(zhí)行。注意:線程只能從就緒狀態(tài)進(jìn)入運(yùn)行狀態(tài)。
4、阻塞(Blocked):阻塞狀態(tài)是線程因?yàn)槟撤N原因放棄CPU使用權(quán),暫時(shí)停止運(yùn)行。直到線程進(jìn)入就緒狀態(tài),才有機(jī)會(huì)轉(zhuǎn)到運(yùn)行狀態(tài)。阻塞的情況分為三種:
(1)等待阻塞:通過調(diào)用線程的wait()方法,讓線程等待某工作的完成。
(2)同步阻塞:線程在獲取synchronized同步鎖失?。ㄒ?yàn)殒i被其他線程占用),它會(huì)進(jìn)入同步阻塞狀態(tài)。
(3)其他阻塞:通過調(diào)用線程的sleep()或發(fā)出了I/O請(qǐng)求時(shí),線程會(huì)進(jìn)入到阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時(shí)、join()等待線程終止或是超時(shí)?;蚴荌/O處理完畢時(shí),線程重新轉(zhuǎn)入就緒狀態(tài)。
5.死亡(Dead):線程執(zhí)行完了或者因異常退出了run()方法,該線程結(jié)束生命周期。
實(shí)現(xiàn)多線程的兩種方式
繼承Thread類
第一步 繼承Therad父類
第二步 重寫run方法
第三步 實(shí)例化線程類
第四步 啟動(dòng)線程
public class ThreadDemo extends Thread{ /** * 繼承Thread * * @author gavin * */ @Override public void run() { // TODO 自動(dòng)生成的方法存根 //打印10次世界 try { for(int i=0;i<10;i++) { System.out.print("世界"); Thread.sleep(200);//阻塞態(tài) 休息200毫秒 } } catch (InterruptedException e) { // TODO 自動(dòng)生成的 catch 塊 e.printStackTrace(); } System.out.println("線程執(zhí)行完成!"); } public static void main(String[] args) { //初始化線程t1 t2 ThreadDemo t1 = new ThreadDemo(); ThreadDemo t2 = new ThreadDemo(); //啟動(dòng)線程 t1.start(); t2.start(); //注意不是調(diào)用run方法 而是啟動(dòng)線程 調(diào)用run方法只是執(zhí)行了run方法但不是多線程執(zhí)行 //打印200次你好 for(int i = 0;i<20;i++) { System.out.print("你好"); try { Thread.sleep(200); } catch (InterruptedException e) { // TODO 自動(dòng)生成的 catch 塊 e.printStackTrace(); } } //結(jié)束態(tài) System.out.println("程序運(yùn)行完成"); } }
注意:多線程每次運(yùn)行得到的結(jié)果是不相同的
Thread.sleep();方法可以使線程阻塞
調(diào)用start();方法才能啟動(dòng)多線程
調(diào)用run方法和普通方法效果相同
實(shí)現(xiàn)Runnable接口
第一步 實(shí)現(xiàn)Runnable接口
第二步 重寫run方法
第三步 實(shí)例化當(dāng)前類(任務(wù)類)
第四步 將任務(wù)類對(duì)象作為參數(shù)傳入Thread中進(jìn)行實(shí)列化
第五步 啟動(dòng)多線程
/** * 實(shí)現(xiàn)二: 實(shí)現(xiàn)Runnable接口 * 當(dāng)類本身有父類的時(shí)候 使用實(shí)現(xiàn)Runnable接口 * @author gavin * */ public class RunnableDemo implements Runnable { @Override public void run() { // TODO 自動(dòng)生成的方法存根 //打印10次世界 try { for(int i=0;i<10;i++) { System.out.print(Thread.currentThread().getName()+"世界\t"); if(i%3==0) { System.out.println(); } Thread.sleep(200);//阻塞態(tài) 休息200毫秒 } } catch (InterruptedException e) { // TODO 自動(dòng)生成的 catch 塊 e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"線程執(zhí)行完成!"); } public static void main(String[] args) { //實(shí)列化當(dāng)前類 RunnableDemo runnableDemo1 = new RunnableDemo(); //創(chuàng)建線程 傳入任務(wù)類 Thread t1 = new Thread(runnableDemo1); Thread t2 = new Thread(runnableDemo1); Thread t3 = new Thread(runnableDemo1); //設(shè)置線程優(yōu)先級(jí) t1.setPriority(Thread.MAX_PRIORITY);//最高優(yōu)先級(jí) t2.setPriority(Thread.MIN_PRIORITY);//最低優(yōu)先級(jí) t3.setPriority(5);//設(shè)置默認(rèn)優(yōu)先級(jí) 優(yōu)先級(jí)為5 優(yōu)先級(jí)從0-10最高為10 //啟動(dòng)線程 t1.start(); t2.start(); t3.start(); } }
注意 setPriority 可以設(shè)置線程的優(yōu)先級(jí)
但并不代表線程一定優(yōu)先執(zhí)行完
線程的安全性和原子性
由于線程之間可以共享內(nèi)存,則某個(gè)對(duì)象(變量)是可以被多個(gè)線程共享的,是可以被多個(gè)線程同時(shí)訪問的。當(dāng)多個(gè)線程訪問某個(gè)類時(shí),不管運(yùn)行時(shí)環(huán)境采用何種調(diào)度方式或者這些進(jìn)程將如何交替執(zhí)行,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同,這個(gè)類都能表現(xiàn)出正確的行為,那么就稱這個(gè)類是線程安全的。
舉個(gè)栗子:甲乙兩個(gè)人,甲負(fù)責(zé)向筐里放蘋果,乙負(fù)責(zé)從筐里數(shù)蘋果,甲乙同時(shí)進(jìn)行,問乙如何操作才能正確?
不幸的是,以上代碼不是線程安全的,因?yàn)閏ount++并非是原子操作,實(shí)際上,它包含了三個(gè)獨(dú)立的操作:讀取count的值,將值加1,然后將計(jì)算結(jié)果寫入count。如果線程A讀到count為10,馬上線程B讀到count也為10,線程A加1寫入后為11,線程B由于已經(jīng)讀過count值為10,執(zhí)行加1寫入后依然為11,這樣就丟失了一次計(jì)數(shù)。在并發(fā)編程中,這種由于不恰當(dāng)?shù)膱?zhí)行時(shí)序而出現(xiàn)不正確的結(jié)果是一種非常重要的情況,它有一個(gè)正式的名字:
競(jìng)態(tài)條件(Race Condition)。
Java 內(nèi)存模型中的可見性、原子性和有序性。
可見性:當(dāng)多個(gè)線程訪問同一個(gè)變量x時(shí),線程1修改了變量x的值,線程1、線程2…線程n能夠立即讀
取到線程1修改后的值。
有序性:即程序執(zhí)行時(shí)按照代碼書寫的先后順序執(zhí)行。在Java內(nèi)存模型中,允許編譯器和處理器對(duì)指
令進(jìn)行重排序,但是重排序過程不會(huì)影響到單線程程序的執(zhí)行,卻會(huì)影響到多線程并發(fā)執(zhí)行的正確性。
原子性:原子性通常指多個(gè)操作不存在只執(zhí)行一部分的情況,要么全部執(zhí)行、要么全部不執(zhí)行。
鎖的概念和使用
竟態(tài)條件會(huì)使運(yùn)行結(jié)果變得不可靠,程序的運(yùn)行結(jié)果取決于方法的調(diào)用順序,將方法以串行的
方式來訪問,我們稱這種方式為同步鎖(synchronized)。
Java實(shí)現(xiàn)同步鎖的方式有:
▶同步方法synchronized method
▶ 同步代碼塊 synchronized(Lock)
▶ 等待與喚醒 wait 和 notify
▶ 使用特殊域變量(volatile)實(shí)現(xiàn)線程同步
▶ 使用重入鎖實(shí)現(xiàn)線程同步ReentrantLock
▶ 使用局部變量實(shí)現(xiàn)線程同步ThreadLocal
synchronized
synchronized是Java 的內(nèi)置鎖機(jī)制,是在JVM上的
可以同步代碼塊,也可以同步方法
//同步代碼塊
synchronized(object){ }
//同步方法
public synchronized void method() { // do something }
注:同步是一種高開銷的操作,因此應(yīng)該盡量減少同步的內(nèi)容。通常沒有必要同步整個(gè)方法。
ReentrantLock
可重入鎖,是一種顯示鎖,在JavaSE5.0中新增了一個(gè)java.util.concurrent包來支持同步。
ReentrantLock類是可重入、互斥、實(shí)現(xiàn)了Lock接口的鎖,它與使用synchronized方法和快具有相同
的基本行為和語義,并且擴(kuò)展了其能力。
ReentrantLock() : 創(chuàng)建一個(gè)ReentrantLock實(shí)例
lock() : 獲得鎖
unlock() : 釋放鎖
可重入: 甲獲得鎖后釋放鎖或鎖失效,乙可繼續(xù)獲得這個(gè)鎖
生產(chǎn)消費(fèi)者模型
生產(chǎn)消費(fèi)者模型是一個(gè)非常典型的多線程并發(fā)處理的模型,在實(shí)際的生產(chǎn)應(yīng)用中也有非常廣泛的使用。
生產(chǎn)消費(fèi)者模型中的類–存儲(chǔ)類
import java.util.LinkedList; public class Store { /* * 存儲(chǔ) 隊(duì)列實(shí)現(xiàn) * * @author gavin * */ //創(chuàng)建隊(duì)列 LinkedList<Integer> list = new LinkedList<Integer>(); //設(shè)置最大存儲(chǔ)值 int max = 10; //生產(chǎn)者生產(chǎn) 放入隊(duì)尾 public void push(int n) { synchronized(list) { try { if(list.size()>=max){ System.out.println("存滿了"); //線程掛起 list.wait(); }else { //隊(duì)列沒有存滿 繼續(xù)存 System.out.println("存入:"+n); list.add(n); //放完之后必須有 因此喚醒取的線程 list.notifyAll(); } }catch (Exception e) { // TODO 自動(dòng)生成的 catch 塊 e.printStackTrace(); } } } //消費(fèi)者消費(fèi) 從隊(duì)頭取出 public int pop() { try { synchronized(list){ if(list.size()<=0) { System.out.println("隊(duì)列空了。。。。"); //空了之后就不能取了 因此線程掛起 list.wait(); }else { //從對(duì)頭取出 int n = list.poll(); System.out.println("取出:"+n); //取出了就一定不會(huì)滿 因此要喚醒線程 list.notifyAll(); return n; } } }catch (Exception e) { // TODO: handle exception } return 0; } }
生產(chǎn)消費(fèi)者模型中的類–生產(chǎn)者
/** * 生產(chǎn)者 */ public class Producer implements Runnable{ private Store store; public Producer(Store store) { // TODO 自動(dòng)生成的構(gòu)造函數(shù)存根 this.store = store; } @Override public void run() { // TODO 自動(dòng)生成的方法存根 try { //生產(chǎn)需要事件 休息100毫秒再生產(chǎn) Thread.sleep(100); //產(chǎn)生隨機(jī)數(shù)字 store.push((int)(Math.random()*100)); }catch (Exception e) { // TODO: handle exception } } }
生產(chǎn)消費(fèi)者模型中的類–消費(fèi)者
public class Customer implements Runnable{ private Store store; public Customer(Store store) { // TODO 自動(dòng)生成的構(gòu)造函數(shù)存根 this.store = store; } @Override public void run() { // TODO 自動(dòng)生成的方法存根 try { //消費(fèi)者消費(fèi)需要時(shí)間 休息200毫秒 Thread.sleep(200); //從隊(duì)頭取出 store.pop(); }catch (Exception e) { // TODO: handle exception } } }
測(cè)試類
package com.qingsu.pcm; public class TestPcm { public static void main(String[] args) { Store store = new Store(); while(true) { Producer producer = new Producer(store); Customer customer = new Customer(store); Thread t1 = new Thread(producer); Thread t2 = new Thread(customer); t1.start(); t2.start(); } } }
效果
volatile變量
volatile具有可見性、有序性,不具備原子性。
我們了解到synchronized是阻塞式同步,稱為重量級(jí)鎖。
而volatile是非阻塞式同步,稱為輕量級(jí)鎖。
被volatile修飾的變量能夠保證每個(gè)線程能夠獲取該變量的最新值,
從而避免出現(xiàn)數(shù)據(jù)臟讀的現(xiàn)象。
線程池的概念和使用
線程的創(chuàng)建是比較消耗內(nèi)存的,所以我們要事先創(chuàng)建若干個(gè)可執(zhí)行的線程放進(jìn)一個(gè)“池(容器)”里面,需要的時(shí)候就直接從池里面取出來不需要自己創(chuàng)建,使用完畢也不需要銷毀而是放進(jìn)“池”中,從而減少了創(chuàng)建和銷毀對(duì)象所產(chǎn)生的開銷。
ExecutorService:線程池接口
ExecutorService pool(池名稱) = Executors.常用線程池名;
常用線程池:
newsingleThreadExecutor :單個(gè)線程的線程池,即線程池中每次只有一個(gè)線程在工作,單線程串行執(zhí)行任務(wù)
newfixedThreadExecutor(n):固定數(shù)量的線程池,每提交一個(gè)任務(wù)就是一個(gè)線程,直到達(dá)到線程池的最大數(shù)量,然后在后面等待隊(duì)列前面的線程執(zhí)行或者銷毀()。該方式一般會(huì)使線程具有一定的執(zhí)行順序
newCacheThreadExecutor:一個(gè)可緩存的線程池。當(dāng)線程池超過了處理任務(wù)所需要的線程數(shù),那么就會(huì)回收部分閑置線程(一般是閑置60s)。當(dāng)有任務(wù)來時(shí)而線程不夠時(shí),線程池又會(huì)創(chuàng)建新的線程,當(dāng)線程夠時(shí)就調(diào)用池中線程。適
用于大量的耗時(shí)較少的線程任務(wù)。
newScheduleThreadExecutor:一個(gè)大小無限的線程池,該線程池多用于執(zhí)行延遲任務(wù)或者固定周期的任務(wù)。
示例
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestPcmTwo { /* * 利用線程池創(chuàng)建線程 */ public static void main(String[] args) { Store store = new Store(); //創(chuàng)建10個(gè)線程的線程池 ExecutorService service = Executors.newFixedThreadPool(10); ExecutorService serviceTwo = Executors.newFixedThreadPool(10); //創(chuàng)建可緩存的線程池 //可緩存的 //ExecutorService service = Executors.newCachedThreadPool(); //ExecutorService serviceTwo = Executors.newCachedThreadPool(); for(int i =0 ;i<11;i++) { service.execute(new Producer(store)); } for(int i =0 ;i<11;i++) { serviceTwo.execute(new Customer(store)); } //關(guān)閉線程池 service.shutdown(); serviceTwo.shutdown(); } }
到此這篇關(guān)于一文精通Java 多線程之全方位解讀的文章就介紹到這了,更多相關(guān)Java 多線程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringCloud微服務(wù)架構(gòu)實(shí)戰(zhàn)之微服務(wù)治理功能的實(shí)現(xiàn)
這篇文章主要介紹了SpringCloud微服務(wù)架構(gòu)實(shí)戰(zhàn)之微服務(wù)治理,這些治理工具主要包括服務(wù)的注冊(cè)與發(fā)現(xiàn)、負(fù)載均衡管理、動(dòng)態(tài)路由、服務(wù)降級(jí)和故障轉(zhuǎn)移、鏈路跟蹤、服務(wù)監(jiān)控等,需要的朋友可以參考下2022-02-02使用IDEA配置Tomcat和連接MySQL數(shù)據(jù)庫(JDBC)詳細(xì)步驟
這篇文章主要介紹了使用IDEA配置Tomcat和連接MySQL數(shù)據(jù)庫(JDBC)詳細(xì)步驟,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12Java將json對(duì)象轉(zhuǎn)換為map鍵值對(duì)案例詳解
這篇文章主要介紹了Java將json對(duì)象轉(zhuǎn)換為map鍵值對(duì)案例詳解,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-09-09MyBatis3源碼解析之如何獲取數(shù)據(jù)源詳解
用myBatis3與spring整合的時(shí)候,我們可以通過多種方式獲取數(shù)據(jù)源,下面這篇文章主要給大家介紹了關(guān)于MyBatis3源碼解析之如何獲取數(shù)據(jù)源的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06springboot 如何修改默認(rèn)端口及application.properties常用配置
這篇文章主要介紹了springboot 如何修改默認(rèn)端口及application.properties常用配置操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08使用SpringBoot編寫一個(gè)優(yōu)雅的單元測(cè)試
這篇文章主要為大家詳細(xì)介紹了如何使用SpringBoot編寫一個(gè)優(yōu)雅的單元測(cè)試,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-07-07Java list與set中contains()方法效率案例詳解
這篇文章主要介紹了Java list與set中contains()方法效率案例詳解,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08