Java——多线程学习
參考文章:https://www.jb51.net/article/127443.htm
1、先來說下多線程的概念:
- 多線程:一個進程如果有多條執行路徑,則稱為多線程程序。
簡單來說就是原來是一條路徑執行(只有一個CPU的情況下),現在是多條路徑執行。就相當于一條公路,原來是一條路,為提高使用效率,充分使用這條道路,在路中間加了一些線,變成了多車道(仍是單向的)。
2、多線程的優點:
1、關于資源利用率更高:
- 假如一個應用程序需要從磁盤中讀取和處理文件。從磁盤讀取一個文件需要5秒,處理一個文件需要2秒。那么處理這樣的兩個文件則需要
- 從磁盤中讀取文件的時候,大部分的CPU時間用于等待磁盤去讀取數據。在這段時間里CPU處于空閑狀態。它可以做一些別的事情。通過改變操作的順序,就能夠更好的使用CPU資源。看下面的順序:
- CPU等待第一個文件被讀取完。然后開始讀取第二個文件。當第二文件在被讀取的時候(IO讀取,CPU處于空閑),CPU會去處理第一個文件。在等待磁盤讀取文件的時候,CPU大部分時間是空閑的。
總的說來,CPU能夠在等待IO的時候做一些其他的事情。這個不一定就是磁盤IO。它也可以是網絡的IO,或者用戶輸入。通常情況下,網絡和磁盤的IO比內存的IO慢的多。
2、程序設計更簡單:
- 在單線程應用程序中,如果你想編寫程序手動處理上面所提到的讀取和處理的順序,你必須記錄每個文件讀取和處理的狀態。
- 相反,你可以啟動兩個線程,每個線程分別運行一個文件的讀取和處理。線程會在等待磁盤讀取文件的過程中被阻塞。在等待的時候,其他的線程能夠使用CPU去處理已經讀取完的文件。
- 其結果就是,磁盤總是在繁忙地讀取不同的文件到內存中。這會帶來磁盤和CPU利用率的提升。而且每個線程只需要記錄一個文件,因此這種方式也很容易編程實現。
3、 程序響應更快:
- 如果一個請求需要占用大量的時間來處理,在這段時間內客戶端就無法發送新的請求給服務端。只有服務器在監聽的時候,請求才能被接收。
- 另一種設計是,監聽線程把請求傳遞給工作者線程(worker thread),然后立刻返回去監聽。而工作者線程則能夠處理這個請求并發送一個回復給客戶端。
- 這樣就大大提高了程序的響應速度。
以下是一個多線程實現買票的例子:
1、案例需求:
- 某電影院目前正在上映國產大片,共有100張票,而它有3個窗口賣票,請設計一個程序模擬該電影院賣票。
實現步驟:
代碼實現:
public class SellTicket implements Runnable {private int tickets = 100;//在SellTicket類中重寫run()方法實現賣票,代碼步驟如下@Overridepublic void run() {while (true) {if (tickets > 0) {System.out.println(Thread.currentThread().getName() + "正在出售第" +tickets + "張票");tickets--;}}} } public class SellTicketDemo {public static void main(String[] args) {//創建SellTicket類的對象SellTicket st = new SellTicket();//創建三個Thread類的對象,把SellTicket對象作為構造方法的參數,并給出對應的窗口名稱Thread t1 = new Thread(st,"窗口1");Thread t2 = new Thread(st,"窗口2");Thread t3 = new Thread(st,"窗口3");//啟動線程t1.start();t2.start();t3.start();} }執行結果:
可以看到票數出現了異常,出現了重復的票,甚至還有可能出現負數的票。
2、賣票案例的問題:
問題產生原因:
- 線程執行的隨機性導致的
詳細解析:
相同的票出現了多次:
public class SellTicket implements Runnable {private int tickets = 100;@Overridepublic void run() {while (true) {//tickets = 100;//t1,t2,t3//假設t1線程搶到CPU的執行權if (tickets > 0) {//通過sleep()方法來模擬出票時間try {Thread.sleep(100);//t1線程休息100毫秒//t2線程搶到了CPU的執行權,t2線程就開始執行,執行到這里的時候,t2線程休息100毫秒//t3線程搶到了CPU的執行權,t3線程就開始執行,執行到這里的時候,t3線程休息100毫秒} catch (InterruptedException e) {e.printStackTrace();}//假設線程按照順序醒過來//t1搶到CPU的執行權,在控制臺輸出:窗口1正在出售第100張票System.out.println(Thread.currentThread().getName() + "正在出售第"+tickets + "張票");//t2搶到CPU的執行權,在控制臺輸出:窗口2正在出售第100張票//t3搶到CPU的執行權,在控制臺輸出:窗口3正在出售第100張票tickets--;//如果這三個線程還是按照順序來,這里就執行了3次--的操作,最終票就變成了97}}} }出現了負數的票:
public class SellTicket implements Runnable {private int tickets = 100;public void run() {while (true) {//tickets = 1;//t1,t2,t3//假設t1線程搶到CPU的執行權if (tickets > 0) {//通過sleep()方法來模擬出票時間try {Thread.sleep(100);//t1線程休息100毫秒//t2線程搶到了CPU的執行權,t2線程就開始執行,執行到這里的時候,t2線程休息100毫秒//t3線程搶到了CPU的執行權,t3線程就開始執行,執行到這里的時候,t3線程休息100毫秒} catch (InterruptedException e) {e.printStackTrace();}//假設線程按照順序醒過來//t1搶到了CPU的執行權,在控制臺輸出:窗口1正在出售第1張票//假設t1繼續擁有CPU的執行權,就會執行tickets--;操作,tickets = 0;//t2搶到了CPU的執行權,在控制臺輸出:窗口1正在出售第0張票//假設t2繼續擁有CPU的執行權,就會執行tickets--;操作,tickets = -1;//t3搶到了CPU的執行權,在控制臺輸出:窗口3正在出售第-1張票//假設t2繼續擁有CPU的執行權,就會執行tickets--;操作,tickets = -2;System.out.println(Thread.currentThread().getName() + "正在出售第" +tickets + "張票");tickets--;}}} }3、同步代碼塊解決數據安全問題:
安全問題出現的條件:
如何解決多線程安全問題呢?
- 基本思想:讓程序沒有安全問題的環境
如何實現?
- 把多條語句操作共享數據的代碼給鎖起來,讓任意時刻只能有一個線程執行即可。
Java提供了同步代碼塊的方式來解決:
同步代碼塊格式:
synchronized(任意對象) {多條語句操作共享數據的代碼 }synchronized(任意對象):就相當于給代碼加鎖了,任意對象就可以看成是一把鎖。
同步的好處和弊端:
- 好處:解決了多線程的數據安全問題
- 弊端:當線程很多時,因為每個線程都會去判斷同步上的鎖,這是很耗費資源的,無形中會降低程序的運行效率。
代碼演示:
package sellticket;public class SellTicket implements Runnable {private int tickets = 100;private Object obj = new Object();//使用同一個對象加鎖,相當于使用同一把鎖@Overridepublic void run() {while (true) {//tickets = 100;//t1,t2,t3//假設t1搶到了CPU的執行權//假設t2搶到了CPU的執行權synchronized (obj) {//t1進來后,就會把這段代碼給鎖起來if (tickets > 0) {try {Thread.sleep(100);//t1休息100毫秒} catch (InterruptedException e) {e.printStackTrace();}//窗口1正在出售第100張票System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + " 張票");tickets--; //tickets = 99;}}//t1出來了,這段代碼的鎖就被釋放了}} } public class SellTicketDemo {public static void main(String[] args) {SellTicket st = new SellTicket();Thread t1 = new Thread(st, "窗口1");Thread t2 = new Thread(st, "窗口2");Thread t3 = new Thread(st, "窗口3");t1.start();t2.start();t3.start();} }4、同步方法解決數據安全問題:
同步方法:就是把synchronized關鍵字加到方法上。
修飾符 synchronized 返回值類型 方法名(方法參數) {方法體; }同步方法的鎖對象是:this
靜態同步方法:
- 同步靜態方法:就是把synchronized關鍵字加到靜態方法上
例:
同步方法:
private synchronized void sellTicket() {if (tickets > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "張票");tickets--;}}靜態同步方法:
private static synchronized void sellTicket() {if (tickets > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "正在出售第" +tickets + "張票");tickets--;}} }5、Lock鎖:
雖然我們可以理解同步代碼塊和同步方法的鎖對象問題,但是我們并沒有直接看到在哪里加上了鎖,在哪里釋放了鎖,為了更清晰的表達如何加鎖和釋放鎖,JDK5以后提供了一個新的鎖對象Lock
- Lock是接口不能直接實例化,采用它的實現類ReentrantLock來實例化。
ReentrantLock構造方法:
加鎖解鎖方法:
例:
關于java多線程的總結暫時就這些了。
總結
以上是生活随笔為你收集整理的Java——多线程学习的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java SE 重点知识笔记
- 下一篇: 我的第一个Spring MVC程序