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代碼任意的對象,所以,定義在這里面。