Java中Lock鎖的使用、死鎖問題、多線程生產者和消費者、線程池、匿名內部類使用多線程、定時器、面試題

Lock鎖的使用

雖然我們可以理解同步代碼塊和同步方法的鎖對象問題,但是我們并沒有直接看到在哪里加上了鎖,在哪里釋放了鎖,為了更清晰的表達如何加鎖和釋放鎖,JDK5以后提供了一個新的鎖對象Lock

  • Lock
    • void lock():獲取鎖
    • void unlock():釋放鎖
  • ReentrantLock:是Lock的實現類

那么我們就來用Lock鎖對象改進上篇中我們出售票的需求代碼

public class SellTicket implements Runnable { 
// 定義票 
private int tickets = 100;
 // 定義鎖對象 
private Lock lock = new ReentrantLock(); 
@Override 
public void run() { 
while (true) { 
try { 
// 加鎖 
lock.lock(); 
if (tickets > 0) {
 try { 
Thread.sleep(100); 
} catch (InterruptedException e) { 
e.printStackTrace(); 
} 
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "張票"); } } finally { 
// 釋放鎖 lock.unlock();
 }
 } 
}
}
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(); 
}
}

運行程序,我們同樣可以得到一樣的結果,但是我們更清楚的看到在哪里加上了鎖,在哪里釋放了鎖。

  • 死鎖問題
    • 同步弊端
      • 效率低
      • 如果出現了同步嵌套,就容易產生死鎖問題
    • 死鎖問題及其代碼
      • 是指兩個或者兩個以上的線程在執行的過程中,因爭奪資源產生的一種互相等待現象
public class MyLock { 
// 創建兩把鎖對象
 public static final Object objA = new Object();
 public static final Object objB = new Object();
}
public class DieLock extends Thread { 
private boolean flag; 
public DieLock(boolean flag) {
 this.flag = flag; 
} 
@Override 
public void run() {
 if (flag) { 
synchronized (MyLock.objA) { 
System.out.println("if objA"); 
synchronized (MyLock.objB) { 
System.out.println("if objB");
 } 
} 
} else { 
synchronized (MyLock.objB) {
 System.out.println("else objB");
 synchronized (MyLock.objA) {
 System.out.println("else objA"); 
} 
} 
} 
}
}
public class DieLockDemo { 
public static void main(String[] args) { 
DieLock dl1 = new DieLock(true); 
DieLock dl2 = new DieLock(false);
 dl1.start(); 
dl2.start(); 
}
}

運行程序
這里寫圖片描述

可以看到這兩個線程在爭奪資源時,發生了一種互相等待的現象,這就是死鎖。在我們開發中,我們應該盡量避免死鎖的發生。

多線程生產者和消費者問題

什么是生產者和消費者
簡單來說就是生產一個,消費一個,具體點就是

- 生產者
    - 先看是否有數據,有就等待;沒有就生產,生產完成之后通知消費者來消費數據
- 消費者
    - 先看是否有數據,有就消費;沒有就等待,通知生產者生產數據

為了處理這樣的問題,java提供了一種機制,等待喚醒機制。

我們用代碼來演示

我們先創建以下類
- 資源類:Student
- 設置學生數據:SetThread(生產者)
- 獲取學生數據:GetThread(消費者)
- 測試類:StudentDemo

/* * 資源類:Student */
public class Student { 
String name; 
int age; 
boolean flag; // 默認情況是沒有數據,如果是true,說明有數據
}
/* * 設置學生數據 生產者 */
public class SetThread implements Runnable { 
private Student s;
 private int x = 0; 
public SetThread(Student s) { this.s = s; }
 @Override
 public void run() { 
while (true) { 
synchronized (s) { 
//判斷有沒有
 if(s.flag){
 try { 
s.wait(); //t1等著,釋放鎖
 } catch (InterruptedException e) { 
e.printStackTrace();
 }
 }
 if (x % 2 == 0) {
 s.name = "阿杜";
 s.age = 27; 
} else { 
s.name = "杜鵬程"; 
s.age = 23; 
} 
x++;
 //x=1 //修改標記 
s.flag = true; 
//喚醒線程 
s.notify(); //喚醒t2,喚醒并不表示你立馬可以執行,必須還得搶CPU的執行權。
 }
 //t1有,或者t2有 
} 
}
}
/* * 獲取學生數據:消費者 */
public class GetThread implements Runnable { 
private Student s; 
public GetThread(Student s) { 
this.s = s;
 } 
@Override 
public void run() {
 while (true) { synchronized (s) { 
if(!s.flag){ 
try { 
s.wait(); //t2就等待了。立即釋放鎖。將來醒過來的時候,是從這里醒過來的時候
 } catch (InterruptedException e) { 
e.printStackTrace(); 
} 
} 
System.out.println(s.name + "---" + s.age); 
//阿杜---27 //杜鵬程---23 //修改標記 
s.flag = false; //喚醒線程 
s.notify(); //喚醒t1 
} 
} 
}
}
/* * 測試類 */
public class StudentDemo { 
public static void main(String[] args) { 
//創建資源 
Student s = new Student(); 
//設置和獲取的類 
SetThread st = new SetThread(s); 
GetThread gt = new GetThread(s); 
//線程類 
Thread t1 = new Thread(st); 
Thread t2 = new Thread(gt); 
//啟動線程
 t1.start();
 t2.start(); 
}
}

線程池

程序啟動一個新線程成本是比較高的,因為它涉及到要與操作系統進行交互。而使用線程池可以很好的提高性能,尤其是當程序中要創建大量生存期很短的線程時,更應該考慮使用線程池。
線程池的好處:線程池里的每一個線程代碼結束后,并不會死亡,而是再次回到線程池中成為空閑狀態,等待下一個對象來使用。

