Java学习笔记5-2——多线程
目錄
- 線程同步
- 三大不安全案例
- 一、不安全的買票過程
- 二、不安全的取錢過程
- 三、線程不安全的集合
- synchronized
- 解決三大不安全案例
- 一、解決不安全的買票過程
- 二、解決不安全的取錢過程
- 三、解決集合線程不安全
- 死鎖
- 可重入鎖(ReentrantLock)
- 線程協作(線程通信)
- 生產者消費者問題:
- 【重點理解】解決方法1:管程法
- 解決方法1:信號燈法
- 線程池
- 回顧總結線程的創建
線程同步
處理多線程問題時,多個線程訪問同一對象,并且某些線程還想修改這個對象。這時候就需要線程同步。線程同步其實就是一種等待機制,多個需要同時訪問此對象的線程進入這個對象的等待池形成隊列,等待前面線程使用完畢,下一個線程再使用。
- 形成條件:隊列+鎖
- 由于同一線程的多個線程共享同一塊存儲空間,在帶來方便的同時,也帶來了訪問沖突問題,為了保證數據在方法中被訪問的正確性,在訪問時加入鎖機制synchronized,當一個線程獲得對象的排他鎖,獨占資源,其他線程必須等待,使用后釋放鎖即可。
存在以下問題:
- 一個線程持有鎖會導致其他所有需要此鎖的線程掛起;
- 在多線程競爭下,加鎖,釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題;
- 如果一個優先級高的線程等待一個優先級低的線程釋放鎖,會導致優先級倒置,引起性能問題。
三大不安全案例
一、不安全的買票過程
不安全的情況下的例子:
//不安全的買票 public class UnsafeBuyTicket {public static void main(String[] args) {BuyTicket station = new BuyTicket();new Thread(station,"小明").start();new Thread(station,"老師").start();new Thread(station,"黃牛黨").start();} }class BuyTicket implements Runnable{//票private int ticketNums=10;boolean flag = true; // 外部停止方式@Overridepublic void run() {//買票while (flag){try {buy();} catch (InterruptedException e) {e.printStackTrace();}}}private void buy() throws InterruptedException {// 判斷是否有票if(ticketNums<=0){flag = false;return;}//模擬延時Thread.sleep(100);// 買票System.out.println(Thread.currentThread().getName()+"拿到"+ticketNums--);} }結果是余票數量到達負數或者同一張票被多個人同時買了。
二、不安全的取錢過程
不安全的情況下的例子:
// 不安全的取錢過程 // 兩個人去銀行取錢,操作同一賬戶 public class UnsafeBank {public static void main(String[] args) {// 賬戶Account account = new Account(100,"結婚基金");Drawing you = new Drawing(account,50,"你");Drawing wife = new Drawing(account,100,"老婆");you.start();wife.start();} }// 賬戶 class Account{int money;// 余額String name;//賬戶名public Account(int money, String name) {this.money = money;this.name = name;} }// 銀行:模擬取款 class Drawing extends Thread{Account account;// 賬戶int drawingMoney;// 取了多少錢int nowMoney;// 現在手里有多少錢public Drawing(Account account, int drawingMoney, String name) {super(name);// 線程的名字(看哪個線程取了錢)this.account = account;this.drawingMoney = drawingMoney;}// 取錢@Overridepublic void run() {//判斷有沒有錢if (account.money-drawingMoney<0){System.out.println(Thread.currentThread().getName()+"操作時賬戶錢不夠,取不了");return;}// 模擬延時 sleep可以放大問題的發生try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 卡內的錢=余額-取走的錢account.money = account.money - drawingMoney;//手里的錢nowMoney = nowMoney + drawingMoney;System.out.println(account.name+"余額為:"+account.money);//this.getName()即為Thread.currentThread().getName()System.out.println(this.getName()+"手里的錢:"+nowMoney);} }賬戶原有100w,你先取走50w,賬戶剩下50w;老婆此時卻看到賬戶剩100w,取走100w,結果實際賬戶剩-50w。
三、線程不安全的集合
//線程不安全的集合 public class UnsafeList {public static void main(String[] args) throws InterruptedException {List<String> list = new ArrayList<>();for (int i = 0; i < 50000; i++) {new Thread(()->{list.add(Thread.currentThread().getName());// 將五萬個線程分別添加進數組集合}).start();}Thread.sleep(6000);//等待6秒 讓其充分準備好,同時放大問題性System.out.println(list.size());} }可能會導致同一瞬間操作同一位置,把兩個或多個線程添加進同一位置,那個出錯位置的后來添加進來的線程會覆蓋掉先進來的,所以得到的list.size()少于50000
實際輸出結果:49999 如上所說,發生了錯誤
synchronized
由于我們可以通過private關鍵字來保證數據對象只能被方法訪問,所以我們只需要針對方法提出一套機制,這套機制就是synchronized關鍵字,它包括兩種用法:synchronized方法和synchronized塊。
- 同步方法:public synchronized void method(int args){ }
synchronized方法控制對“對象”的訪問,每個對象對應一把鎖,每個synchronized方法都必須獲得調用該方法的對象的鎖才能執行,否則線程會阻塞,方法一旦執行,就獨占該鎖,直到方法返回才釋放鎖,后面被阻塞的線程才能獲得這個鎖,繼續執行。
- 缺陷:若將一個大的方法聲明為synchronized將會影響效率
- 注意:方法里面有內容需要修改才需要鎖。 鎖得太多會浪費資源,降低效率。比如代碼A對對象的操作是只讀,代碼B對對象的操作是修改,所以代碼B才需要同步,代碼A不需要。
- synchronized鎖的是this
解決三大不安全案例
一、解決不安全的買票過程
public class UnsafeBuyTicket {public static void main(String[] args) {BuyTicket station = new BuyTicket();new Thread(station,"小明").start();new Thread(station,"老師").start();new Thread(station,"黃牛黨").start();} }class BuyTicket implements Runnable{//票private int ticketNums=10;boolean flag = true; // 外部停止方式@Overridepublic void run() {//買票while (flag){try {//模擬延時Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}buy();}}// synchronized 使其變成同步方法。用這個方法的對象都會拿到一把鎖private synchronized void buy() {// 判斷是否有票if(ticketNums<=0){flag = false;return;}// 買票System.out.println(Thread.currentThread().getName()+"拿到"+ticketNums--);} }輸出結果:每個線程有序執行
老師拿到10
黃牛黨拿到9
小明拿到8
小明拿到7
老師拿到6
黃牛黨拿到5
黃牛黨拿到4
老師拿到3
小明拿到2
黃牛黨拿到1
二、解決不安全的取錢過程
同步塊: synchronized(Obj ){ }
- Obj稱為同步監視器
- Obj可以是任何對象,但是推薦使用共享資源作為同步監視器
- 同步方法中無需指定同步監視器,因為同步方法的同步監視器就是this這個對象本身,或者是class(在反射中再講解)
- 同步監視器的執行過程:
1.第一個線程訪問,鎖定同步監視器,執行其中代碼
2.第二個線程訪問,發現同步監視器被鎖定,無法訪問
3.第一個線程訪問完畢,解鎖同步監視器
4.第二個線程訪問,發現同步監視器沒有鎖,然后鎖定并訪問
輸出:
結婚基金余額為:50
你手里的錢:50
老婆操作時賬戶錢不夠,取不了
發現已成功解決。
- 為什么不能將重寫的run方法用synchronized修飾?
【答】因為synchronized鎖的是this,也就是說鎖了重寫的run方法。重寫的run方法是模擬銀行取出錢的過程,說白了就是把銀行鎖了,銀行并沒有發生修改行為,真正發生修改行為的是賬戶這個對象。又或者說這里不應該鎖整個方法,因為不能說整個方法都在修改,而應該鎖那個對象。所以要用synchronized塊針對account對象進行加鎖同步。也就是說要加鎖的是變化的量(增刪改)。
三、解決集合線程不安全
通過添加synchronized塊解決集合線程不安全
public class UnsafeList {public static void main(String[] args) throws InterruptedException {List<String> list = new ArrayList<>();for (int i = 0; i < 50000; i++) {new Thread(()->{synchronized(list) { //鎖住list對象,讓線程一個個排隊添加進去list.add(Thread.currentThread().getName());// 將五萬個線程分別添加進數組集合}}).start();}Thread.sleep(6000);//等待6秒 讓其充分準備好,同時放大問題性System.out.println(list.size());} }或者直接使用線程安全的集合[在java.util.concurrent(JUC)下的CopyOnWriteArrayList]:
//測試JUC安全類型的集合 public class TestJUC {public static void main(String[] args) {CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();for (int i = 0; i < 50000; i++) {new Thread(()->{list.add(Thread.currentThread().getName());// 將五萬個線程分別添加進數組集合}).start();}try {Thread.sleep(5000);//等待5秒 讓其充分準備好,同時放大問題性} catch (InterruptedException e) {e.printStackTrace();}System.out.println(list.size());} }同樣解決問題,因為這是別人已經寫好的,不用再添加synchronized塊。
死鎖
多個線程各自占有一些共享資源,并且互相等待其它線程占有的資源才能運行,而導致兩個或者多個線程都在等待對方釋放資源,都停止的情形。某一同步塊同時擁有“兩個以上對象的鎖”時,就可能會發生“死鎖”的問題。
死鎖例子:
// 死鎖:多個線程互相抱著對方需要的資源,然后形成僵持 public class DeadLock {public static void main(String[] args) {Makeup g1 = new Makeup(0,"白雪公主");Makeup g2 = new Makeup(1,"灰姑娘");g1.start();g2.start();} }// 口紅 class Lipstick{ }// 鏡子 class Mirror{ }class Makeup extends Thread{// 需要的資源只有一份,用static來保證只有一份static Lipstick lipstick = new Lipstick();static Mirror mirror = new Mirror();int chioce;String girlName;Makeup(int chioce,String girlName){this.chioce = chioce;this.girlName = girlName;}@Overridepublic void run() {// 化妝try {makeup();} catch (InterruptedException e) {e.printStackTrace();}}private void makeup() throws InterruptedException {if (chioce == 0) {synchronized (lipstick) {System.out.println(this.girlName + "獲得口紅的鎖");Thread.sleep(1000);synchronized (mirror) {System.out.println(this.girlName + "獲得鏡子的鎖");}}} else {synchronized (mirror) {System.out.println(this.girlName + "獲得鏡子的鎖");Thread.sleep(1000);synchronized (lipstick) {System.out.println(this.girlName + "獲得口紅的鎖");}}}} }結果:(兩者發生僵持)
白雪公主獲得口紅的鎖
灰姑娘獲得鏡子的鎖
改進:synchronized塊不嵌套,不包住對方即將要用的資源的鎖即可:
if (chioce == 0) {synchronized (lipstick) {System.out.println(this.girlName + "獲得口紅的鎖");Thread.sleep(1000);}synchronized (mirror) {System.out.println(this.girlName + "獲得鏡子的鎖");}} else {synchronized (mirror) {System.out.println(this.girlName + "獲得鏡子的鎖");Thread.sleep(1000);}synchronized (lipstick) {System.out.println(this.girlName + "獲得口紅的鎖");}}輸出:
白雪公主獲得口紅的鎖
灰姑娘獲得鏡子的鎖
白雪公主獲得鏡子的鎖
灰姑娘獲得口紅的鎖
【總結】產生死鎖的四個必要條件:
上面四個產生死鎖的必要條件中只要想辦法打破其中任意一個或多個條件就可以避免死鎖發生。
可重入鎖(ReentrantLock)
從JDK5.0開始,Java提供了更強大的線程同步機制——通過顯式定義同步鎖對象來實現同步。同步鎖使用Lock對象充當
java.util.concurrent.locks.Lock接口是控制多個線程對共享資源進行訪問的工具。鎖提供了對共享資源的獨占訪問,每次只能有一個線程對Lock對象加鎖,線程開始訪問共享資源之前應先獲得Lock對象。
ReentrantLock類實現了Lock,它擁有與synchronized相同的并發性和內存語義,在實現線程安全控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖。
class A {private final ReentrantLock lock = new ReentrantLock();public void m() {lock.lock();try {// 保證線程安全的代碼;} finally {lock.unlock();// 如果同步代碼有異常,要將unlock()寫入finally語句塊}} }synchronized與Lock的對比:
- Lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖),synchronized是隱式鎖,出了作用域自動釋放
- Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
- 使用Lock鎖,JVM將花費較少的時間來調度線程,性能更好。并且具有更好的擴展性(提供更多的子類)
- 優先使用順序:
Lock > 同步代碼塊(已經進入了方法體,分配了相應資源) > 同步方法(在方法體之外)
線程協作(線程通信)
生產者消費者問題:
(如何實現線程之間的通信,如何在某線程在工作的時候讓另一線程等待)
問題分析:
這是一個線程同步問題,生產者和消費者共享同一個資源,并且生產者和消費者之間相互依賴,互為條件。
- 對于生產者,生產出產品之前,要通知消費者等待,而生產了產品之后,又需要馬上通知消費者消費;
- 對于消費者,在消費之后,要通知生產者已經結束消費,需要生產新的產品以供消費;
- 在生產者消費者問題中,僅有synchronized是不夠的,
synchronized可阻止并發更新同一個共享資源,實現了同步,
synchronized不能用來實現不同線程之間的消息傳遞(通信)
Java提供了幾個方法解決線程之間的通信問題:
| wait() | 表示線程一直等待,直到其他線程通知,與sleep不同(sleep抱著鎖睡覺),wait會釋放鎖 |
| wait(long timeout) | 指定等待的毫秒數 |
| notify() | 喚醒一個處于等待狀態的線程 |
| notifyAll() | 喚醒同一個對象上所有調用wait()方法的線程,優先級別高的線程優先調度 |
以上均是Object類的方法,都只能在同步方法或者同步代碼塊中使用,否則會拋出IllegalMonitorStateException
【重點理解】解決方法1:管程法
并發協作模型“生產者/消費者模型”之管程法:
- 生產者:負責生產數據的模塊(可能是方法,對象,線程,進程);
- 消費者:負責處理數據的模塊(可能是方法,對象,線程,進程);
- 緩沖區:消費者不能直接使用生產者的數據,他們之間有個“緩沖區”
- 生產者將生產好的數據放入緩沖區,消費者從緩沖區拿出數據
解決方法1:信號燈法
并發協作模型“生產者/消費者模型”之信號燈法:
// 生產者消費者問題2 信號燈法(通過標志位解決) public class PnC2 {public static void main(String[] args) {TV tv = new TV();new Actor(tv).start();new Audience(tv).start();} }// 生產者——>演員 class Actor extends Thread{TV tv;public Actor(TV tv){this.tv = tv;}@Overridepublic void run() {for (int i = 0; i < 20; i++) {if (i%2 == 0){try {this.tv.play("唱歌");} catch (InterruptedException e) {e.printStackTrace();}}else{try {this.tv.play("跳舞");} catch (InterruptedException e) {e.printStackTrace();}}}} }// 消費者——>觀眾 class Audience extends Thread{TV tv;public Audience(TV tv){this.tv = tv;}@Overridepublic void run() {for (int i = 0; i < 20; i++) {try {tv.watch();} catch (InterruptedException e) {e.printStackTrace();}}} }// 產品——>錄播的節目 class TV{// 演員表演,觀眾等待 T// 觀眾觀看,演員等待 FString programme;//表演節目boolean flag = true;//表演public synchronized void play(String programme) throws InterruptedException {if (!flag)this.wait();System.out.println("演員表演了——>:"+programme);//通知觀眾觀看this.notifyAll();this.programme = programme;this.flag = !this.flag;}//觀看public synchronized void watch() throws InterruptedException {if (flag) this.wait();System.out.println("觀眾觀看了:"+programme);//通知演員表演this.notifyAll();this.flag = !this.flag;} }線程池
背景:
經常創建和銷毀、使用量特別大的資源,比如并發情況下的線程,對性能影響很大
思路:
提前創建好多個線程,放入線程池中,使用時直接獲取,使用完放回池中。可以避免頻繁創建銷毀、實現重復利用。類似生活中公共交通工具。
好處:
1.提高響應速度(減少了創建線程的時間)
2.降低資源的消耗(重復利用線程池中線程,不需要每次都創建)
3.便于線程管理
-
JDK 5.0起提供了線程池相關API:ExecutorService 和 Executors
-
ExecutorService:真正的線程池接口。常見子類ThreadPoolExecutor
-
Executors:工具類、線程池的工廠類、用于創建并返回不同類型的線程池
常用參數:
- corePoolSize:核心池的大小
- maximumPoolSize:最大線程數
- keepAliveTime:線程沒有任務時最多保持多長時間后會終止
常用方法:
- void execute(Runnable command):執行任務/命令,沒有返回值,一 般用來執行Runnable
- <T>Future<T> submit(Callable task):執行任務,有返回值,一般用來執行Callable
- void shutdown():關閉連接池
回顧總結線程的創建
//回顧總結線程的創建 public class Summary {public static void main(String[] args) {new MyThread1().start();new Thread(new MyThread2()).start();//FutureTask實現了一個RunnableFuture接口,RunnableFuture接口繼承了Runnable,實現了run方法FutureTask<Integer> futureTask = new FutureTask<>(new MyThread3());new Thread(futureTask).start();try {Integer integer = futureTask.get();System.out.println(integer);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}} }//1.繼承Thread類 class MyThread1 extends Thread {@Overridepublic void run() {System.out.println("MyThread1");} }//2.實現Runnable接口 class MyThread2 implements Runnable {@Overridepublic void run() {System.out.println("MyThread2");} }//3.實現Callable接口 class MyThread3 implements Callable<Integer> {@Overridepublic Integer call() throws Exception {System.out.println("MyThread3");return 100;} } 創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的Java学习笔记5-2——多线程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 小程序“自定义关键词”功能的常见问答
- 下一篇: [Java]集合的小抄 Java初学者必