Java——多线程使用详解
多線程:
- 多線程就是同時執行多個應用程序,需要硬件的支持
- 同時執行:不是某個時間段同時,cpu切換的比較快,所有用戶會感覺是在同時運行
并發與并行:
-
并行(parallel):指在同一時刻,有多條指令在多個處理器上同時執行。并行必須借助于多核cpu實現
-
并發(concurrency):指在同一時刻只能有一條指令執行,但多個進程指令被快速的輪換執行,使得在宏觀上具有多個進程同時執行的效果,但在微觀上并不是同時執行的,只是把時間分成若干段,通過cpu時間片輪轉使多個進程快速交替的執行。
程序、線程、進程:
程序:是指編譯好的可執行文件,程序啟動的時候,進程作為支撐
進程:是正在運行的程序(比如360殺毒軟件),進程可以產生多線程
獨立性:進程是一個能獨立運行的基本單位,同時也是系統分配資源和調度的獨立單位
動態性:進程的實質是程序的一次執行過程,進程是動態產生,動態消亡的
并發性:任何進程都可以同其他進程一起并發執行
進程基本的狀態有5種:分別為初始態、就緒態、運行態、掛起態、終止態其中初始態為進程準備階段,常與就緒態結合來看。
線程:是程序正在做的事情,線程是進程的單個控制流(比如360的殺毒,掃描木馬)
單線程:一個進程如果只有一條執行路徑,則稱為單線程程序
多線程:一個進程如果有多條執行路徑,則稱為多線程程序
進程與線程的區別:
- 進程:有獨立的內存空間,進程中的數據存放空間(堆空間和棧空間)是獨立的,至少有一個線程。
- 線程:堆空間是共享的,棧空間是獨立的,線程消耗的資源比進程小的多。
多線程同時執行原理:
比如我們同時運行qq和微信,其實不是同時運行的而是CPU在多個線程間快速切換,造成"同時"執行的假象
多線程的好處:
- 可以"同時"執行多個任務
- 可以提高資源的利用率(CPU/網絡)
線程越多越好嗎:
1.創建和銷毀線程需要消耗CPU和內存資源
2.線程太多,CPU需要在大量的線程間切換,造成資源的浪費
進程與并發:
在使用進程實現并發時會出現以下問題:
孤兒進程:
父進程比子進程先結束,子進程成為孤兒進程,子進程的父進程成為init進程,稱為init進程領養孤兒進程。
僵尸進程:
僵尸進程: 進程終止,父進程尚未回收,子進程殘留資源(PCB)存放于內核中,變成僵尸(Zombie)進程。
Windows&Linux進程:
Windows下的進程和Linux下的進程是不一樣的,它比較懶惰,從來不執行任何東西,只是為線程提供執行環境。然后由線程負責執行包含在進程的地址空間中的代碼。當創建一個進程的時候,操作系統會自動創建這個進程的第一個線程,成為主線程。
創建線程:
實現多線程有三種方法:
開啟線程
| void run() | 在線程開啟后,此方法將被調用執行 |
| void start() | 使此線程開始執行,Java虛擬機會調用run方法() |
為什么要重寫run()方法?
因為run()是用來封裝被線程執行的代碼
run()方法和start()方法的區別?
- run():封裝線程執行的代碼,直接調用,相當于普通方法的調用,并沒有開啟線程
- start():啟動線程;然后由JVM調用此線程的run()方法
繼承Thread:
實現步驟:
- 定義一個類MyThread繼承Thread類
- 在MyThread類中重寫run()方法
- 創建MyThread類的對象
- 啟動線程
多線程類:
public class ThreadDemo extends Thread {// run是用來封裝被線程執行的代碼的,一定要重寫@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println("線程啟動" + i);}} }測試類:
public class TestThread {public static void main(String[] args) {// 創建線程對象ThreadDemo thread1 = new ThreadDemo();ThreadDemo thread2 = new ThreadDemo();// 開啟線程thread1.start();thread2.start();} }實現Runnable:
實現步驟:
- 定義一個類MyRunnable實現Runnable接口
- 在MyRunnable類中重寫run()方法
- 創建MyRunnable類的對象
- 創建Thread類的對象,把MyRunnable對象作為構造方法的參數
- 啟動線程
對象類:
public class RunnableDemo implements Runnable{@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println("線程啟動" + i);}} }測試類:
public class TestRunnable {public static void main(String[] args) {// 創建參數對象RunnableDemo r1 = new RunnableDemo();RunnableDemo r2 = new RunnableDemo();// 創建線程Thread thread1 = new Thread(r1);Thread thread2 = new Thread(r2);// 開啟線程thread1.start();thread2.start();} }實現Callable接口:
實現步驟:
- 定義一個類MyCallable實現Callable接口
- 在MyCallable類中重寫call()方法
- 創建MyCallable類的對象
- 創建Future的實現類FutureTask對象,把MyCallable對象作為構造方法的參數
- 創建Thread類的對象,把FutureTask對象作為構造方法的參數
- 啟動線程
- 再調用get方法,就可以獲取線程結束之后的結果。
構造方法:
| Thread(Runnable target) | 分配一個新的Thread對象 |
| Thread(Runnable target, String name) | 分配一個新的Thread對象 |
對象類:
public class CallDemo implements Callable<String> {@Overridepublic String call() throws Exception {for (int i = 0; i < 10; i++) {System.out.println("線程執行" + i);}// 返回值表示運行完以后的結果return "執行完畢";} }測試類:
public class TestCall {public static void main(String[] args) throws ExecutionException, InterruptedException {// 創建對象,線程要執行的代碼CallDemo call = new CallDemo();// 獲取線程執行完畢的結果,可以作為參數傳給ThreadFutureTask<String> sft = new FutureTask<>(call);Thread thread = new Thread(sft);thread.start();// get:獲取線程運行后的結果,如果在線程沒開啟前就獲取get方法會一直等待,所以get方法要寫在start后面System.out.println(sft.get()); } }三種實現方式對比:
實現Runnable、Callable接口
好處: 擴展性強,實現該接口的同時還可以繼承其他的類
缺點: 編程相對復雜,不能直接使用Thread類中的方法
繼承Thread類
好處: 編程比較簡單,可以直接使用Thread類中的方法
缺點: 可以擴展性較差,不能再繼承其他的類
獲取與設置線程名稱:
| void setName(String name) | 將此線程的名稱更改為等于參數name |
| String getName() | 返回此線程的名稱 |
| Thread currentThread() | 返回對當前正在執行的線程對象的引用 |
對象類:
public class ThreadDemo extends Thread {// 要寫構造,否則不能傳線程名稱public ThreadDemo() {}public ThreadDemo(String name) {super(name);}@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName() + i);// 獲取當前線程對象}// 線程沒有設置名字的話,返回的是默認的名字,每個線程都是有默認名字的System.out.println("當前執行的線程是:" + Thread.currentThread().getName());} }測試類:
public class Test {public static void main(String[] args) {ThreadDemo t1 = new ThreadDemo();t1.start();t1.setName("線程一");// 通過構造設置,對象要創建有參構造ThreadDemo t2 = new ThreadDemo("線程二:");t2.start();// 如果是通過接口創建線程那么是不能用getname的,所以currentThread就可以代替了System.out.println(Thread.currentThread().getName());} }線程睡眠:
| static void sleep(long millis) | 使當前正在執行的線程停留(暫停執行)指定的毫秒數 |
對象類:
public class DemoRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 10; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + i);}} }測試類:
public class Test {public static void main(String[] args) {DemoRunnable dr = new DemoRunnable();Thread thread = new Thread(dr);thread.start();} }線程優先級:
- Java使用的是搶占式調度模型,線程優先級高只是槍戰CPU的幾率更大,不代表一定優先執行
| final int getPriority() | 返回此線程的優先級 |
| final void setPriority(int newPriority) | 更改此線程的優先級線程默認優先級是5;線程優先級的范圍是:1-10 |
守護線程:
| void setDaemon(boolean on) | 將此線程標記為守護線程,當運行的線程都是守護線程時,Java虛擬機將退出 |
線程一:
public class Thread1 extends Thread {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(getName() + " :" + i);}} }線程二:
public class Thread2 extends Thread {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName() + " :" + i);}} }測試類:
public class Test {public static void main(String[] args) {// 創建線程Thread1 t1 = new Thread1();Thread2 t2 = new Thread2();// 設置線程名字t1.setName("我女朋友");t2.setName("我");// 設置我女朋友為守護線程,我要掛了我女朋友也不能繼續活,這就是家庭地位// 因為此時守護線程還有執行權,cpu執行很快,所以守護線程不是馬上停止的,要把執行權走完t1.setDaemon(true);t1.start();t2.start();} }線程安全:
賣票案例分析線程安全:
重復票:定義三個線程去賣100張票,三個線程sleep后,第一個線程醒來拿到CPU執行權,此時將票減1,為99,剛減完線程二醒了。線程二拿到CPU執行權,此時將票減1,這時候是從99減1,為98,那這時候線程一也要變成98,因為執行的是同一個線程對象。剛減完線程三醒了。線程二拿到CPU執行權,此時將票減1,這時候是從98減1,為97…這時候就會出現重復票數
負數票:此時票數為1,線程1、2、3開始sleep,線程1醒來后,拿到CPU執行權,做減1操作,此時票數為0,線程1被銷毀。線程2醒過來,然后拿到CPU執行權,做減1操作,此時票數為-1,線程2被銷毀。線程3醒過來拿到CPU執行權,做減1操作,此時票數為-2,線程3被銷毀。所以就會出現負數票問題
安全問題出現的條件
如何解決多線程安全問題呢?
把多條語句操作共享數據的代碼給鎖起來,讓任意時刻只能有一個線程執行,Java提供了同步代碼塊的方式來解決,
例:兩個人同時上廁所,但是就一個馬桶,于是用戶A進入后,用戶B只能在外面等,只有用戶A出來,用戶B才能進去。
同步代碼塊:
synchronized(任意對象){ 多條語句操作共享數據的代碼 }// synchronized(任意對象):默認情況是打開的,只要有一個線程進去執行代碼,鎖就會關閉,當線程執行完出來,鎖才會自動打開同步代碼塊的好處和弊端:
好處:解決了多線程的數據安全問題
弊端:當線程很多時,每個線程都會去判斷同步上的鎖,很耗費資源,會降低程序的運行效率
對象類:
public class SellTicket implements Runnable {// 定義總票數private static int tickets = 100;// 創建一個任意的對象private final Object object = new Object();@Overridepublic void run() {while (true) {synchronized (object) {if (tickets > 0) {try {// 進來先睡一會Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "張票");tickets--;} else {break;}}}} }測試類:
public class SellTicketDemo {public static void main(String[] args) {// 創建對象SellTicket st = new SellTicket();// 創建線程并設置名字,多線程同一個對象Thread t1 = new Thread(st, "窗口一");Thread t2 = new Thread(st, "窗口二");// 啟動線程t1.start();t2.start();} }同步方法:
同步方法:鎖對象是:this
修飾符 synchronized 返回值類型 方法名(方法參數) {方法體;}對象類:
public class SellTicket2 implements Runnable {// 定義總票數private static int tickets = 100;// 創建一個任意的對象private final Object object = new Object();@Overridepublic void run() {while (true) {// 同步方法// 判斷當前線程,是就調用方法,然后判斷票數是不是0,不是就繼續循環if ("窗口一".equals(Thread.currentThread().getName())) {boolean result = synchronizedMethod();if (result) {break;}}// 同步代碼塊if ("窗口二".equals(Thread.currentThread().getName())) {// 因為同步方法的鎖是this,所以代碼塊也要是this才能都用一把鎖synchronized (this) {if (tickets > 0) {try {// 進來先睡一會Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "張票");tickets--;} else {break;}}}}}private synchronized boolean synchronizedMethod() {if (tickets > 0) {try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "張票");tickets--;// 不是最后一張就繼續循環return false;} else {// 最后一張就返回true,停止return true;}} }測試類:
public class SellTicketDemo {public static void main(String[] args) {SellTicket2 st = new SellTicket2();Thread t1 = new Thread(st, "窗口一");Thread t2 = new Thread(st, "窗口二");t1.start();t2.start();} }靜態同步方法:鎖對象是:類名.class
修飾符 static synchronized 返回值類型 方法名(方法參數) {方法體;}靜態方法在方法前加static,this換成類名.class就行了
Lock鎖:
為了更清晰的表達如何加鎖和釋放鎖,JDK5以后提供了一個新的鎖對象Lock
Lock是接口不能直接實例化,可以用它的實現類ReentrantLock來實例化
ReentrantLock構造方法
| ReentrantLock() | 創建一個ReentrantLock的實例 |
加鎖解鎖方法
| void lock() | 獲得鎖 |
| void unlock() | 釋放鎖 |
對象類:
public class Ticket implements Runnable {// 票的數量private int ticket = 100;// 創建Lock鎖private ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {while (true) {try {// 獲得鎖lock.lock();if (ticket == 0) {// 賣完了break;} else {Thread.sleep(30);ticket--;System.out.println(Thread.currentThread().getName() + "在賣票,還剩下" + ticket + "張票");}} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}} }測試類:
public static void main(String[] args) {Ticket ticket = new Ticket();Thread t1 = new Thread(ticket, "線程一");Thread t2 = new Thread(ticket, "線程二");// 設置優先級t1.setPriority(10);t2.setPriority(5);t1.start();t2.start();}死鎖:
線程死鎖是指由于兩個或者多個線程互相持有對方所需要的資源,導致這些線程處于等待狀態,無法前往執行
死鎖產生的條件:
1.有多個線程
2.有多把鎖
3.有同步代碼塊嵌套
解決辦法:
干掉其中某個條件
public class Demo05 {public static void main(String[] args) {MyRunnable mr = new MyRunnable();new Thread(mr).start();new Thread(mr).start();} }class MyRunnable implements Runnable {Object objA = new Object();Object objB = new Object();/*嵌套1 objA嵌套1 objB嵌套2 objB嵌套1 objA*/@Overridepublic void run() {synchronized (objA) {System.out.println("嵌套1 objA");synchronized (objB) {// t2, objA, 拿不到B鎖,等待System.out.println("嵌套1 objB");}}synchronized (objB) {System.out.println("嵌套2 objB");synchronized (objA) {// t1 , objB, 拿不到A鎖,等待System.out.println("嵌套2 objA");}}} }生產者&消費者:
生產和消費主要是包含了兩類線程:
生產和消費也稱為等待喚醒機制
生產者線程用于生產數據
消費者線程用于消費數據
實現:
為了解耦生產者和消費者的關系,通常會采用共享的數據區域,就像是一個倉庫
生產者生產數據之后直接放置在共享數據區中,并不需要關心消費者的行為
消費者只需要從共享數據區中去獲取數據,并不需要關心生產者的行為
Object類的等待和喚醒方法
| void wait() | 導致當前線程等待,直到另一個線程調用該對象的 notify()方法或 notifyAll()方法 |
| void notify() | 喚醒正在等待對象監視器的單個線程 |
| void notifyAll() | 喚醒正在等待對象監視器的所有線程 |
對象類:
public class Desk {// 定義一個標記判斷食物的狀態,為false表示沒有食物,true代表有食物private boolean flag;// 食物的個數private int count;// 鎖對象private final Object lock = new Object();public Desk() {}public Desk(boolean flag, int count) {this.flag = flag;this.count = count;}public boolean isFlag() {return flag;}public void setFlag(boolean flag) {this.flag = flag;}public int getCount() {return count;}public void setCount(int count) {this.count = count;}public Object getLock() {return lock;}@Overridepublic String toString() {return "Desk{" +"flag=" + flag +", count=" + count +", lock=" + lock +'}';} }生產者:
public class Cooker extends Thread {private Desk desk;public Cooker(Desk desk) {this.desk = desk;}@Overridepublic void run() {while (true) {synchronized (desk.getLock()) {if (desk.getCount() == 0) {break;} else if (!desk.isFlag()) {System.out.println("生產者正在生產食物");// 有食物就改狀態讓消費者去消費desk.setFlag(true);desk.getLock().notifyAll();} else {// 沒有食物就線程等待喚醒try {desk.getLock().wait();} catch (InterruptedException e) {e.printStackTrace();}}}}} }消費者:
public class Foodie extends Thread {private Desk desk;public Foodie(Desk desk) {this.desk = desk;}@Overridepublic void run() {while (true) {synchronized (desk.getLock()) {if (desk.getCount() == 0) {break;// 布爾類型變量的getset是is開頭的} else if (desk.isFlag()) {System.out.println("消費者消費了一個食物");// 消費完畢,將標記設為false,當為false時可以讓生產者去生產desk.setFlag(false);// 喚醒等待中的所有線程desk.getLock().notifyAll();// 消費一個減少一個desk.setCount(desk.getCount() - 1);} else {try {// 線程等待喚醒desk.getLock().wait();} catch (InterruptedException e) {e.printStackTrace();}}}}} }測試類:
public class Demo {public static void main(String[] args) {Desk desk = new Desk(false, 10);Foodie foodie = new Foodie(desk);Cooker cooker = new Cooker(desk);foodie.start();cooker.start();} }阻塞隊列:
常見BlockingQueue:
ArrayBlockingQueue:底層是數組,有界
LinkedBlockingQueue:底層是鏈表,無界.但不是真正的無界,最大為int的最大值
BlockingQueue的核心方法:
| put(anObject): | 將參數放入隊列,如果放不進去會阻塞 |
| take() | 取出第一個數據,取不到會阻塞 |
阻塞隊列等待&喚醒:
阻塞隊列底層是有自動加鎖的,但是運行起來,可能打印出的是存兩個打印兩個,這個是控制臺打印的問題。
生產者者線程
public class Cooker extends Thread {private ArrayBlockingQueue<String> bd;public Cooker(ArrayBlockingQueue<String> bd) {this.bd = bd;}@Overridepublic void run() {while (true) {try {bd.put("漢堡包");System.out.println("廚師放入一個漢堡包");} catch (InterruptedException e) {e.printStackTrace();}}} }消費者線程
private ArrayBlockingQueue<String> bd;public Foodie(ArrayBlockingQueue<String> bd) {this.bd = bd;}@Overridepublic void run() {while (true) {try {String take = bd.take();System.out.println("吃貨將" + take + "拿出來吃了");} catch (InterruptedException e) {e.printStackTrace();}}}測試類:
public class Demo {public static void main(String[] args) {// 創建阻塞隊列對象,容量為1ArrayBlockingQueue<String> bd = new ArrayBlockingQueue<>(1);Foodie f = new Foodie(bd);Cooker c = new Cooker(bd);f.start();c.start();}}總結
以上是生活随笔為你收集整理的Java——多线程使用详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 9月计算机一级报名入口,北京市2018年
- 下一篇: SpringMVC异常处理 自定义异常