JDK5新增了一個Executors工廠類來產生線程池,有如下幾個方法:

- public static ExecutorService newCachedThreadPool():創建一個具有緩存功能的線程池。緩存:百度瀏覽過的信息再次訪問
- public static ExecutorService newFixedThreadPool(int nThreads):創建一個可重用的,具有固定線程數的線程池
- public static ExecutorService newSingleThreadExecutor():創建一個只有單線程的線程池,相當于上個方法的參數是1

下面我們就來實現一個線程的代碼,我們先來分析一波實現的步驟

  • 創建一個線程池對象,控制要創建幾個線程對象。
    • public static ExecutorService newFixedThreadPool(int nThreads)
  • 這種線程池的線程可以執行:
    • 可以執行Runnable對象或者Callable對象代表的線程
    • 做一個類實現Runnable接口。
  • 調用如下方法即可
    • Future < ?> submit(Runnable task)
    • < T> Future < T> submit(Callable task)
  • 可以結束該線程
public class MyRunnable implements Runnable { 
@Override
 public void run() { 
for (int x = 0; x < 100; x++) { System.out.println(Thread.currentThread().getName() + ":" + x); 
} 
}
}
public class ExecutorsDemo { 
public static void main(String[] args) { 
// 創建一個線程池對象,控制要創建幾個線程對象。 
// public static ExecutorService newFixedThreadPool(int nThreads) ExecutorService pool = Executors.newFixedThreadPool(2); 
// 可以執行Runnable對象或者Callable對象代表的線程 
pool.submit(new MyRunnable()); 
pool.submit(new MyRunnable()); 
//結束線程池
 pool.shutdown(); 
}
}

這樣我們就運用線程池開啟了一個線程

匿名內部類使用多線程

- 匿名內部類方式使用多線程
    - new Thread(){代碼…}.start();
    - new Thread(new Runnable(){代碼…}).start();
public class ThreadDemo { 
public static void main(String[] args) { 
// 繼承Thread類來實現多線程 
new Thread() { 
public void run() { 
for (int x = 0; x < 100; x++) { System.out.println(Thread.currentThread().getName() + ":" + x);
 } 
}
 }.start(); 
// 實現Runnable接口來實現多線程 
new Thread(new Runnable() { 
@Override 
public void run() { 
for (int x = 0; x < 100; x++) { System.out.println(Thread.currentThread().getName() + ":"+ x);
 }
 }
 }) { 
}.start();
 }
}

定時器

定時器是一個應用十分廣泛的線程工具,可用于調度多個定時任務以后臺線程的方式執行。在Java中,可以通過Timer和TimerTask類來實現定義調度的功能

- Timer定時
   - public Timer()
   - public void schedule(TimerTask task, long delay)
   - public void schedule(TimerTask task,long delay,long period)
- TimerTask:任務
public class TimerDemo { 
 public static void main(String[] args) {
 // 創建定時器對象
 Timer t = new Timer();
 // 3秒后執行爆炸任務
 // t.schedule(new MyTask(), 3000);
 // 3秒后執行爆炸任務并結束任務
 t.schedule(new MyTask(t), 3000);
 }
}
// 做一個任務
class MyTask extends TimerTask {
 private Timer t;
 public MyTask(){}
 public MyTask(Timer t){
 this.t = t;
 }
 @Override
 public void run() {
 System.out.println("蹦,爆炸了"); 
t.cancel();//取消任務
 }
}

我們實現了3秒后爆炸并結束任務的代碼,也可以實現連環炸,就是3秒后爆炸,然后間隔幾秒又接著炸,實現起來也很簡單

public class TimerDemo2 { 
public static void main(String[] args) { 
// 創建定時器對象
Timer t = new Timer();
// 3秒后執行爆炸任務第一次,如果不成功,每隔2秒再繼續炸
t.schedule(new MyTask2(), 3000, 2000); 
}
}
// 做一個任務
class MyTask2 extends TimerTask { 
@Override
public void run() { 
System.out.println("beng,爆炸了");
}
}

面試題

我們來總結一下多線程這塊常見的面試題

  • 啟動一個線程是run()還是start()?它們的區別?

    啟動一個線程是start();
    - run():封裝了被線程執行的代碼,直接調用僅僅是普通方法的調用
    - start():啟動線程,并由JVM自動調用run()方法

  • sleep()和wait()方法的區別?

    • sleep():必須指時間;不釋放鎖。
  • wait():可以不指定時間,也可以指定時間;釋放鎖。

  • 為什么wait(),notify(),notifyAll()等方法都定義在Object類中?

因為這些方法的調用是依賴于鎖對象的,而同步代碼塊的鎖對象是任意鎖。
而Object代碼任意的對象,所以,定義在這里面。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,767評論 18 399
  • 本文主要講了java中多線程的使用方法、線程同步、線程數據傳遞、線程狀態及相應的一些線程函數用法、概述等。 首先講...
    李欣陽閱讀 2,503評論 1 15
  • Java多線程學習 [-] 一擴展javalangThread類 二實現javalangRunnable接口 三T...
    影馳閱讀 2,987評論 1 18
  • (一)Java部分 1、列舉出JAVA中6個比較常用的包【天威誠信面試題】 【參考答案】 java.lang;ja...
    獨云閱讀 7,142評論 0 62
  • 文章來源:http://www.54tianzhisheng.cn/2017/06/04/Java-Thread/...
    beneke閱讀 1,529評論 0 1