一、問題引出
多個線程訪問同一個資源時,如果操作不當就很容易產生意想不到的錯誤,比如常見的搶票程序:
public class Demo1 {
public static void main(String[] args) {
Ticket tt = new Ticket();
new Thread(tt, "甲").start();
new Thread(tt, "乙").start();
}
}
class Ticket implements Runnable {
private int ticketCount = 10;
@Override
public void run() {
while (ticketCount > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "搶到了第【" + (10 - ticketCount + 1) + "】張火車票");
ticketCount--;
}
}
}
結果:
乙搶到了第【1】張火車票
甲搶到了第【2】張火車票
乙搶到了第【3】張火車票
甲搶到了第【4】張火車票
乙搶到了第【5】張火車票
甲搶到了第【6】張火車票
乙搶到了第【7】張火車票
甲搶到了第【8】張火車票
乙搶到了第【9】張火車票
甲搶到了第【9】張火車票
乙搶到了第【11】張火車票
以上代碼可以看出,第9張票被兩個人搶,且出現了第11張票,這不符合實際。爲什麼會出現這種現象?原因是當乙搶到第9張票但還未執行ticketCount–;語句時,甲也進入了run()方法,此時票數仍然是第9張票,也打印出了第9票,因此第9張票被搶了兩遍。之後甲和乙都會執行ticketCount–;語句,當ticketCount =0時,無論哪個線程搶到資源都會打印搶到了第【11】張火車票,當ticketCount = -1時,停止。
綜上分析,問題出現的主要原因就是甲、乙兩個線程同時訪問同一資源,造成了資源污染。
二、線程同步
上面可知當多個線程同時訪問同一資源,就可能會造成了資源污染,解決這個問題的方法就是在某個線程訪問資源時,其他線程在資源或者方法外面等待,也就是線程同步,或者叫加鎖。
線程同步就是指多個操作在同一時間段內只能有一個線程進行,而其他線程要等待此線程完成之後纔可以繼續進行。
要實現線程同步,需要通過關鍵字synchronized關鍵字,利用這個關鍵字可以定義同步方法或者代碼塊。格式如下:
synchronized(同步對象){
操作;
}
一般要進行同步對象處理時,採用當前對象this進行同步。上面的搶票代碼加上同步後如下:
public class Demo1 {
public static void main(String[] args) {
Ticket tt = new Ticket();
new Thread(tt, "甲").start();
new Thread(tt, "乙").start();
}
}
class Ticket implements Runnable {
private int ticketCount = 10;
@Override
public void run() {
synchronized(this){
while (ticketCount > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "搶到了第【" + (10 - ticketCount + 1) + "】張火車票");
ticketCount--;
}
}
}
}
結果:
甲搶到了第【1】張火車票
甲搶到了第【2】張火車票
甲搶到了第【3】張火車票
甲搶到了第【4】張火車票
甲搶到了第【5】張火車票
甲搶到了第【6】張火車票
甲搶到了第【7】張火車票
甲搶到了第【8】張火車票
甲搶到了第【9】張火車票
甲搶到了第【10】張火車票
加鎖或者同步處理以後,雖然多線程同時訪問同一資源的問題雖然解決了,但是線程同步會降低整體性能。
三、死鎖
死鎖就是多個線程間相互等待的狀態。
class MyThread implements Runnable{
int flag = 1;
// 必須是靜態資源
static Object o1 = new Object();
static Object o2 = new Object();
@Override
public void run() {
System.out.println("flag= " + flag);
if(flag == 1){
synchronized (o1){
System.out.println(Thread.currentThread().getName() + "我搶到了o1,還需要o2");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println("111");
}
}
}
if(flag == 0){
synchronized (o2){
System.out.println(Thread.currentThread().getName() + "我搶到了o2,還需要o1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("222");
}
}
}
}
}
public class DeadLock {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
// 設置線程1先搶佔o1
myThread1.flag = 1;
// 設置線程1先搶佔o2
myThread2.flag = 0;
Thread t1 = new Thread(myThread1);
Thread t2 = new Thread(myThread2);
t1.start();
t2.start();
}
}
有兩個線程,都需要鎖住同樣的2個對象(a、b)才能完成操作,其中線程1已經鎖住了a對象,線程2鎖住了b對象,兩個線程都不釋放鎖就會造成死鎖。程序無法停止。
四、生產者和消費者模式
生產者和消費者模式是用來解決死鎖問題的。
爲什麼可以解決呢?
生產者和消費者模式中,一種是生產者線程用於生產數據,另一種是消費者線程用於消費數據,爲了解耦生產者和消費者的關係,通常會採用共享的數據區域,就像是一個倉庫,生產者生產數據之後直接放置在共享數據區中,並不需要關心消費者的行爲;而消費者只需要從共享數據區中去獲取數據,就不再需要關心生產者的行爲。但是,這個共享數據區域中應該具備這樣的線程間併發協作的功能,需要根據信號來生產或獲取:當生產者線程生產物品時,如果沒有空緩衝區可用,那麼生產者線程必須等待消費者線程釋放出一個空緩衝區。當消費者線程消費物品時,如果沒有滿的緩衝區,那麼消費者線程將被阻塞,直到新的物品被生產出來。這裏實際體現了信號燈法:flag = true,生產者生產,消費者等待;反之,生產者等待,消費者生產。
下面以蒸包子和喫包子爲例,廚師作爲生產者輸出包子,喫包子的作爲消費者,如果沒有采用生產者消費者模式,那就可能出現喫包子的沒包子喫或者蒸包子的產能過剩的情況;而採用生產者消費者模式後,廚師蒸好包子後不是接着蒸,而是把蒸好的包子放在籃子裏然後停下來告訴消費者可以吃了,然後消費者接到信號開始從籃子裏拿包子喫,直到喫完後會給廚師一個信號,告訴他可以接着蒸包子了,這時消費者停下,廚師開始蒸包子,接着循環下去。
/**
* 裝包子的籃子類
*/
class Container{
private String baozi;
// 籃子狀態信號標誌:flag = true,籃子空了,生產者生產;flag = false,籃子滿了,消費者消費
private boolean flag = true;
// 模擬生產過程,生產過程要加鎖,防止生產過程中消費者過來消費
public synchronized void play(String baozi) throws InterruptedException {
// 生產者等待
if(!flag){
this.wait();
}
// 生產者生產
Thread.sleep(500); // 模擬生產耗時
// 包子蒸好了
this.baozi = baozi;
System.out.println("生產" + baozi);
// 通知消費者來喫
this.notify();
// 停止生產
this.flag = false;
}
// 模擬消費過程,消費過程要加鎖,防止消費過程中生產者過來生產
public synchronized void eat() throws InterruptedException {
// 消費者等待
if(flag){
this.wait();
}
// 消費者消費
Thread.sleep(100); // 模擬消費耗時
System.out.println("籃子裏的包子已經喫完了");
// 通知生產者需要蒸包子了
this.notify();
// 停止喫包子
this.flag = true;
}
}
/**
* 定義生產者
*/
class Player implements Runnable{
private Container container;
public Player(Container container) {
this.container = container;
}
// 單日生產素餡的包子,雙日生產肉餡的包子
@Override
public void run() {
for (int i = 0; i < 6; i++) {
if(i%2 == 0){
try {
container.play("肉餡的包子");
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
try {
container.play("素餡的包子");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 定義消費者
*/
class Consumer implements Runnable{
private Container container;
public Consumer(Container container) {
this.container = container;
}
// 不管啥餡的包子我都喫
@Override
public void run() {
for (int i = 0; i < 6; i++) {
try {
container.eat();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class CpDemo {
public static void main(String[] args) {
// 同一個籃子
Container container = new Container();
Player player = new Player(container);
Consumer consumer = new Consumer(container);
new Thread(player, "生產者").start();
new Thread(consumer, "消費者").start();
}
}
生產肉餡的包子
籃子裏的包子已經喫完了
生產素餡的包子
籃子裏的包子已經喫完了
生產肉餡的包子
籃子裏的包子已經喫完了
生產素餡的包子
籃子裏的包子已經喫完了
生產肉餡的包子
籃子裏的包子已經喫完了
生產素餡的包子
籃子裏的包子已經喫完了
以上,生產者線程和消費者線程交替進行,就能很好的避免死鎖問題。
五、volatile和synchronized的區別
volatile關鍵字主要是在屬性定義上使用,表示此屬性爲直接數據操作,而不進行副本的拷貝處理。
volatile和synchronized的區別:
- volatile主要是在屬性定義上使用,而synchronized是在代碼塊或方法上使用
- volatile無法描述同步處理,是一種直接內存處理,避免了副本操作;synchronized是同步操作。
六、sleep和wait的區別
① 這兩個方法來自不同的類分別是,sleep來自Thread類,和wait來自Object類。
sleep是Thread的靜態類方法,誰調用的誰去睡覺,即使在a線程裏調用b的sleep方法,實際上還是a去睡覺,要讓b線程睡覺要在b的代碼中調用sleep。
② 鎖: sleep方法不會釋放lock,但是wait會釋放,而且會加入到等待隊列中,使得其他線程可以使用同步控制塊或者方法。
sleep不需要被喚醒(休眠之後推出阻塞),但是wait需要(不指定時間需要被別人中斷)。sleep不出讓系統資源;wait是進入線程等待池等待,出讓系統資源,其他線程可以佔用CPU。一般wait不會加時間限制,因爲如果wait線程的運行資源不夠,再出來也沒用,要等待其他線程調用notify/notifyAll喚醒等待池中的所有線程,纔會進入就緒隊列等待OS分配系統資源。sleep(milliseconds)可以用時間指定使它自動喚醒過來,如果時間不到只能調用interrupt()強行打斷。
③ 使用範圍:wait,notify和notifyAll只能在同步控制方法或者同步控制塊裏面使用,而sleep可以在任何地方使用。sleep方法不依賴於同步器synchronized,但是wait需要依賴synchronized關鍵字。
七、notify和notifyAll的區別?
先要理解鎖池和等待池:
- 鎖池:假設線程A已經擁有了某個對象(注意:不是類)的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),由於這些線程在進入對象的synchronized方法之前必須先獲得該對象的鎖的擁有權,但是該對象的鎖目前正被線程A擁有,所以這些線程就進入了該對象的鎖池中。
- 等待池:假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖後,進入到了該對象的等待池中
notify和notifyAll的區別
- 如果線程調用了對象的 wait()方法,那麼線程便會處於該對象的等待池中,等待池中的線程不會去競爭該對象的鎖。
當有線程調用了對象的 notifyAll()方法(喚醒所有 wait 線程)或 - notify()方法(只隨機喚醒一個 wait 線程),被喚醒的的線程便會進入該對象的鎖池中,鎖池中的線程會去競爭該對象鎖。也就是說,調用了notify後只要一個線程會由等待池進入鎖池,而notifyAll會將該對象等待池內的所有線程移動到鎖池中,等待鎖競爭
- 優先級高的線程競爭到對象鎖的概率大,假若某線程沒有競爭到該對象鎖,它還會留在鎖池中,唯有線程再次調用 wait()方法,它纔會重新回到等待池中。而競爭到對象鎖的線程則繼續往下執行,直到執行完了 synchronized 代碼塊,它會釋放掉該對象鎖,這時鎖池中的線程會繼續競爭該對象鎖。